diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 50f532ab..b904660c 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -13,27 +13,29 @@ jobs:
steps:
- name: Check out Git repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- - name: Set up JDK 1.8
- uses: actions/setup-java@v1
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
with:
java-version: 11
+ distribution: 'zulu'
- name: Build With Maven
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
+ run: mvn -B verify #org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
# Comment "Build With Maven" and uncomment the below when you want a snapshot build to be deployed
# *********Don't forget to switch to Java 1.8 as well********
-# - name: Publish Maven snapshot
-# uses: samuelmeuli/action-maven-publish@v1
-# with:
-# gpg_private_key: ${{ secrets.gpg_private_key }}
-# gpg_passphrase: ${{ secrets.gpg_passphrase }}
-# nexus_username: ${{ secrets.nexus_username }}
-# nexus_password: ${{ secrets.nexus_password }}
-# maven_profiles: snapshot-release
+ - name: Publish Maven snapshot
+ uses: samuelmeuli/action-maven-publish@v1
+ with:
+ gpg_private_key: ${{ secrets.gpg_private_key }}
+ gpg_passphrase: ${{ secrets.gpg_passphrase }}
+ nexus_username: ${{ secrets.nexus_username }}
+ nexus_password: ${{ secrets.nexus_password }}
+ maven_profiles: snapshot-release
+ maven_args: -B
diff --git a/cost-benefit-calculator/pom.xml b/cost-benefit-calculator/pom.xml
index 5f1a4188..b8dbff91 100644
--- a/cost-benefit-calculator/pom.xml
+++ b/cost-benefit-calculator/pom.xml
@@ -12,6 +12,13 @@
cost-benefit-calculator
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.7
+
+
org.hjug.refactorfirst.changepronenessrankerchange-proneness-ranker
diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java
index 04f6ae7c..895b860b 100644
--- a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java
+++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java
@@ -1,12 +1,19 @@
package org.hjug.cbc;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
+import static net.sourceforge.pmd.RuleViolation.CLASS_NAME;
+import static net.sourceforge.pmd.RuleViolation.PACKAGE_NAME;
+
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
+import net.sourceforge.pmd.*;
+import net.sourceforge.pmd.lang.LanguageRegistry;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.hjug.git.ChangePronenessRanker;
@@ -14,11 +21,36 @@
import org.hjug.git.RepositoryLogReader;
import org.hjug.git.ScmLogInfo;
import org.hjug.metrics.*;
+import org.hjug.metrics.rules.CBORule;
@Slf4j
public class CostBenefitCalculator {
- Map filesToScan = new HashMap<>();
+ private Report report;
+ private String projBaseDir = null;
+
+ // copied from PMD's PmdTaskImpl.java and modified
+ public void runPmdAnalysis(String projectBaseDir) throws IOException {
+ projBaseDir = projectBaseDir;
+ PMDConfiguration configuration = new PMDConfiguration();
+
+ try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {
+ RuleSetLoader rulesetLoader = pmd.newRuleSetLoader();
+ pmd.addRuleSets(rulesetLoader.loadRuleSetsWithoutException(List.of("category/java/design.xml")));
+
+ Rule cboClassRule = new CBORule();
+ cboClassRule.setLanguage(LanguageRegistry.PMD.getLanguageByFullName("Java"));
+ pmd.addRuleSet(RuleSet.forSingleRule(cboClassRule));
+
+ log.info("files to be scanned: " + Paths.get(projectBaseDir));
+
+ try (Stream files = Files.walk(Paths.get(projectBaseDir))) {
+ files.forEach(file -> pmd.files().addFile(file));
+ }
+
+ report = pmd.performAnalysisAndCollectReport();
+ }
+ }
public List calculateGodClassCostBenefitValues(String repositoryPath) {
@@ -27,11 +59,16 @@ public List calculateGodClassCostBenefitValues(String reposito
log.info("Initiating Cost Benefit calculation");
try {
repository = repositoryLogReader.gitRepository(new File(repositoryPath));
+ for (String file :
+ repositoryLogReader.listRepositoryContentsAtHEAD(repository).keySet()) {
+ log.info("Files at HEAD: {}", file);
+ }
} catch (IOException e) {
log.error("Failure to access Git repository", e);
}
- List godClasses = getGodClasses(getFilesToScan(repositoryLogReader, repository));
+ // pass repo path here, not ByteArrayOutputStream
+ List godClasses = getGodClasses();
List scmLogInfos = getRankedChangeProneness(repositoryLogReader, repository, godClasses);
@@ -40,26 +77,63 @@ public List calculateGodClassCostBenefitValues(String reposito
List rankedDisharmonies = new ArrayList<>();
for (GodClass godClass : godClasses) {
- rankedDisharmonies.add(new RankedDisharmony(godClass, rankedLogInfosByPath.get(godClass.getFileName())));
+ if (rankedLogInfosByPath.containsKey(godClass.getFileName())) {
+ rankedDisharmonies.add(
+ new RankedDisharmony(godClass, rankedLogInfosByPath.get(godClass.getFileName())));
+ }
+ }
+
+ rankedDisharmonies.sort(
+ Comparator.comparing(RankedDisharmony::getRawPriority).reversed());
+
+ int godClassPriority = 1;
+ for (RankedDisharmony rankedGodClassDisharmony : rankedDisharmonies) {
+ rankedGodClassDisharmony.setPriority(godClassPriority++);
}
return rankedDisharmonies;
}
+ private List getGodClasses() {
+ List godClasses = new ArrayList<>();
+ for (RuleViolation violation : report.getViolations()) {
+ if (violation.getRule().getName().contains("GodClass")) {
+ GodClass godClass = new GodClass(
+ violation.getAdditionalInfo().get(CLASS_NAME),
+ getFileName(violation),
+ violation.getAdditionalInfo().get(PACKAGE_NAME),
+ violation.getDescription());
+ log.info("God Class identified: {}", godClass.getFileName());
+ godClasses.add(godClass);
+ }
+ }
+
+ GodClassRanker godClassRanker = new GodClassRanker();
+ godClassRanker.rankGodClasses(godClasses);
+
+ return godClasses;
+ }
+
List getRankedChangeProneness(
RepositoryLogReader repositoryLogReader, Repository repository, List disharmonies) {
List scmLogInfos = new ArrayList<>();
- log.info("Calculating Change Proneness for each God Class");
+ log.info("Calculating Change Proneness");
for (Disharmony disharmony : disharmonies) {
String path = disharmony.getFileName();
ScmLogInfo scmLogInfo = null;
try {
scmLogInfo = repositoryLogReader.fileLog(repository, path);
+ log.info("Successfully fetched scmLogInfo for {}", scmLogInfo.getPath());
} catch (GitAPIException | IOException e) {
- log.error("Error reading Git repository contents", e);
+ log.error("Error reading Git repository contents.", e);
+ } catch (NullPointerException e) {
+ log.error("Encountered nested class in a class containing a violation. Class: {}", path);
}
- scmLogInfos.add(scmLogInfo);
+ if (null != scmLogInfo) {
+ log.info("adding {}", scmLogInfo.getPath());
+ scmLogInfos.add(scmLogInfo);
+ }
}
ChangePronenessRanker changePronenessRanker = new ChangePronenessRanker(repository, repositoryLogReader);
@@ -67,25 +141,6 @@ List getRankedChangeProneness(
return scmLogInfos;
}
- private List getGodClasses(Map filesToScan) {
- PMDGodClassRuleRunner ruleRunner = new PMDGodClassRuleRunner();
-
- log.info("Identifying God Classes from files in repository");
- List godClasses = new ArrayList<>();
- for (Map.Entry entry : filesToScan.entrySet()) {
- String filePath = entry.getKey();
- ByteArrayOutputStream value = entry.getValue();
-
- ByteArrayInputStream inputStream = new ByteArrayInputStream(value.toByteArray());
- Optional godClassOptional = ruleRunner.runGodClassRule(filePath, inputStream);
- godClassOptional.ifPresent(godClasses::add);
- }
-
- GodClassRanker godClassRanker = new GodClassRanker();
- godClassRanker.rankGodClasses(godClasses);
- return godClasses;
- }
-
public List calculateCBOCostBenefitValues(String repositoryPath) {
RepositoryLogReader repositoryLogReader = new GitLogReader();
@@ -97,7 +152,7 @@ public List calculateCBOCostBenefitValues(String repositoryPat
log.error("Failure to access Git repository", e);
}
- List cboClasses = getCBOClasses(getFilesToScan(repositoryLogReader, repository));
+ List cboClasses = getCBOClasses();
List scmLogInfos = getRankedChangeProneness(repositoryLogReader, repository, cboClasses);
@@ -109,37 +164,35 @@ public List calculateCBOCostBenefitValues(String repositoryPat
rankedDisharmonies.add(new RankedDisharmony(cboClass, rankedLogInfosByPath.get(cboClass.getFileName())));
}
- return rankedDisharmonies;
- }
+ rankedDisharmonies.sort(
+ Comparator.comparing(RankedDisharmony::getRawPriority).reversed());
- private List getCBOClasses(Map filesToScan) {
+ int cboPriority = 1;
+ for (RankedDisharmony rankedCBODisharmony : rankedDisharmonies) {
+ rankedCBODisharmony.setPriority(cboPriority++);
+ }
- CBORuleRunner ruleRunner = new CBORuleRunner();
+ return rankedDisharmonies;
+ }
- log.info("Identifying highly coupled classes from files in repository");
+ private List getCBOClasses() {
List cboClasses = new ArrayList<>();
- for (Map.Entry entry : filesToScan.entrySet()) {
- String filePath = entry.getKey();
- ByteArrayOutputStream value = entry.getValue();
-
- ByteArrayInputStream inputStream = new ByteArrayInputStream(value.toByteArray());
- Optional godClassOptional = ruleRunner.runCBOClassRule(filePath, inputStream);
- godClassOptional.ifPresent(cboClasses::add);
+ for (RuleViolation violation : report.getViolations()) {
+ if (violation.getRule().getName().contains("CBORule")) {
+ log.info(violation.getDescription());
+ CBOClass godClass = new CBOClass(
+ violation.getAdditionalInfo().get(CLASS_NAME),
+ getFileName(violation),
+ violation.getAdditionalInfo().get(PACKAGE_NAME),
+ violation.getDescription());
+ log.info("Highly Coupled class identified: {}", godClass.getFileName());
+ cboClasses.add(godClass);
+ }
}
-
return cboClasses;
}
- private Map getFilesToScan(
- RepositoryLogReader repositoryLogReader, Repository repository) {
-
- try {
- if (filesToScan.isEmpty()) {
- filesToScan = repositoryLogReader.listRepositoryContentsAtHEAD(repository);
- }
- } catch (IOException e) {
- log.error("Error reading Git repository contents", e);
- }
- return filesToScan;
+ private String getFileName(RuleViolation violation) {
+ return violation.getFileId().getUriString().replace("file:///" + projBaseDir.replace("\\", "/") + "/", "");
}
}
diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedDisharmony.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedDisharmony.java
index 75d39e69..6cba952f 100644
--- a/cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedDisharmony.java
+++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedDisharmony.java
@@ -16,7 +16,7 @@ public class RankedDisharmony {
private final Integer effortRank;
private final Integer changePronenessRank;
private final Integer rawPriority;
- private Integer priority;
+ private Integer priority = 0;
private Integer wmc;
private Integer wmcRank;
diff --git a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java
index 6649a25a..246d24c6 100644
--- a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java
+++ b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java
@@ -8,20 +8,19 @@
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.hjug.git.GitLogReader;
-import org.hjug.metrics.GodClass;
-import org.hjug.metrics.PMDGodClassRuleRunner;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
-public class CostBenefitCalculatorTest {
+class CostBenefitCalculatorTest {
@TempDir
public File tempFolder;
+ private String faceletsPath = "org/apache/myfaces/tobago/facelets/";
+ private String hudsonPath = "hudson/model/";
private Git git;
private Repository repository;
@@ -29,6 +28,8 @@ public class CostBenefitCalculatorTest {
public void setUp() throws GitAPIException {
git = Git.init().setDirectory(tempFolder).call();
repository = git.getRepository();
+ new File(tempFolder.getPath() + "/" + faceletsPath).mkdirs();
+ new File(tempFolder.getPath() + "/" + hudsonPath).mkdirs();
}
@AfterEach
@@ -37,88 +38,64 @@ public void tearDown() {
}
@Test
- void testCostBenefitCalculation() throws IOException, GitAPIException, InterruptedException {
- String attributeHandler = "AttributeHandler.java";
- InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler);
- writeFile(attributeHandler, convertInputStreamToString(resourceAsStream));
+ void testCBOViolation() throws IOException, GitAPIException, InterruptedException {
+ // Has CBO violation
+ String user = "User.java";
+ InputStream userResourceAsStream = getClass().getClassLoader().getResourceAsStream(hudsonPath + user);
+ writeFile(hudsonPath + user, convertInputStreamToString(userResourceAsStream));
git.add().addFilepattern(".").call();
RevCommit firstCommit = git.commit().setMessage("message").call();
- // Sleeping for one second to guarantee commits have different time stamps
- Thread.sleep(1000);
-
- // write contents of updated file to original file
- InputStream resourceAsStream2 = getClass().getClassLoader().getResourceAsStream("AttributeHandler2.java");
- writeFile(attributeHandler, convertInputStreamToString(resourceAsStream2));
-
- InputStream resourceAsStream3 =
- getClass().getClassLoader().getResourceAsStream("AttributeHandlerAndSorter.java");
- writeFile("AttributeHandlerAndSorter.java", convertInputStreamToString(resourceAsStream3));
-
- git.add().addFilepattern(".").call();
- RevCommit secondCommit = git.commit().setMessage("message").call();
-
CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator();
- List disharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(
+ costBenefitCalculator.runPmdAnalysis(git.getRepository().getDirectory().getParent());
+ List disharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(
git.getRepository().getDirectory().getPath());
- Assertions.assertEquals(0, disharmonies.get(0).getPriority().intValue());
- Assertions.assertEquals(0, disharmonies.get(1).getPriority().intValue());
+ Assertions.assertFalse(disharmonies.isEmpty());
}
@Test
- void scanClassesInRepo2() throws IOException, GitAPIException {
+ void testCostBenefitCalculation() throws IOException, GitAPIException, InterruptedException {
+
String attributeHandler = "AttributeHandler.java";
- InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler);
- writeFile(attributeHandler, convertInputStreamToString(resourceAsStream));
+ InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(faceletsPath + attributeHandler);
+ writeFile(faceletsPath + attributeHandler, convertInputStreamToString(resourceAsStream));
git.add().addFilepattern(".").call();
- git.commit().setMessage("message").call();
-
- GitLogReader gitLogReader = new GitLogReader();
- Map filesToScan = gitLogReader.listRepositoryContentsAtHEAD(repository);
-
- PMDGodClassRuleRunner ruleRunner = new PMDGodClassRuleRunner();
+ RevCommit firstCommit = git.commit().setMessage("message").call();
- Map godClasses = new HashMap<>();
- for (String filePath : filesToScan.keySet()) {
- ByteArrayInputStream inputStream =
- new ByteArrayInputStream(filesToScan.get(filePath).toByteArray());
- Optional godClassOptional = ruleRunner.runGodClassRule(filePath, inputStream);
- godClassOptional.ifPresent(godClass -> godClasses.put(filePath, godClass));
- }
+ // Sleeping for one second to guarantee commits have different time stamps
+ Thread.sleep(1000);
- Assertions.assertFalse(godClasses.isEmpty());
- }
+ // write contents of updated file to original file
+ InputStream resourceAsStream2 =
+ getClass().getClassLoader().getResourceAsStream(faceletsPath + "AttributeHandler2.java");
+ writeFile(faceletsPath + attributeHandler, convertInputStreamToString(resourceAsStream2));
- @Test
- void scanClassesInRepo() throws IOException, GitAPIException {
- String attributeHandler = "AttributeHandler.java";
- InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler);
- writeFile(attributeHandler, convertInputStreamToString(resourceAsStream));
+ InputStream resourceAsStream3 =
+ getClass().getClassLoader().getResourceAsStream(faceletsPath + "AttributeHandlerAndSorter.java");
+ writeFile(faceletsPath + "AttributeHandlerAndSorter.java", convertInputStreamToString(resourceAsStream3));
git.add().addFilepattern(".").call();
- git.commit().setMessage("message").call();
-
- GitLogReader gitLogReader = new GitLogReader();
- Map filesToScan = gitLogReader.listRepositoryContentsAtHEAD(repository);
+ RevCommit secondCommit = git.commit().setMessage("message").call();
- PMDGodClassRuleRunner ruleRunner = new PMDGodClassRuleRunner();
+ CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator();
+ costBenefitCalculator.runPmdAnalysis(git.getRepository().getDirectory().getParent());
+ List disharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(
+ git.getRepository().getDirectory().getPath());
- Map godClasses = new HashMap<>();
- for (String filePath : filesToScan.keySet()) {
- ByteArrayInputStream inputStream =
- new ByteArrayInputStream(filesToScan.get(filePath).toByteArray());
- Optional godClassOptional = ruleRunner.runGodClassRule(filePath, inputStream);
- godClassOptional.ifPresent(godClass -> godClasses.put(filePath, godClass));
- }
+ Assertions.assertEquals(1, disharmonies.get(0).getRawPriority().intValue());
+ Assertions.assertEquals(1, disharmonies.get(1).getRawPriority().intValue());
- Assertions.assertFalse(godClasses.isEmpty());
+ Assertions.assertEquals(1, disharmonies.get(0).getPriority().intValue());
+ Assertions.assertEquals(2, disharmonies.get(1).getPriority().intValue());
}
private void writeFile(String name, String content) throws IOException {
+ // Files.writeString(Path.of(git.getRepository().getWorkTree().getPath()), content);
File file = new File(git.getRepository().getWorkTree(), name);
+
try (FileOutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(content.getBytes(UTF_8));
}
diff --git a/cost-benefit-calculator/src/test/resources/hudson/model/User.java b/cost-benefit-calculator/src/test/resources/hudson/model/User.java
new file mode 100644
index 00000000..8f9ca606
--- /dev/null
+++ b/cost-benefit-calculator/src/test/resources/hudson/model/User.java
@@ -0,0 +1,1276 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt,
+ * Tom Huybrechts, Vincent Latombe, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.model;
+
+import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import hudson.BulkChange;
+import hudson.CopyOnWrite;
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.ExtensionPoint;
+import hudson.Util;
+import hudson.XmlFile;
+import hudson.init.InitMilestone;
+import hudson.init.Initializer;
+import hudson.model.Descriptor.FormException;
+import hudson.model.listeners.SaveableListener;
+import hudson.security.ACL;
+import hudson.security.AccessControlled;
+import hudson.security.SecurityRealm;
+import hudson.security.UserMayOrMayNotExistException2;
+import hudson.util.FormApply;
+import hudson.util.FormValidation;
+import hudson.util.RunList;
+import hudson.util.XStream2;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Predicate;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+import jenkins.model.IdStrategy;
+import jenkins.model.Jenkins;
+import jenkins.model.ModelObjectWithContextMenu;
+import jenkins.scm.RunWithSCM;
+import jenkins.security.ImpersonatingUserDetailsService2;
+import jenkins.security.LastGrantedAuthoritiesProperty;
+import jenkins.security.UserDetailsCache;
+import jenkins.util.SystemProperties;
+import net.sf.json.JSONObject;
+import org.apache.commons.lang.StringUtils;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.StaplerProxy;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.export.Exported;
+import org.kohsuke.stapler.export.ExportedBean;
+import org.kohsuke.stapler.interceptor.RequirePOST;
+import org.kohsuke.stapler.verb.POST;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+/**
+ * Represents a user.
+ *
+ *
+ * In Hudson, {@link User} objects are created in on-demand basis;
+ * for example, when a build is performed, its change log is computed
+ * and as a result commits from users who Hudson has never seen may be discovered.
+ * When this happens, new {@link User} object is created.
+ *
+ *
+ * If the persisted record for an user exists, the information is loaded at
+ * that point, but if there's no such record, a fresh instance is created from
+ * thin air (this is where {@link UserPropertyDescriptor#newInstance(User)} is
+ * called to provide initial {@link UserProperty} objects.
+ *
+ *
+ * Such newly created {@link User} objects will be simply GC-ed without
+ * ever leaving the persisted record, unless {@link User#save()} method
+ * is explicitly invoked (perhaps as a result of a browser submitting a
+ * configuration.)
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@ExportedBean
+public class User extends AbstractModelObject implements AccessControlled, DescriptorByNameOwner, Saveable, Comparable, ModelObjectWithContextMenu, StaplerProxy {
+
+ public static final XStream2 XSTREAM = new XStream2();
+ private static final Logger LOGGER = Logger.getLogger(User.class.getName());
+ static final String CONFIG_XML = "config.xml";
+
+ /**
+ * Escape hatch for StaplerProxy-based access control
+ */
+ @Restricted(NoExternalUse.class)
+ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")
+ public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = SystemProperties.getBoolean(User.class.getName() + ".skipPermissionCheck");
+
+ /**
+ * Jenkins now refuses to let the user login if he/she doesn't exist in {@link SecurityRealm},
+ * which was necessary to make sure users removed from the backend will get removed from the frontend.
+ *
+ * Unfortunately this infringed some legitimate use cases of creating Jenkins-local users for
+ * automation purposes. This escape hatch switch can be enabled to resurrect that behaviour.
+ *
+ * See JENKINS-22346.
+ */
+ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")
+ public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName() + ".allowNonExistentUserToLogin");
+
+ /**
+ * Jenkins historically created a (usually) ephemeral user record when an user with Overall/Administer permission
+ * accesses a /user/arbitraryName URL.
+ *
+ * Unfortunately this constitutes a CSRF vulnerability, as malicious users can make admins create arbitrary numbers
+ * of ephemeral user records, so the behavior was changed in Jenkins 2.44 / 2.32.2.
+ *
+ * As some users may be relying on the previous behavior, setting this to true restores the previous behavior. This
+ * is not recommended.
+ *
+ * SECURITY-406.
+ */
+ @Restricted(NoExternalUse.class)
+ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")
+ public static boolean ALLOW_USER_CREATION_VIA_URL = SystemProperties.getBoolean(User.class.getName() + ".allowUserCreationViaUrl");
+
+ /**
+ * The username of the 'unknown' user used to avoid null user references.
+ */
+ private static final String UNKNOWN_USERNAME = "unknown";
+
+ /**
+ * These usernames should not be used by real users logging into Jenkins. Therefore, we prevent
+ * users with these names from being saved.
+ */
+ private static final String[] ILLEGAL_PERSISTED_USERNAMES = new String[]{ACL.ANONYMOUS_USERNAME,
+ ACL.SYSTEM_USERNAME, UNKNOWN_USERNAME};
+
+ private final int version = 10; // Not currently used, but it may be helpful in the future to store a version.
+ private String id;
+ private volatile String fullName;
+ private volatile String description;
+
+ @CopyOnWrite
+ private volatile List properties = new ArrayList<>();
+
+ static {
+ XSTREAM.alias("user", User.class);
+ }
+
+ private User(String id, String fullName) {
+ this.id = id;
+ this.fullName = fullName;
+ load(id);
+ }
+
+ private void load(String userId) {
+ clearExistingProperties();
+ loadFromUserConfigFile(userId);
+ removeNullsThatFailedToLoad();
+ allocateDefaultPropertyInstancesAsNeeded();
+ setUserToProperties();
+ }
+
+ private void setUserToProperties() {
+ for (UserProperty p : properties) {
+ p.setUser(this);
+ }
+ }
+
+ private void allocateDefaultPropertyInstancesAsNeeded() {
+ for (UserPropertyDescriptor d : UserProperty.all()) {
+ if (getProperty(d.clazz) == null) {
+ UserProperty up = d.newInstance(this);
+ if (up != null)
+ properties.add(up);
+ }
+ }
+ }
+
+ private void removeNullsThatFailedToLoad() {
+ properties.removeIf(Objects::isNull);
+ }
+
+ private void loadFromUserConfigFile(String userId) {
+ XmlFile config = getConfigFile();
+ try {
+ if (config != null && config.exists()) {
+ config.unmarshal(this);
+ this.id = userId;
+ }
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "Failed to load " + config, e);
+ }
+ }
+
+ private void clearExistingProperties() {
+ properties.clear();
+ }
+
+ private XmlFile getConfigFile() {
+ File existingUserFolder = getExistingUserFolder();
+ return existingUserFolder == null ? null : new XmlFile(XSTREAM, new File(existingUserFolder, CONFIG_XML));
+ }
+
+ /**
+ * Returns the {@link jenkins.model.IdStrategy} for use with {@link User} instances. See
+ * {@link hudson.security.SecurityRealm#getUserIdStrategy()}
+ *
+ * @return the {@link jenkins.model.IdStrategy} for use with {@link User} instances.
+ * @since 1.566
+ */
+ @NonNull
+ public static IdStrategy idStrategy() {
+ Jenkins j = Jenkins.get();
+ SecurityRealm realm = j.getSecurityRealm();
+ if (realm == null) {
+ return IdStrategy.CASE_INSENSITIVE;
+ }
+ return realm.getUserIdStrategy();
+ }
+
+ @Override
+ public int compareTo(@NonNull User that) {
+ return idStrategy().compare(this.id, that.id);
+ }
+
+ @Exported
+ public String getId() {
+ return id;
+ }
+
+ public @NonNull String getUrl() {
+ return "user/" + Util.rawEncode(idStrategy().keyFor(id));
+ }
+
+ @Override
+ public @NonNull String getSearchUrl() {
+ return "/user/" + Util.rawEncode(idStrategy().keyFor(id));
+ }
+
+ /**
+ * The URL of the user page.
+ */
+ @Exported(visibility = 999)
+ public @NonNull String getAbsoluteUrl() {
+ return Jenkins.get().getRootUrl() + getUrl();
+ }
+
+ /**
+ * Gets the human readable name of this user.
+ * This is configurable by the user.
+ */
+ @Exported(visibility = 999)
+ public @NonNull String getFullName() {
+ return fullName;
+ }
+
+ /**
+ * Sets the human readable name of the user.
+ * If the input parameter is empty, the user's ID will be set.
+ */
+ public void setFullName(String name) {
+ if (Util.fixEmptyAndTrim(name) == null) name = id;
+ this.fullName = name;
+ }
+
+ @Exported
+ public @CheckForNull String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets the description of the user.
+ *
+ * @since 1.609
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * Gets the user properties configured for this user.
+ */
+ public Map, UserProperty> getProperties() {
+ return Descriptor.toMap(properties);
+ }
+
+ /**
+ * Updates the user object by adding a property.
+ */
+ public synchronized void addProperty(@NonNull UserProperty p) throws IOException {
+ UserProperty old = getProperty(p.getClass());
+ List ps = new ArrayList<>(properties);
+ if (old != null)
+ ps.remove(old);
+ ps.add(p);
+ p.setUser(this);
+ properties = ps;
+ save();
+ }
+
+ /**
+ * List of all {@link UserProperty}s exposed primarily for the remoting API.
+ */
+ @Exported(name = "property", inline = true)
+ public List getAllProperties() {
+ if (hasPermission(Jenkins.ADMINISTER)) {
+ return Collections.unmodifiableList(properties);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Gets the specific property, or null.
+ */
+ public T getProperty(Class clazz) {
+ for (UserProperty p : properties) {
+ if (clazz.isInstance(p))
+ return clazz.cast(p);
+ }
+ return null;
+ }
+
+ /**
+ * Creates an {@link Authentication} object that represents this user.
+ *
+ * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm.
+ * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will
+ * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has
+ * logged in.
+ *
+ * @throws UsernameNotFoundException If this user is not a valid user in the backend {@link SecurityRealm}.
+ * @since 2.266
+ */
+ public @NonNull Authentication impersonate2() throws UsernameNotFoundException {
+ return this.impersonate(this.getUserDetailsForImpersonation2());
+ }
+
+ /**
+ * @deprecated use {@link #impersonate2}
+ * @since 1.419
+ */
+ @Deprecated
+ public @NonNull org.acegisecurity.Authentication impersonate() throws org.acegisecurity.userdetails.UsernameNotFoundException {
+ try {
+ return org.acegisecurity.Authentication.fromSpring(impersonate2());
+ } catch (AuthenticationException x) {
+ throw org.acegisecurity.AuthenticationException.fromSpring(x);
+ }
+ }
+
+ /**
+ * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm.
+ * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will
+ * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has
+ * logged in.
+ *
+ * @return userDetails for the user, in case he's not found but seems legitimate, we provide a userDetails with minimum access
+ * @throws UsernameNotFoundException If this user is not a valid user in the backend {@link SecurityRealm}.
+ * @since 2.266
+ */
+ public @NonNull UserDetails getUserDetailsForImpersonation2() throws UsernameNotFoundException {
+ ImpersonatingUserDetailsService2 userDetailsService = new ImpersonatingUserDetailsService2(
+ Jenkins.get().getSecurityRealm().getSecurityComponents().userDetails2
+ );
+
+ try {
+ UserDetails userDetails = userDetailsService.loadUserByUsername(id);
+ LOGGER.log(Level.FINE, "Impersonation of the user {0} was a success", id);
+ return userDetails;
+ } catch (UserMayOrMayNotExistException2 e) {
+ LOGGER.log(Level.FINE, "The user {0} may or may not exist in the SecurityRealm, so we provide minimum access", id);
+ } catch (UsernameNotFoundException e) {
+ if (ALLOW_NON_EXISTENT_USER_TO_LOGIN) {
+ LOGGER.log(Level.FINE, "The user {0} was not found in the SecurityRealm but we are required to let it pass, due to ALLOW_NON_EXISTENT_USER_TO_LOGIN", id);
+ } else {
+ LOGGER.log(Level.FINE, "The user {0} was not found in the SecurityRealm", id);
+ throw e;
+ }
+ }
+
+ return new LegitimateButUnknownUserDetails(id);
+ }
+
+ /**
+ * @deprecated use {@link #getUserDetailsForImpersonation2}
+ */
+ @Deprecated
+ public @NonNull org.acegisecurity.userdetails.UserDetails getUserDetailsForImpersonation() throws org.acegisecurity.userdetails.UsernameNotFoundException {
+ try {
+ return org.acegisecurity.userdetails.UserDetails.fromSpring(getUserDetailsForImpersonation2());
+ } catch (AuthenticationException x) {
+ throw org.acegisecurity.AuthenticationException.fromSpring(x);
+ }
+ }
+
+ /**
+ * Only used for a legitimate user we have no idea about. We give it only minimum access
+ */
+ private static class LegitimateButUnknownUserDetails extends org.springframework.security.core.userdetails.User {
+ private LegitimateButUnknownUserDetails(String username) throws IllegalArgumentException {
+ super(
+ username, "",
+ true, true, true, true,
+ Set.of(SecurityRealm.AUTHENTICATED_AUTHORITY2)
+ );
+ }
+ }
+
+ /**
+ * Creates an {@link Authentication} object that represents this user using the given userDetails
+ *
+ * @param userDetails Provided by {@link #getUserDetailsForImpersonation2()}.
+ * @see #getUserDetailsForImpersonation2()
+ */
+ @Restricted(NoExternalUse.class)
+ public @NonNull Authentication impersonate(@NonNull UserDetails userDetails) {
+ return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), "", userDetails.getAuthorities());
+ }
+
+ /**
+ * Accepts the new description.
+ */
+ @RequirePOST
+ public void doSubmitDescription(StaplerRequest req, StaplerResponse rsp) throws IOException {
+ checkPermission(Jenkins.ADMINISTER);
+
+ description = req.getParameter("description");
+ save();
+
+ rsp.sendRedirect("."); // go to the top page
+ }
+
+ /**
+ * Gets the fallback "unknown" user instance.
+ *
+ * This is used to avoid null {@link User} instance.
+ */
+ public static @NonNull User getUnknown() {
+ return getById(UNKNOWN_USERNAME, true);
+ }
+
+ /**
+ * Gets the {@link User} object by its id or full name.
+ *
+ * @param create If true, this method will never return null for valid input
+ * (by creating a new {@link User} object if none exists.)
+ * If false, this method will return null if {@link User} object
+ * with the given name doesn't exist.
+ * @return Requested user. May be {@code null} if a user does not exist and
+ * {@code create} is false.
+ * @deprecated use {@link User#get(String, boolean, java.util.Map)}
+ */
+ @Deprecated
+ public static @Nullable User get(String idOrFullName, boolean create) {
+ return get(idOrFullName, create, Collections.emptyMap());
+ }
+
+ /**
+ * Gets the {@link User} object by its id or full name.
+ *
+ * In order to resolve the user ID, the method invokes {@link CanonicalIdResolver} extension points.
+ * Note that it may cause significant performance degradation.
+ * If you are sure the passed value is a User ID, it is recommended to use {@link #getById(String, boolean)}.
+ *
+ * @param create If true, this method will never return null for valid input
+ * (by creating a new {@link User} object if none exists.)
+ * If false, this method will return null if {@link User} object
+ * with the given name doesn't exist.
+ * @param context contextual environment this user idOfFullName was retrieved from,
+ * that can help resolve the user ID
+ * @return An existing or created user. May be {@code null} if a user does not exist and
+ * {@code create} is false.
+ */
+ public static @Nullable User get(String idOrFullName, boolean create, @NonNull Map context) {
+ if (idOrFullName == null) {
+ return null;
+ }
+
+ User user = AllUsers.get(idOrFullName);
+ if (user != null) {
+ return user;
+ }
+
+ String id = CanonicalIdResolver.resolve(idOrFullName, context);
+ return getOrCreateById(id, idOrFullName, create);
+ }
+
+ /**
+ * Retrieve a user by its ID, and create a new one if requested.
+ *
+ * @return An existing or created user. May be {@code null} if a user does not exist and
+ * {@code create} is false.
+ */
+ private static @Nullable User getOrCreateById(@NonNull String id, @NonNull String fullName, boolean create) {
+ User u = AllUsers.get(id);
+ if (u == null && (create || UserIdMapper.getInstance().isMapped(id))) {
+ u = new User(id, fullName);
+ AllUsers.put(id, u);
+ if (!id.equals(fullName) && !UserIdMapper.getInstance().isMapped(id)) {
+ try {
+ u.save();
+ } catch (IOException x) {
+ LOGGER.log(Level.WARNING, "Failed to save user configuration for " + id, x);
+ }
+ }
+ }
+ return u;
+ }
+
+ /**
+ * Gets the {@link User} object by its id or full name.
+ *
+ * Creates a user on-demand.
+ *
+ *
+ * Use {@link #getById} when you know you have an ID.
+ * In this method Jenkins will try to resolve the {@link User} by full name with help of various
+ * {@link hudson.tasks.UserNameResolver}.
+ * This is slow (see JENKINS-23281).
+ *
+ * @deprecated This method is deprecated, because it causes unexpected {@link User} creation
+ * by API usage code and causes performance degradation of used to retrieve users by ID.
+ * Use {@link #getById} when you know you have an ID.
+ * Otherwise use {@link #getOrCreateByIdOrFullName(String)} or {@link #get(String, boolean, Map)}.
+ */
+ @Deprecated
+ public static @NonNull User get(String idOrFullName) {
+ return getOrCreateByIdOrFullName(idOrFullName);
+ }
+
+ /**
+ * Get the user by ID or Full Name.
+ *
+ * If the user does not exist, creates a new one on-demand.
+ *
+ *
+ * Use {@link #getById} when you know you have an ID.
+ * In this method Jenkins will try to resolve the {@link User} by full name with help of various
+ * {@link hudson.tasks.UserNameResolver}.
+ * This is slow (see JENKINS-23281).
+ *
+ * @param idOrFullName User ID or full name
+ * @return User instance. It will be created on-demand.
+ * @since 2.91
+ */
+ public static @NonNull User getOrCreateByIdOrFullName(@NonNull String idOrFullName) {
+ return get(idOrFullName, true, Collections.emptyMap());
+ }
+
+
+ /**
+ * Gets the {@link User} object representing the currently logged-in user, or null
+ * if the current user is anonymous.
+ *
+ * @since 1.172
+ */
+ public static @CheckForNull User current() {
+ return get2(Jenkins.getAuthentication2());
+ }
+
+ /**
+ * Gets the {@link User} object representing the supplied {@link Authentication} or
+ * {@code null} if the supplied {@link Authentication} is either anonymous or {@code null}
+ *
+ * @param a the supplied {@link Authentication} .
+ * @return a {@link User} object for the supplied {@link Authentication} or {@code null}
+ * @since 2.266
+ */
+ public static @CheckForNull User get2(@CheckForNull Authentication a) {
+ if (a == null || a instanceof AnonymousAuthenticationToken)
+ return null;
+
+ // Since we already know this is a name, we can just call getOrCreateById with the name directly.
+ return getById(a.getName(), true);
+ }
+
+ /**
+ * @deprecated use {@link #get2(Authentication)}
+ * @since 1.609
+ */
+ @Deprecated
+ public static @CheckForNull User get(@CheckForNull org.acegisecurity.Authentication a) {
+ return get2(a != null ? a.toSpring() : null);
+ }
+
+ /**
+ * Gets the {@link User} object by its {@code id}
+ *
+ * @param id the id of the user to retrieve and optionally create if it does not exist.
+ * @param create If {@code true}, this method will never return {@code null} for valid input (by creating a
+ * new {@link User} object if none exists.) If {@code false}, this method will return
+ * {@code null} if {@link User} object with the given id doesn't exist.
+ * @return the a User whose id is {@code id}, or {@code null} if {@code create} is {@code false}
+ * and the user does not exist.
+ * @since 1.651.2 / 2.3
+ */
+ public static @Nullable User getById(String id, boolean create) {
+ return getOrCreateById(id, id, create);
+ }
+
+ /**
+ * Gets all the users.
+ */
+ public static @NonNull Collection getAll() {
+ final IdStrategy strategy = idStrategy();
+ ArrayList users = new ArrayList<>(AllUsers.values());
+ users.sort((o1, o2) -> strategy.compare(o1.getId(), o2.getId()));
+ return users;
+ }
+
+ /**
+ * To be called from {@link Jenkins#reload} only.
+ */
+ @Restricted(NoExternalUse.class)
+ public static void reload() throws IOException {
+ UserIdMapper.getInstance().reload();
+ AllUsers.reload();
+ }
+
+ /**
+ * Called when changing the {@link IdStrategy}.
+ *
+ * @since 1.566
+ */
+ public static void rekey() {
+ /* There are many and varied ways in which this could cause erratic or
+ problematic behavior. Such changes should really only occur during initial
+ setup and under very controlled situations. After this sort of a change
+ the whole webapp should restart. It's possible that this rekeying,
+ or greater issues in the realm change, could affect currently logged
+ in users and even the user making the change. */
+ try {
+ reload();
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "Failed to perform rekey operation.", e);
+ }
+ }
+
+ /**
+ * Returns the user name.
+ */
+ @Override
+ public @NonNull String getDisplayName() {
+ return getFullName();
+ }
+
+ /**
+ * true if {@link RunWithSCM#hasParticipant} or {@link hudson.model.Cause.UserIdCause}
+ */
+ private boolean relatedTo(@NonNull Run, ?> b) {
+ if (b instanceof RunWithSCM && ((RunWithSCM) b).hasParticipant(this)) {
+ return true;
+ }
+ for (Cause cause : b.getCauses()) {
+ if (cause instanceof Cause.UserIdCause) {
+ String userId = ((Cause.UserIdCause) cause).getUserId();
+ if (userId != null && idStrategy().equals(userId, getId())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Searches for builds which include changes by this user or which were triggered by this user.
+ */
+ @SuppressWarnings("unchecked")
+ @WithBridgeMethods(List.class)
+ public @NonNull RunList getBuilds() {
+ return RunList.fromJobs((Iterable) Jenkins.get().
+ allItems(Job.class)).filter((Predicate>) this::relatedTo);
+ }
+
+ /**
+ * Gets all the {@link AbstractProject}s that this user has committed to.
+ *
+ * @since 1.191
+ */
+ public @NonNull Set> getProjects() {
+ Set> r = new HashSet<>();
+ for (AbstractProject, ?> p : Jenkins.get().allItems(AbstractProject.class, p -> p.hasParticipant(this)))
+ r.add(p);
+ return r;
+ }
+
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ /**
+ * Called by tests in the JTH. Otherwise this shouldn't be called.
+ * Even in the tests this usage is questionable.
+ * @deprecated removed without replacement
+ */
+ @Deprecated
+ public static void clear() {
+ if (ExtensionList.lookup(AllUsers.class).isEmpty()) {
+ return;
+ }
+ UserIdMapper.getInstance().clear();
+ AllUsers.clear();
+ }
+
+ private static File getConfigFileFor(String id) {
+ return new File(getUserFolderFor(id), "config.xml");
+ }
+
+ private static File getUserFolderFor(String id) {
+ return new File(getRootDir(), idStrategy().filenameOf(id));
+ }
+ /**
+ * Returns the folder that store all the user information.
+ * Useful for plugins to save a user-specific file aside the config.xml.
+ * Exposes implementation details that may be subject to change.
+ *
+ * @return The folder containing the user configuration files or {@code null} if the user was not yet saved.
+ *
+ * @since 2.129
+ */
+
+ public @CheckForNull File getUserFolder() {
+ return getExistingUserFolder();
+ }
+
+ private @CheckForNull File getExistingUserFolder() {
+ return UserIdMapper.getInstance().getDirectory(id);
+ }
+
+ /**
+ * Gets the directory where Hudson stores user information.
+ */
+ static File getRootDir() {
+ return new File(Jenkins.get().getRootDir(), "users");
+ }
+
+ /**
+ * Is the ID allowed? Some are prohibited for security reasons. See SECURITY-166.
+ *
+ * Note that this is only enforced when saving. These users are often created
+ * via the constructor (and even listed on /asynchPeople), but our goal is to
+ * prevent anyone from logging in as these users. Therefore, we prevent
+ * saving a User with one of these ids.
+ *
+ * @param id ID to be checked
+ * @return {@code true} if the username or fullname is valid.
+ * For {@code null} or blank IDs returns {@code false}.
+ * @since 1.600
+ */
+ public static boolean isIdOrFullnameAllowed(@CheckForNull String id) {
+ if (StringUtils.isBlank(id)) {
+ return false;
+ }
+ final String trimmedId = id.trim();
+ for (String invalidId : ILLEGAL_PERSISTED_USERNAMES) {
+ if (trimmedId.equalsIgnoreCase(invalidId))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Save the user configuration.
+ */
+ @Override
+ public synchronized void save() throws IOException {
+ if (!isIdOrFullnameAllowed(id)) {
+ throw FormValidation.error(Messages.User_IllegalUsername(id));
+ }
+ if (!isIdOrFullnameAllowed(fullName)) {
+ throw FormValidation.error(Messages.User_IllegalFullname(fullName));
+ }
+ if (BulkChange.contains(this)) {
+ return;
+ }
+ XmlFile xmlFile = new XmlFile(XSTREAM, constructUserConfigFile());
+ xmlFile.write(this);
+ SaveableListener.fireOnChange(this, xmlFile);
+ }
+
+ private File constructUserConfigFile() throws IOException {
+ return new File(putUserFolderIfAbsent(), CONFIG_XML);
+ }
+
+ private File putUserFolderIfAbsent() throws IOException {
+ return UserIdMapper.getInstance().putIfAbsent(id, true);
+ }
+
+ /**
+ * Deletes the data directory and removes this user from Hudson.
+ *
+ * @throws IOException if we fail to delete.
+ */
+ public void delete() throws IOException {
+ String idKey = idStrategy().keyFor(id);
+ File existingUserFolder = getExistingUserFolder();
+ UserIdMapper.getInstance().remove(id);
+ AllUsers.remove(id);
+ deleteExistingUserFolder(existingUserFolder);
+ UserDetailsCache.get().invalidate(idKey);
+ }
+
+ private void deleteExistingUserFolder(File existingUserFolder) throws IOException {
+ if (existingUserFolder != null && existingUserFolder.exists()) {
+ Util.deleteRecursive(existingUserFolder);
+ }
+ }
+
+ /**
+ * Exposed remote API.
+ */
+ public Api getApi() {
+ return new Api(this);
+ }
+
+ /**
+ * Accepts submission from the configuration page.
+ */
+ @POST
+ public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
+ checkPermission(Jenkins.ADMINISTER);
+
+ JSONObject json = req.getSubmittedForm();
+ String oldFullName = this.fullName;
+ fullName = json.getString("fullName");
+ description = json.getString("description");
+
+ List props = new ArrayList<>();
+ int i = 0;
+ for (UserPropertyDescriptor d : UserProperty.all()) {
+ UserProperty p = getProperty(d.clazz);
+
+ JSONObject o = json.optJSONObject("userProperty" + i++);
+ if (o != null) {
+ if (p != null) {
+ p = p.reconfigure(req, o);
+ } else {
+ p = d.newInstance(req, o);
+ }
+ p.setUser(this);
+ }
+
+ if (p != null)
+ props.add(p);
+ }
+ this.properties = props;
+
+ save();
+
+ if (oldFullName != null && !oldFullName.equals(this.fullName)) {
+ UserDetailsCache.get().invalidate(oldFullName);
+ }
+
+ FormApply.success(".").generateResponse(req, rsp, this);
+ }
+
+ /**
+ * Deletes this user from Hudson.
+ */
+ @RequirePOST
+ public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException {
+ checkPermission(Jenkins.ADMINISTER);
+ if (idStrategy().equals(id, Jenkins.getAuthentication2().getName())) {
+ rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Cannot delete self");
+ return;
+ }
+
+ delete();
+
+ rsp.sendRedirect2("../..");
+ }
+
+ public void doRssAll(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
+ RSS.rss(req, rsp, "Jenkins:" + getDisplayName() + " (all builds)", getUrl(), getBuilds().newBuilds());
+ }
+
+ public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
+ RSS.rss(req, rsp, "Jenkins:" + getDisplayName() + " (failed builds)", getUrl(), getBuilds().regressionOnly());
+ }
+
+ public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
+ final List lastBuilds = new ArrayList<>();
+ for (Job, ?> p : Jenkins.get().allItems(Job.class)) {
+ for (Run, ?> b = p.getLastBuild(); b != null; b = b.getPreviousBuild()) {
+ if (relatedTo(b)) {
+ lastBuilds.add(b);
+ break;
+ }
+ }
+ }
+ // historically these have been reported sorted by project name, we switched to the lazy iteration
+ // so we only have to sort the sublist of runs rather than the full list of irrelevant projects
+ lastBuilds.sort((o1, o2) -> Items.BY_FULL_NAME.compare(o1.getParent(), o2.getParent()));
+ RSS.rss(req, rsp, "Jenkins:" + getDisplayName() + " (latest builds)", getUrl(), RunList.fromRuns(lastBuilds), Run.FEED_ADAPTER_LATEST);
+ }
+
+ @Override
+ @NonNull
+ public ACL getACL() {
+ ACL base = Jenkins.get().getAuthorizationStrategy().getACL(this);
+ // always allow a non-anonymous user full control of himself.
+ return ACL.lambda2((a, permission) -> (idStrategy().equals(a.getName(), id) && !(a instanceof AnonymousAuthenticationToken))
+ || base.hasPermission2(a, permission));
+ }
+
+ /**
+ * With ADMINISTER permission, can delete users with persisted data but can't delete self.
+ */
+ public boolean canDelete() {
+ final IdStrategy strategy = idStrategy();
+ return hasPermission(Jenkins.ADMINISTER) && !strategy.equals(id, Jenkins.getAuthentication2().getName())
+ && UserIdMapper.getInstance().isMapped(id);
+ }
+
+ /**
+ * Checks for authorities (groups) associated with this user.
+ * If the caller lacks {@link Jenkins#ADMINISTER}, or any problems arise, returns an empty list.
+ * {@link SecurityRealm#AUTHENTICATED_AUTHORITY2} and the username, if present, are omitted.
+ *
+ * @return a possibly empty list
+ * @since 1.498
+ */
+ public @NonNull List getAuthorities() {
+ if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
+ return Collections.emptyList();
+ }
+ List r = new ArrayList<>();
+ Authentication authentication;
+ try {
+ authentication = impersonate2();
+ } catch (UsernameNotFoundException x) {
+ LOGGER.log(Level.FINE, "cannot look up authorities for " + id, x);
+ return Collections.emptyList();
+ }
+ for (GrantedAuthority a : authentication.getAuthorities()) {
+ if (a.equals(SecurityRealm.AUTHENTICATED_AUTHORITY2)) {
+ continue;
+ }
+ String n = a.getAuthority();
+ if (n != null && !idStrategy().equals(n, id)) {
+ r.add(n);
+ }
+ }
+ r.sort(String.CASE_INSENSITIVE_ORDER);
+ return r;
+ }
+
+ public Object getDynamic(String token) {
+ for (Action action : getTransientActions()) {
+ if (Objects.equals(action.getUrlName(), token))
+ return action;
+ }
+ for (Action action : getPropertyActions()) {
+ if (Objects.equals(action.getUrlName(), token))
+ return action;
+ }
+ return null;
+ }
+
+ /**
+ * Return all properties that are also actions.
+ *
+ * @return the list can be empty but never null. read only.
+ */
+ public List getPropertyActions() {
+ List actions = new ArrayList<>();
+ for (UserProperty userProp : getProperties().values()) {
+ if (userProp instanceof Action) {
+ actions.add((Action) userProp);
+ }
+ }
+ return Collections.unmodifiableList(actions);
+ }
+
+ /**
+ * Return all transient actions associated with this user.
+ *
+ * @return the list can be empty but never null. read only.
+ */
+ public List getTransientActions() {
+ List actions = new ArrayList<>();
+ for (TransientUserActionFactory factory : TransientUserActionFactory.all()) {
+ actions.addAll(factory.createFor(this));
+ }
+ return Collections.unmodifiableList(actions);
+ }
+
+ @Override
+ public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
+ return new ContextMenu().from(this, request, response);
+ }
+
+ @Override
+ @Restricted(NoExternalUse.class)
+ public Object getTarget() {
+ if (!SKIP_PERMISSION_CHECK) {
+ if (!Jenkins.get().hasPermission(Jenkins.READ)) {
+ return null;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Gets list of Illegal usernames, for which users should not be created.
+ * Always includes users from {@link #ILLEGAL_PERSISTED_USERNAMES}
+ *
+ * @return List of usernames
+ */
+ @Restricted(NoExternalUse.class)
+ /*package*/ static Set getIllegalPersistedUsernames() {
+ return new HashSet<>(Arrays.asList(ILLEGAL_PERSISTED_USERNAMES));
+ }
+
+ private Object writeReplace() {
+ return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this));
+ }
+
+ private static class Replacer {
+ private final String id;
+
+ Replacer(User u) {
+ id = u.getId();
+ }
+
+ private Object readResolve() {
+ return getById(id, false);
+ }
+ }
+
+ /**
+ * Per-{@link Jenkins} holder of all known {@link User}s.
+ */
+ @Extension
+ @Restricted(NoExternalUse.class)
+ public static final class AllUsers {
+
+ private final ConcurrentMap byName = new ConcurrentHashMap<>();
+
+ @Initializer(after = InitMilestone.JOB_CONFIG_ADAPTED)
+ public static void scanAll() {
+ for (String userId : UserIdMapper.getInstance().getConvertedUserIds()) {
+ User user = new User(userId, userId);
+ getInstance().byName.putIfAbsent(idStrategy().keyFor(userId), user);
+ }
+ }
+
+ /**
+ * Keyed by {@link User#id}. This map is used to ensure
+ * singleton-per-id semantics of {@link User} objects.
+ *
+ * This extension point may be useful to map SCM user names to Jenkins {@link User} IDs.
+ * Currently the extension point is used in {@link User#get(String, boolean, Map)}.
+ *
+ * @see jenkins.model.DefaultUserCanonicalIdResolver
+ * @see FullNameIdResolver
+ * @since 1.479
+ */
+ public abstract static class CanonicalIdResolver extends AbstractDescribableImpl implements ExtensionPoint, Comparable {
+
+ /**
+ * context key for realm (domain) where idOrFullName has been retrieved from.
+ * Can be used (for example) to distinguish ambiguous committer ID using the SCM URL.
+ * Associated Value is a {@link String}
+ */
+ public static final String REALM = "realm";
+
+ @Override
+ public int compareTo(@NonNull CanonicalIdResolver o) {
+ // reverse priority order
+ return Integer.compare(o.getPriority(), getPriority());
+ }
+
+ /**
+ * extract user ID from idOrFullName with help from contextual infos.
+ * can return {@code null} if no user ID matched the input
+ */
+ public abstract @CheckForNull String resolveCanonicalId(String idOrFullName, Map context);
+
+ /**
+ * Gets priority of the resolver.
+ * Higher priority means that it will be checked earlier.
+ *
+ * Overriding methods must not use {@link Integer#MIN_VALUE}, because it will cause collisions
+ * with {@link jenkins.model.DefaultUserCanonicalIdResolver}.
+ *
+ * @return Priority of the resolver.
+ */
+ public int getPriority() {
+ return 1;
+ }
+
+ //Such sorting and collection rebuild is not good for User#get(...) method performance.
+
+ /**
+ * Gets all extension points, sorted by priority.
+ *
+ * @return Sorted list of extension point implementations.
+ * @since 2.93
+ */
+ public static List all() {
+ List resolvers = new ArrayList<>(ExtensionList.lookup(CanonicalIdResolver.class));
+ Collections.sort(resolvers);
+ return resolvers;
+ }
+
+ /**
+ * Resolves users using all available {@link CanonicalIdResolver}s.
+ *
+ * @param idOrFullName ID or full name of the user
+ * @param context Context
+ * @return Resolved User ID or {@code null} if the user ID cannot be resolved.
+ * @since 2.93
+ */
+ @CheckForNull
+ public static String resolve(@NonNull String idOrFullName, @NonNull Map context) {
+ for (CanonicalIdResolver resolver : CanonicalIdResolver.all()) {
+ String id = resolver.resolveCanonicalId(idOrFullName, context);
+ if (id != null) {
+ LOGGER.log(Level.FINE, "{0} mapped {1} to {2}", new Object[]{resolver, idOrFullName, id});
+ return id;
+ }
+ }
+
+ // De-facto it is not going to happen OOTB, because the current DefaultUserCanonicalIdResolver
+ // always returns a value. But we still need to check nulls if somebody disables the extension point
+ return null;
+ }
+ }
+
+
+ /**
+ * Resolve user ID from full name
+ */
+ @Extension
+ @Symbol("fullName")
+ public static class FullNameIdResolver extends CanonicalIdResolver {
+
+ @Override
+ public String resolveCanonicalId(String idOrFullName, Map context) {
+ for (User user : getAll()) {
+ if (idOrFullName.equals(user.getFullName())) return user.getId();
+ }
+ return null;
+ }
+
+ @Override
+ public int getPriority() {
+ return -1; // lower than default
+ }
+ }
+
+
+ /**
+ * Tries to verify if an ID is valid.
+ * If so, we do not want to even consider users who might have the same full name.
+ */
+ @Extension
+ @Restricted(NoExternalUse.class)
+ public static class UserIDCanonicalIdResolver extends User.CanonicalIdResolver {
+
+ private static /* not final */ boolean SECURITY_243_FULL_DEFENSE =
+ SystemProperties.getBoolean(User.class.getName() + ".SECURITY_243_FULL_DEFENSE", true);
+
+ private static final ThreadLocal resolving = ThreadLocal.withInitial(() -> false);
+
+ @Override
+ public String resolveCanonicalId(String idOrFullName, Map context) {
+ User existing = getById(idOrFullName, false);
+ if (existing != null) {
+ return existing.getId();
+ }
+ if (SECURITY_243_FULL_DEFENSE) {
+ if (!resolving.get()) {
+ resolving.set(true);
+ try {
+ UserDetails userDetails = UserDetailsCache.get().loadUserByUsername(idOrFullName);
+ return userDetails.getUsername();
+ } catch (UsernameNotFoundException x) {
+ LOGGER.log(Level.FINE, "not sure whether " + idOrFullName + " is a valid username or not", x);
+ } catch (ExecutionException x) {
+ LOGGER.log(Level.FINE, "could not look up " + idOrFullName, x);
+ } finally {
+ resolving.set(false);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int getPriority() {
+ // should always come first so that ID that are ids get mapped correctly
+ return Integer.MAX_VALUE;
+ }
+
+ }
+
+}
diff --git a/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler.java b/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler.java
new file mode 100644
index 00000000..40d2f88e
--- /dev/null
+++ b/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler.java
@@ -0,0 +1,368 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.facelets;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.myfaces.tobago.component.Attributes;
+import org.apache.myfaces.tobago.component.SupportsMarkup;
+import org.apache.myfaces.tobago.component.SupportsRenderedPartially;
+import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.el.ConstantMethodBinding;
+import org.apache.myfaces.tobago.internal.util.StringUtils;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.el.ELException;
+import javax.el.ExpressionFactory;
+import javax.el.MethodExpression;
+import javax.el.ValueExpression;
+import javax.faces.FacesException;
+import javax.faces.component.ActionSource;
+import javax.faces.component.ActionSource2;
+import javax.faces.component.EditableValueHolder;
+import javax.faces.component.UIComponent;
+import javax.faces.component.ValueHolder;
+import javax.faces.convert.Converter;
+import javax.faces.event.MethodExpressionActionListener;
+import javax.faces.event.MethodExpressionValueChangeListener;
+import javax.faces.validator.MethodExpressionValidator;
+import javax.faces.view.facelets.ComponentHandler;
+import javax.faces.view.facelets.FaceletContext;
+import javax.faces.view.facelets.TagAttribute;
+import javax.faces.view.facelets.TagConfig;
+import javax.faces.view.facelets.TagException;
+import javax.faces.view.facelets.TagHandler;
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+
+//from Apache MyFaces 2.0.8
+//Retrieved from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source
+public final class AttributeHandler extends TagHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AttributeHandler.class);
+
+ private final TagAttribute name;
+
+ private final TagAttribute value;
+
+ private final TagAttribute mode;
+
+ public AttributeHandler(final TagConfig config) {
+ super(config);
+ this.name = getRequiredAttribute(Attributes.NAME);
+ this.value = getRequiredAttribute(Attributes.VALUE);
+ this.mode = getAttribute(Attributes.MODE);
+ }
+
+ public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {
+ if (parent == null) {
+ throw new TagException(tag, "Parent UIComponent was null");
+ }
+
+ if (ComponentHandler.isNew(parent)) {
+
+ if (mode != null) {
+ if ("isNotSet".equals(mode.getValue())) {
+ boolean result = false;
+ String expressionString = value.getValue();
+ if (!value.isLiteral()) {
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ result = true;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ result = false;
+ break;
+ }
+ }
+ } else {
+ result = StringUtils.isEmpty(expressionString);
+ }
+ parent.getAttributes().put(name.getValue(), result);
+ } else if ("isSet".equals(mode.getValue())) {
+ boolean result = true;
+ String expressionString = value.getValue();
+ if (!value.isLiteral()) {
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ result = false;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ result = true;
+ break;
+ }
+ }
+ } else {
+ result = StringUtils.isNotEmpty(expressionString);
+ }
+ parent.getAttributes().put(name.getValue(), result);
+ } else if ("action".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ // when the action hasn't been set while using a composition.
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Variable can't be resolved: value='" + expressionString + "'");
+ }
+ expressionString = null;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(
+ faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));
+ ((ActionSource2) parent).setActionExpression(action);
+ }
+ } else if ("actionListener".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ if (LOG.isDebugEnabled()) {
+ // when the action hasn't been set while using a composition.
+ LOG.debug("Variable can't be resolved: value='" + expressionString + "'");
+ }
+ expressionString = null;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ LOG.warn("Only expressions are supported mode=actionListener value='" + expressionString + "'");
+ expressionString = null;
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final MethodExpression actionListener
+ = new TagMethodExpression(value, expressionFactory.createMethodExpression(
+ faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));
+ ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));
+ }
+ } else if ("actionFromValue".equals(mode.getValue())) {
+ if (!value.isLiteral()) {
+ final String result = value.getValue(faceletContext);
+ parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));
+ }
+ } else if ("valueIfSet".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ String lastExpressionString = null;
+ while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression != null) {
+ lastExpressionString = expressionString;
+ expressionString = expression.getExpressionString();
+ } else {
+ // restore last value
+ expressionString = lastExpressionString;
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final String attributeName = name.getValue(faceletContext);
+ if (containsMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);
+ parent.setValueExpression(attributeName, expression);
+ } else {
+ final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);
+ parent.getAttributes().put(attributeName, literalValue);
+ }
+ }
+ } else {
+ throw new FacesException("Type " + mode + " not supported");
+ }
+ } else {
+
+ final String nameValue = name.getValue(faceletContext);
+ if (Attributes.RENDERED.equals(nameValue)) {
+ if (value.isLiteral()) {
+ parent.setRendered(value.getBoolean(faceletContext));
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));
+ }
+ } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)
+ && parent instanceof SupportsRenderedPartially) {
+
+ if (value.isLiteral()) {
+ final String[] components = ComponentUtils.splitList(value.getValue());
+ ((SupportsRenderedPartially) parent).setRenderedPartially(components);
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ } else if (Attributes.STYLE_CLASS.equals(nameValue)) {
+ // TODO expression
+ ComponentUtils.setStyleClasses(parent, value.getValue());
+ } else if (Attributes.MARKUP.equals(nameValue)) {
+ if (parent instanceof SupportsMarkup) {
+ if (value.isLiteral()) {
+ ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));
+ } else {
+ final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);
+ parent.setValueExpression(nameValue, expression);
+ }
+ } else {
+ LOG.error("Component is not instanceof SupportsMarkup. Instance is: " + parent.getClass().getName());
+ }
+ } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {
+ final MethodExpression methodExpression
+ = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);
+ if (methodExpression != null) {
+ ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));
+ }
+ } else if (parent instanceof EditableValueHolder
+ && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {
+ final MethodExpression methodExpression =
+ getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);
+ if (methodExpression != null) {
+ ((EditableValueHolder) parent).addValueChangeListener(
+ new MethodExpressionValueChangeListener(methodExpression));
+ }
+ } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {
+ setConverter(faceletContext, parent, nameValue);
+ } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {
+ final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);
+ if (action != null) {
+ ((ActionSource2) parent).setActionExpression(action);
+ }
+ } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {
+ final MethodExpression action
+ = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);
+ if (action != null) {
+ ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));
+ }
+ } else if (!parent.getAttributes().containsKey(nameValue)) {
+ if (value.isLiteral()) {
+ parent.getAttributes().put(nameValue, value.getValue());
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ }
+ }
+ }
+ }
+
+ private boolean isMethodOrValueExpression(final String string) {
+ return (string.startsWith("${") || string.startsWith("#{")) && string.endsWith("}");
+ }
+
+ private boolean containsMethodOrValueExpression(final String string) {
+ return (string.contains("${") || string.contains("#{")) && string.contains("}");
+ }
+
+ private boolean isSimpleExpression(final String string) {
+ return string.indexOf('.') < 0 && string.indexOf('[') < 0;
+ }
+
+ private String removeElParenthesis(final String string) {
+ return string.substring(2, string.length() - 1);
+ }
+
+ private ValueExpression getExpression(final FaceletContext faceletContext) {
+ final String myValue = removeElParenthesis(value.getValue());
+ return faceletContext.getVariableMapper().resolveVariable(myValue);
+ }
+
+ private MethodExpression getMethodExpression(
+ final FaceletContext faceletContext, final Class returnType, final Class[] args) {
+ // in a composition may be we get the method expression string from the current variable mapper
+ // the expression can be empty
+ // in this case return nothing
+ if (value.getValue().startsWith("${")) {
+ final ValueExpression expression = getExpression(faceletContext);
+ if (expression != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,
+ expression.getExpressionString(), returnType, args));
+ } else {
+ return null;
+ }
+ } else {
+ return value.getMethodExpression(faceletContext, returnType, args);
+ }
+ }
+
+ private Object getValue(
+ final FaceletContext faceletContext, final UIComponent parent, final String expressionString,
+ final String attributeName) {
+ Class type = Object.class;
+ try {
+ type = PropertyUtils.getReadMethod(
+ new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();
+ } catch (final IntrospectionException e) {
+ LOG.warn("Can't determine expected type", e);
+ }
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final ValueExpression valueExpression = expressionFactory
+ .createValueExpression(faceletContext, expressionString, type);
+ return valueExpression.getValue(faceletContext);
+ }
+
+ private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {
+ // in a composition may be we get the converter expression string from the current variable mapper
+ // the expression can be empty
+ // in this case return nothing
+ if (value.getValue().startsWith("${")) {
+ final ValueExpression expression = getExpression(faceletContext);
+ if (expression != null) {
+ setConverter(faceletContext, parent, nameValue, expression);
+ }
+ } else {
+ setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ }
+
+ private void setConverter(
+ final FaceletContext faceletContext, final UIComponent parent, final String nameValue,
+ final ValueExpression expression) {
+ if (expression.isLiteralText()) {
+ final Converter converter =
+ faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());
+ ((ValueHolder) parent).setConverter(converter);
+ } else {
+ parent.setValueExpression(nameValue, expression);
+ }
+ }
+}
\ No newline at end of file
diff --git a/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler2.java b/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler2.java
new file mode 100644
index 00000000..c828e3f1
--- /dev/null
+++ b/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler2.java
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.facelets;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.myfaces.tobago.component.Attributes;
+import org.apache.myfaces.tobago.component.SupportsMarkup;
+import org.apache.myfaces.tobago.component.SupportsRenderedPartially;
+import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.el.ConstantMethodBinding;
+import org.apache.myfaces.tobago.internal.util.StringUtils;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.el.ELException;
+import javax.el.ExpressionFactory;
+import javax.el.MethodExpression;
+import javax.el.ValueExpression;
+import javax.faces.FacesException;
+import javax.faces.component.ActionSource;
+import javax.faces.component.ActionSource2;
+import javax.faces.component.EditableValueHolder;
+import javax.faces.component.UIComponent;
+import javax.faces.component.ValueHolder;
+import javax.faces.convert.Converter;
+import javax.faces.event.MethodExpressionActionListener;
+import javax.faces.event.MethodExpressionValueChangeListener;
+import javax.faces.validator.MethodExpressionValidator;
+import javax.faces.view.facelets.ComponentHandler;
+import javax.faces.view.facelets.FaceletContext;
+import javax.faces.view.facelets.TagAttribute;
+import javax.faces.view.facelets.TagConfig;
+import javax.faces.view.facelets.TagException;
+import javax.faces.view.facelets.TagHandler;
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+
+//from Apache MyFaces 2.0.8
+//Retrieved from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source
+public final class AttributeHandler extends TagHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AttributeHandler.class);
+
+ private final TagAttribute name;
+
+ private final TagAttribute value;
+
+ private final TagAttribute mode;
+
+ public AttributeHandler(final TagConfig config) {
+ super(config);
+ this.name = getRequiredAttribute(Attributes.NAME);
+ this.value = getRequiredAttribute(Attributes.VALUE);
+ this.mode = getAttribute(Attributes.MODE);
+ }
+
+ public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {
+ if (parent == null) {
+ throw new TagException(tag, "Parent UIComponent was null");
+ }
+
+ if (ComponentHandler.isNew(parent)) {
+
+ if (mode != null) {
+ if ("isNotSet".equals(mode.getValue())) {
+ boolean result = false;
+ String expressionString = value.getValue();
+ if (!value.isLiteral()) {
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ result = true;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ result = false;
+ break;
+ }
+ }
+ } else {
+ result = StringUtils.isEmpty(expressionString);
+ }
+ parent.getAttributes().put(name.getValue(), result);
+ } else if ("isSet".equals(mode.getValue())) {
+ boolean result = true;
+ String expressionString = value.getValue();
+ if (!value.isLiteral()) {
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ result = false;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ result = true;
+ break;
+ }
+ }
+ } else {
+ result = StringUtils.isNotEmpty(expressionString);
+ }
+ parent.getAttributes().put(name.getValue(), result);
+ } else if ("action".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ // when the action hasn't been set while using a composition.
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Variable can't be resolved: value='" + expressionString + "'");
+ }
+ expressionString = null;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(
+ faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));
+ ((ActionSource2) parent).setActionExpression(action);
+ }
+ } else if ("actionListener".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ if (LOG.isDebugEnabled()) {
+ // when the action hasn't been set while using a composition.
+ LOG.debug("Variable can't be resolved: value='" + expressionString + "'");
+ }
+ expressionString = null;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ LOG.warn("Only expressions are supported mode=actionListener value='" + expressionString + "'");
+ expressionString = null;
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final MethodExpression actionListener
+ = new TagMethodExpression(value, expressionFactory.createMethodExpression(
+ faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));
+ ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));
+ }
+ } else if ("actionFromValue".equals(mode.getValue())) {
+ if (!value.isLiteral()) {
+ final String result = value.getValue(faceletContext);
+ parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));
+ }
+ } else if ("valueIfSet".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ String lastExpressionString = null;
+ while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression != null) {
+ lastExpressionString = expressionString;
+ expressionString = expression.getExpressionString();
+ } else {
+ // restore last value
+ expressionString = lastExpressionString;
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final String attributeName = name.getValue(faceletContext);
+ if (containsMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);
+ parent.setValueExpression(attributeName, expression);
+ } else {
+ final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);
+ parent.getAttributes().put(attributeName, literalValue);
+ }
+ }
+ } else {
+ throw new FacesException("Type " + mode + " not supported");
+ }
+ } else {
+
+ final String nameValue = name.getValue(faceletContext);
+ if (Attributes.RENDERED.equals(nameValue)) {
+ if (value.isLiteral()) {
+ parent.setRendered(value.getBoolean(faceletContext));
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));
+ }
+ } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)
+ && parent instanceof SupportsRenderedPartially) {
+
+ if (value.isLiteral()) {
+ final String[] components = ComponentUtils.splitList(value.getValue());
+ ((SupportsRenderedPartially) parent).setRenderedPartially(components);
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ } else if (Attributes.STYLE_CLASS.equals(nameValue)) {
+ // TODO expression
+ ComponentUtils.setStyleClasses(parent, value.getValue());
+ } else if (Attributes.MARKUP.equals(nameValue)) {
+ if (parent instanceof SupportsMarkup) {
+ if (value.isLiteral()) {
+ ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));
+ } else {
+ final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);
+ parent.setValueExpression(nameValue, expression);
+ }
+ } else {
+ LOG.error("Component is not instanceof SupportsMarkup. Instance is: " + parent.getClass().getName());
+ }
+ } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {
+ final MethodExpression methodExpression
+ = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);
+ if (methodExpression != null) {
+ ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));
+ }
+ } else if (parent instanceof EditableValueHolder
+ && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {
+ final MethodExpression methodExpression =
+ getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);
+ if (methodExpression != null) {
+ ((EditableValueHolder) parent).addValueChangeListener(
+ new MethodExpressionValueChangeListener(methodExpression));
+ }
+ } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {
+ setConverter(faceletContext, parent, nameValue);
+ } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {
+ final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);
+ if (action != null) {
+ ((ActionSource2) parent).setActionExpression(action);
+ }
+ } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {
+ final MethodExpression action
+ = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);
+ if (action != null) {
+ ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));
+ }
+ } else if (!parent.getAttributes().containsKey(nameValue)) {
+ if (value.isLiteral()) {
+ parent.getAttributes().put(nameValue, value.getValue());
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ }
+ }
+ }
+ }
+
+ private boolean isMethodOrValueExpression(final String string) {
+ return (string.startsWith("${") || string.startsWith("#{")) && string.endsWith("}");
+ }
+
+ private boolean containsMethodOrValueExpression(final String string) {
+ return (string.contains("${") || string.contains("#{")) && string.contains("}");
+ }
+
+ private boolean isSimpleExpression(final String string) {
+ return string.indexOf('.') < 0 && string.indexOf('[') < 0;
+ }
+
+ private String removeElParenthesis(final String string) {
+ return string.substring(2, string.length() - 1);
+ }
+
+ private ValueExpression getExpression(final FaceletContext faceletContext) {
+ final String myValue = removeElParenthesis(value.getValue());
+ return faceletContext.getVariableMapper().resolveVariable(myValue);
+ }
+
+ private MethodExpression getMethodExpression(
+ final FaceletContext faceletContext, final Class returnType, final Class[] args) {
+ // in a composition may be we get the method expression string from the current variable mapper
+ // the expression can be empty
+ // in this case return nothing
+ if (value.getValue().startsWith("${")) {
+ final ValueExpression expression = getExpression(faceletContext);
+ if (expression != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,
+ expression.getExpressionString(), returnType, args));
+ } else {
+ return null;
+ }
+ } else {
+ return value.getMethodExpression(faceletContext, returnType, args);
+ }
+ }
+
+ private Object getValue(
+ final FaceletContext faceletContext, final UIComponent parent, final String expressionString,
+ final String attributeName) {
+ Class type = Object.class;
+ try {
+ type = PropertyUtils.getReadMethod(
+ new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();
+ } catch (final IntrospectionException e) {
+ LOG.warn("Can't determine expected type", e);
+ }
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final ValueExpression valueExpression = expressionFactory
+ .createValueExpression(faceletContext, expressionString, type);
+ return valueExpression.getValue(faceletContext);
+ }
+
+ private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {
+ // in a composition may be we get the converter expression string from the current variable mapper
+ // the expression can be empty
+ // in this case return nothing
+ if (value.getValue().startsWith("${")) {
+ final ValueExpression expression = getExpression(faceletContext);
+ if (expression != null) {
+ setConverter(faceletContext, parent, nameValue, expression);
+ }
+ } else {
+ setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ }
+
+ private void setConverter(
+ final FaceletContext faceletContext, final UIComponent parent, final String nameValue,
+ final ValueExpression expression) {
+ if (expression.isLiteralText()) {
+ final Converter converter =
+ faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());
+ ((ValueHolder) parent).setConverter(converter);
+ } else {
+ parent.setValueExpression(nameValue, expression);
+ }
+ }
+
+ public static void letsAddASimpleMethod() {
+ System.out.println("Howdy!");
+ }
+}
\ No newline at end of file
diff --git a/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandlerAndSorter.java b/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandlerAndSorter.java
new file mode 100644
index 00000000..ad9633a4
--- /dev/null
+++ b/cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandlerAndSorter.java
@@ -0,0 +1,605 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.facelets;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.myfaces.tobago.component.Attributes;
+import org.apache.myfaces.tobago.component.SupportsMarkup;
+import org.apache.myfaces.tobago.component.SupportsRenderedPartially;
+import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.el.ConstantMethodBinding;
+import org.apache.myfaces.tobago.internal.util.StringUtils;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.el.ELException;
+import javax.el.ExpressionFactory;
+import javax.el.MethodExpression;
+import javax.el.ValueExpression;
+import javax.faces.FacesException;
+import javax.faces.component.ActionSource;
+import javax.faces.component.ActionSource2;
+import javax.faces.component.EditableValueHolder;
+import javax.faces.component.UIComponent;
+import javax.faces.component.ValueHolder;
+import javax.faces.convert.Converter;
+import javax.faces.event.MethodExpressionActionListener;
+import javax.faces.event.MethodExpressionValueChangeListener;
+import javax.faces.validator.MethodExpressionValidator;
+import javax.faces.view.facelets.ComponentHandler;
+import javax.faces.view.facelets.FaceletContext;
+import javax.faces.view.facelets.TagAttribute;
+import javax.faces.view.facelets.TagConfig;
+import javax.faces.view.facelets.TagException;
+import javax.faces.view.facelets.TagHandler;
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+
+import org.apache.myfaces.tobago.event.SortActionEvent;
+import org.apache.myfaces.tobago.internal.component.AbstractUICommand;
+import org.apache.myfaces.tobago.internal.component.AbstractUISheet;
+import org.apache.myfaces.tobago.internal.util.StringUtils;
+import org.apache.myfaces.tobago.model.SheetState;
+import org.apache.myfaces.tobago.util.BeanComparator;
+import org.apache.myfaces.tobago.util.ValueExpressionComparator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.el.ValueExpression;
+import javax.faces.component.UIColumn;
+import javax.faces.component.UICommand;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UIInput;
+import javax.faces.component.UIOutput;
+import javax.faces.component.UISelectBoolean;
+import javax.faces.component.UISelectMany;
+import javax.faces.component.UISelectOne;
+import javax.faces.context.FacesContext;
+import javax.faces.model.DataModel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public final class AttributeHandlerAndSorter extends TagHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(org.apache.myfaces.tobago.facelets.AttributeHandler.class);
+
+ private final TagAttribute name;
+
+ private final TagAttribute value;
+
+ private final TagAttribute mode;
+
+ public AttributeHandler(final TagConfig config) {
+ super(config);
+ this.name = getRequiredAttribute(Attributes.NAME);
+ this.value = getRequiredAttribute(Attributes.VALUE);
+ this.mode = getAttribute(Attributes.MODE);
+ }
+
+ public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {
+ if (parent == null) {
+ throw new TagException(tag, "Parent UIComponent was null");
+ }
+
+ if (ComponentHandler.isNew(parent)) {
+
+ if (mode != null) {
+ if ("isNotSet".equals(mode.getValue())) {
+ boolean result = false;
+ String expressionString = value.getValue();
+ if (!value.isLiteral()) {
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ result = true;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ result = false;
+ break;
+ }
+ }
+ } else {
+ result = StringUtils.isEmpty(expressionString);
+ }
+ parent.getAttributes().put(name.getValue(), result);
+ } else if ("isSet".equals(mode.getValue())) {
+ boolean result = true;
+ String expressionString = value.getValue();
+ if (!value.isLiteral()) {
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ result = false;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ result = true;
+ break;
+ }
+ }
+ } else {
+ result = StringUtils.isNotEmpty(expressionString);
+ }
+ parent.getAttributes().put(name.getValue(), result);
+ } else if ("action".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ // when the action hasn't been set while using a composition.
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Variable can't be resolved: value='" + expressionString + "'");
+ }
+ expressionString = null;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(
+ faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));
+ ((ActionSource2) parent).setActionExpression(action);
+ }
+ } else if ("actionListener".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ while (isSimpleExpression(expressionString)) {
+ if (isMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression == null) {
+ if (LOG.isDebugEnabled()) {
+ // when the action hasn't been set while using a composition.
+ LOG.debug("Variable can't be resolved: value='" + expressionString + "'");
+ }
+ expressionString = null;
+ break;
+ } else {
+ expressionString = expression.getExpressionString();
+ }
+ } else {
+ LOG.warn("Only expressions are supported mode=actionListener value='" + expressionString + "'");
+ expressionString = null;
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final MethodExpression actionListener
+ = new TagMethodExpression(value, expressionFactory.createMethodExpression(
+ faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));
+ ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));
+ }
+ } else if ("actionFromValue".equals(mode.getValue())) {
+ if (!value.isLiteral()) {
+ final String result = value.getValue(faceletContext);
+ parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));
+ }
+ } else if ("valueIfSet".equals(mode.getValue())) {
+ String expressionString = value.getValue();
+ String lastExpressionString = null;
+ while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {
+ final ValueExpression expression
+ = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));
+ if (expression != null) {
+ lastExpressionString = expressionString;
+ expressionString = expression.getExpressionString();
+ } else {
+ // restore last value
+ expressionString = lastExpressionString;
+ break;
+ }
+ }
+ if (expressionString != null) {
+ final String attributeName = name.getValue(faceletContext);
+ if (containsMethodOrValueExpression(expressionString)) {
+ final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);
+ parent.setValueExpression(attributeName, expression);
+ } else {
+ final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);
+ parent.getAttributes().put(attributeName, literalValue);
+ }
+ }
+ } else {
+ throw new FacesException("Type " + mode + " not supported");
+ }
+ } else {
+
+ final String nameValue = name.getValue(faceletContext);
+ if (Attributes.RENDERED.equals(nameValue)) {
+ if (value.isLiteral()) {
+ parent.setRendered(value.getBoolean(faceletContext));
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));
+ }
+ } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)
+ && parent instanceof SupportsRenderedPartially) {
+
+ if (value.isLiteral()) {
+ final String[] components = ComponentUtils.splitList(value.getValue());
+ ((SupportsRenderedPartially) parent).setRenderedPartially(components);
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ } else if (Attributes.STYLE_CLASS.equals(nameValue)) {
+ // TODO expression
+ ComponentUtils.setStyleClasses(parent, value.getValue());
+ } else if (Attributes.MARKUP.equals(nameValue)) {
+ if (parent instanceof SupportsMarkup) {
+ if (value.isLiteral()) {
+ ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));
+ } else {
+ final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);
+ parent.setValueExpression(nameValue, expression);
+ }
+ } else {
+ LOG.error("Component is not instanceof SupportsMarkup. Instance is: " + parent.getClass().getName());
+ }
+ } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {
+ final MethodExpression methodExpression
+ = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);
+ if (methodExpression != null) {
+ ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));
+ }
+ } else if (parent instanceof EditableValueHolder
+ && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {
+ final MethodExpression methodExpression =
+ getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);
+ if (methodExpression != null) {
+ ((EditableValueHolder) parent).addValueChangeListener(
+ new MethodExpressionValueChangeListener(methodExpression));
+ }
+ } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {
+ setConverter(faceletContext, parent, nameValue);
+ } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {
+ final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);
+ if (action != null) {
+ ((ActionSource2) parent).setActionExpression(action);
+ }
+ } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {
+ final MethodExpression action
+ = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);
+ if (action != null) {
+ ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));
+ }
+ } else if (!parent.getAttributes().containsKey(nameValue)) {
+ if (value.isLiteral()) {
+ parent.getAttributes().put(nameValue, value.getValue());
+ } else {
+ parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ }
+ }
+ }
+ }
+
+ private boolean isMethodOrValueExpression(final String string) {
+ return (string.startsWith("${") || string.startsWith("#{")) && string.endsWith("}");
+ }
+
+ private boolean containsMethodOrValueExpression(final String string) {
+ return (string.contains("${") || string.contains("#{")) && string.contains("}");
+ }
+
+ private boolean isSimpleExpression(final String string) {
+ return string.indexOf('.') < 0 && string.indexOf('[') < 0;
+ }
+
+ private String removeElParenthesis(final String string) {
+ return string.substring(2, string.length() - 1);
+ }
+
+ private ValueExpression getExpression(final FaceletContext faceletContext) {
+ final String myValue = removeElParenthesis(value.getValue());
+ return faceletContext.getVariableMapper().resolveVariable(myValue);
+ }
+
+ private MethodExpression getMethodExpression(
+ final FaceletContext faceletContext, final Class returnType, final Class[] args) {
+ // in a composition may be we get the method expression string from the current variable mapper
+ // the expression can be empty
+ // in this case return nothing
+ if (value.getValue().startsWith("${")) {
+ final ValueExpression expression = getExpression(faceletContext);
+ if (expression != null) {
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,
+ expression.getExpressionString(), returnType, args));
+ } else {
+ return null;
+ }
+ } else {
+ return value.getMethodExpression(faceletContext, returnType, args);
+ }
+ }
+
+ private Object getValue(
+ final FaceletContext faceletContext, final UIComponent parent, final String expressionString,
+ final String attributeName) {
+ Class type = Object.class;
+ try {
+ type = PropertyUtils.getReadMethod(
+ new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();
+ } catch (final IntrospectionException e) {
+ LOG.warn("Can't determine expected type", e);
+ }
+ final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();
+ final ValueExpression valueExpression = expressionFactory
+ .createValueExpression(faceletContext, expressionString, type);
+ return valueExpression.getValue(faceletContext);
+ }
+
+ private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {
+ // in a composition may be we get the converter expression string from the current variable mapper
+ // the expression can be empty
+ // in this case return nothing
+ if (value.getValue().startsWith("${")) {
+ final ValueExpression expression = getExpression(faceletContext);
+ if (expression != null) {
+ setConverter(faceletContext, parent, nameValue, expression);
+ }
+ } else {
+ setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));
+ }
+ }
+
+ private void setConverter(
+ final FaceletContext faceletContext, final UIComponent parent, final String nameValue,
+ final ValueExpression expression) {
+ if (expression.isLiteralText()) {
+ final Converter converter =
+ faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());
+ ((ValueHolder) parent).setConverter(converter);
+ } else {
+ parent.setValueExpression(nameValue, expression);
+ }
+ }
+}
+
+//http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/component/Sorter.java/?v=source
+class Sorter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Sorter.class);
+
+ private Comparator comparator;
+
+ /**
+ * @deprecated Please use {@link #perform(org.apache.myfaces.tobago.internal.component.AbstractUISheet)}
+ */
+ @Deprecated
+ public void perform(final SortActionEvent sortEvent) {
+ final AbstractUISheet data = (AbstractUISheet) sortEvent.getComponent();
+ perform(data);
+ }
+
+ public void perform(final AbstractUISheet data) {
+
+ Object value = data.getValue();
+ if (value instanceof DataModel) {
+ value = ((DataModel) value).getWrappedData();
+ }
+ final FacesContext facesContext = FacesContext.getCurrentInstance();
+ final SheetState sheetState = data.getSheetState(facesContext);
+
+ final String sortedColumnId = sheetState.getSortedColumnId();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("sorterId = '{}'", sortedColumnId);
+ }
+
+ if (sortedColumnId == null) {
+ // not to be sorted
+ return;
+ }
+
+ final UIColumn column = (UIColumn) data.findComponent(sortedColumnId);
+ if (column == null) {
+ LOG.warn("No column to sort found, sorterId = '{}'", sortedColumnId);
+ return;
+ }
+
+ final Comparator actualComparator;
+
+ if (value instanceof List || value instanceof Object[]) {
+ final String sortProperty;
+
+ try {
+ final UIComponent child = getFirstSortableChild(column.getChildren());
+ if (child != null) {
+
+ final String attributeName = child instanceof AbstractUICommand ? Attributes.LABEL : Attributes.VALUE;
+ if (child.getValueExpression(attributeName) != null) {
+ final String var = data.getVar();
+ if (var == null) {
+ LOG.error("No sorting performed. Property var of sheet is not set!");
+ unsetSortableAttribute(column);
+ return;
+ }
+ String expressionString = child.getValueExpression(attributeName).getExpressionString();
+ if (isSimpleProperty(expressionString)) {
+ if (expressionString.startsWith("#{")
+ && expressionString.endsWith("}")) {
+ expressionString =
+ expressionString.substring(2,
+ expressionString.length() - 1);
+ }
+ sortProperty = expressionString.substring(var.length() + 1);
+
+ actualComparator = new BeanComparator(
+ sortProperty, comparator, !sheetState.isAscending());
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Sort property is {}", sortProperty);
+ }
+ } else {
+
+ final boolean descending = !sheetState.isAscending();
+ final ValueExpression expression = child.getValueExpression("value");
+ actualComparator = new ValueExpressionComparator(facesContext, var, expression, descending, comparator);
+ }
+ } else {
+ LOG.error("No sorting performed. No Expression target found for sorting!");
+ unsetSortableAttribute(column);
+ return;
+ }
+ } else {
+ LOG.error("No sorting performed. Value is not instanceof List or Object[]!");
+ unsetSortableAttribute(column);
+ return;
+ }
+ } catch (final Exception e) {
+ LOG.error("Error while extracting sortMethod :" + e.getMessage(), e);
+ if (column != null) {
+ unsetSortableAttribute(column);
+ }
+ return;
+ }
+
+ // TODO: locale / comparator parameter?
+ // don't compare numbers with Collator.getInstance() comparator
+// Comparator comparator = Collator.getInstance();
+// comparator = new RowComparator(ascending, method);
+
+ // memorize selected rows
+ List