From 31507c2d6a558ebb456b72f5b5bdfc21018a7d5d Mon Sep 17 00:00:00 2001 From: Jim Bethancourt Date: Fri, 28 Jun 2024 10:44:49 -0500 Subject: [PATCH 1/3] Now using PMD 7.0.0-rc4, enabling Java 21 analysis --- cost-benefit-calculator/pom.xml | 7 + .../org/hjug/cbc/CostBenefitCalculator.java | 157 +- .../java/org/hjug/cbc/RankedDisharmony.java | 2 +- .../hjug/cbc/CostBenefitCalculatorTest.java | 99 +- .../src/test/resources/hudson/model/User.java | 1276 +++++++++++++++++ .../tobago/facelets/AttributeHandler.java | 368 +++++ .../tobago/facelets/AttributeHandler2.java | 372 +++++ .../facelets/AttributeHandlerAndSorter.java | 605 ++++++++ .../java/org/hjug/metrics/CBORuleRunner.java | 90 -- .../hjug/metrics/PMDGodClassRuleRunner.java | 89 -- .../java/org/hjug/metrics/rules/CBORule.java | 141 +- .../org/hjug/metrics/CBORuleRunnerTest.java | 38 - .../metrics/PMDGodClassRuleRunnerTest.java | 65 - .../org/hjug/gdg/GraphDataGeneratorTest.java | 2 +- pom.xml | 15 +- .../RefactorFirstRealMavenReport.java | 29 +- .../hjug/refactorfirst/report/CsvReport.java | 14 +- .../hjug/refactorfirst/report/HtmlReport.java | 33 +- .../report/json/JsonReportExecutor.java | 7 + 19 files changed, 2892 insertions(+), 517 deletions(-) create mode 100644 cost-benefit-calculator/src/test/resources/hudson/model/User.java create mode 100644 cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler.java create mode 100644 cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler2.java create mode 100644 cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandlerAndSorter.java delete mode 100644 effort-ranker/src/main/java/org/hjug/metrics/CBORuleRunner.java delete mode 100644 effort-ranker/src/main/java/org/hjug/metrics/PMDGodClassRuleRunner.java delete mode 100644 effort-ranker/src/test/java/org/hjug/metrics/CBORuleRunnerTest.java delete mode 100644 effort-ranker/src/test/java/org/hjug/metrics/PMDGodClassRuleRunnerTest.java 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.changepronenessranker change-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. + *

+ * The key needs to be generated by {@link IdStrategy#keyFor(String)}. + */ + private static AllUsers getInstance() { + return ExtensionList.lookupSingleton(AllUsers.class); + } + + private static void reload() { + getInstance().byName.clear(); + UserDetailsCache.get().invalidateAll(); + scanAll(); + } + + private static void clear() { + getInstance().byName.clear(); + } + + private static void remove(String id) { + getInstance().byName.remove(idStrategy().keyFor(id)); + } + + private static User get(String id) { + return getInstance().byName.get(idStrategy().keyFor(id)); + } + + private static void put(String id, User user) { + getInstance().byName.putIfAbsent(idStrategy().keyFor(id), user); + } + + private static Collection values() { + return getInstance().byName.values(); + } + } + + /** + * Resolves User IDs by ID, full names or other strings. + *

+ * 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 selectedDataRows = null; + if (sheetState.getSelectedRows().size() > 0) { + selectedDataRows = new ArrayList(sheetState.getSelectedRows().size()); + Object dataRow; + for (final Integer index : sheetState.getSelectedRows()) { + if (value instanceof List) { + dataRow = ((List) value).get(index); + } else { + dataRow = ((Object[]) value)[index]; + } + selectedDataRows.add(dataRow); + } + } + + // do sorting + if (value instanceof List) { + Collections.sort((List) value, actualComparator); + } else { // value is instanceof Object[] + Arrays.sort((Object[]) value, actualComparator); + } + + // restore selected rows + if (selectedDataRows != null) { + sheetState.getSelectedRows().clear(); + for (final Object dataRow : selectedDataRows) { + int index = -1; + if (value instanceof List) { + for (int i = 0; i < ((List) value).size() && index < 0; i++) { + if (dataRow == ((List) value).get(i)) { + index = i; + } + } + } else { + for (int i = 0; i < ((Object[]) value).length && index < 0; i++) { + if (dataRow == ((Object[]) value)[i]) { + index = i; + } + } + } + if (index >= 0) { + sheetState.getSelectedRows().add(index); + } + } + } + + } else { // DataModel?, ResultSet, Result or Object + LOG.warn("Sorting not supported for type " + + (value != null ? value.getClass().toString() : "null")); + } + } + + // XXX needs to be tested + // XXX was based on ^#\{(\w+(\.\w)*)\}$ which is wrong, because there is a + missing after the last \w + boolean isSimpleProperty(final String expressionString) { + if (expressionString.startsWith("#{") && expressionString.endsWith("}")) { + final String inner = expressionString.substring(2, expressionString.length() - 1); + final String[] parts = StringUtils.split(inner, '.'); + for (final String part : parts) { + if (!StringUtils.isAlpha(part)) { + return false; + } + } + return true; + } + return false; + } + + private void unsetSortableAttribute(final UIColumn uiColumn) { + LOG.warn("removing attribute sortable from column " + uiColumn.getId()); + uiColumn.getAttributes().put(Attributes.SORTABLE, Boolean.FALSE); + } + + private UIComponent getFirstSortableChild(final List children) { + UIComponent result = null; + + for (UIComponent child : children) { + result = child; + if (child instanceof UISelectMany + || child instanceof UISelectOne + || child instanceof UISelectBoolean + || (child instanceof AbstractUICommand && child.getChildren().isEmpty()) + || (child instanceof UIInput && RendererTypes.HIDDEN.equals(child.getRendererType()))) { + continue; + // look for a better component if any + } + if (child instanceof UIOutput) { + break; + } + if (child instanceof UICommand + || child instanceof javax.faces.component.UIPanel) { + child = getFirstSortableChild(child.getChildren()); + if (child instanceof UIOutput) { + break; + } + } + } + return result; + } + + public Comparator getComparator() { + return comparator; + } + + public void setComparator(final Comparator comparator) { + this.comparator = comparator; + } +} diff --git a/effort-ranker/src/main/java/org/hjug/metrics/CBORuleRunner.java b/effort-ranker/src/main/java/org/hjug/metrics/CBORuleRunner.java deleted file mode 100644 index 9dc825ad..00000000 --- a/effort-ranker/src/main/java/org/hjug/metrics/CBORuleRunner.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.hjug.metrics; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Optional; -import lombok.extern.slf4j.Slf4j; -import net.sourceforge.pmd.*; -import net.sourceforge.pmd.lang.Language; -import net.sourceforge.pmd.lang.LanguageRegistry; -import net.sourceforge.pmd.lang.java.JavaLanguageModule; -import org.hjug.metrics.rules.CBORule; - -// based on http://sdoulger.blogspot.com/2010/12/call-pmd-from-your-code-with-you-custom.html -@Slf4j -public class CBORuleRunner { - - private SourceCodeProcessor sourceCodeProcessor; - private RuleSets ruleSets; - private Language java = LanguageRegistry.getLanguage(JavaLanguageModule.NAME); - - public CBORuleRunner() { - PMD pmd = new PMD(); - sourceCodeProcessor = pmd.getSourceCodeProcessor(); - - Rule cboClassRule = new CBORule(); - cboClassRule.setLanguage(java); - - // add your rule to the ruleset - RuleSetFactory ruleSetFactory = new RuleSetFactory(); - RuleSet ruleSet2 = ruleSetFactory.createSingleRuleRuleSet(cboClassRule); - - ruleSets = new RuleSets(ruleSet2); - } - - public Optional runCBOClassRule(File file) { - // TODO: Capture file path and file ref? - return runPMD(file); - } - - public Optional runCBOClassRule(String name, InputStream fis) { - return runPMD(name, fis); - } - - /** - * Runs PMD on the specific file with the specific rule. - * @param file target file - * @return List with errors. If empty then no error - */ - public Optional runPMD(File file) { - - FileInputStream fis = null; - try { - fis = new FileInputStream(file); - } catch (FileNotFoundException ignore) { - log.warn("{} was not found", file.getName()); - } - - return runPMD(file.getName(), fis); - } - - public Optional runPMD(String sourceCodeFileName, InputStream inputStream) { - CBOClass cboClass = null; - try { - // Set the javaVersion you are using. (*1) - // pmd.setJavaVersion(SourceType.JAVA_16); -- MAY NEED TO SPECIFY THIS... - // Get a context and initialize it with The Report that PMD will return - final RuleContext ctx = new RuleContext(); - ctx.setReport(new Report()); - // target filename - ctx.setSourceCodeFile(new File(sourceCodeFileName)); - sourceCodeProcessor.processSourceCode(inputStream, ruleSets, ctx); - - // write results - if (!ctx.getReport().isEmpty()) { - for (final RuleViolation violation : ctx.getReport()) { - cboClass = new CBOClass( - violation.getClassName(), - sourceCodeFileName, - violation.getPackageName(), - violation.getDescription()); - } - } - } catch (PMDException ignore) { - log.warn("runPMD failed", ignore); - } - return Optional.ofNullable(cboClass); - } -} diff --git a/effort-ranker/src/main/java/org/hjug/metrics/PMDGodClassRuleRunner.java b/effort-ranker/src/main/java/org/hjug/metrics/PMDGodClassRuleRunner.java deleted file mode 100644 index aeb92ccf..00000000 --- a/effort-ranker/src/main/java/org/hjug/metrics/PMDGodClassRuleRunner.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.hjug.metrics; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Optional; -import lombok.extern.slf4j.Slf4j; -import net.sourceforge.pmd.*; -import net.sourceforge.pmd.lang.Language; -import net.sourceforge.pmd.lang.LanguageRegistry; -import net.sourceforge.pmd.lang.java.JavaLanguageModule; - -// based on http://sdoulger.blogspot.com/2010/12/call-pmd-from-your-code-with-you-custom.html -@Slf4j -public class PMDGodClassRuleRunner { - - private SourceCodeProcessor sourceCodeProcessor; - private Language java = LanguageRegistry.getLanguage(JavaLanguageModule.NAME); - private RuleSets ruleSets; - - public PMDGodClassRuleRunner() { - PMD pmd = new PMD(); - sourceCodeProcessor = pmd.getSourceCodeProcessor(); - - RuleSet ruleSet = new RuleSetLoader().loadFromResource("category/java/design.xml"); - Rule godClassRule = ruleSet.getRuleByName("GodClass"); - godClassRule.setLanguage(java); - - // add your rule to the ruleset - RuleSetFactory ruleSetFactory = new RuleSetFactory(); - RuleSet ruleSet2 = ruleSetFactory.createSingleRuleRuleSet(godClassRule); - ruleSets = new RuleSets(ruleSet2); - } - - public Optional runGodClassRule(File file) { - // TODO: Capture file path and file ref? - return runPMD(file); - } - - public Optional runGodClassRule(String name, InputStream fis) { - return runPMD(name, fis); - } - - /** - * Runs PMD on the specific file with the specific rule. - * @param file target file - * @return List with errors. If empty then no error - */ - public Optional runPMD(File file) { - - FileInputStream fis = null; - try { - fis = new FileInputStream(file); - } catch (FileNotFoundException ignore) { - log.warn("{} was not found", file.getName()); - } - - return runPMD(file.getName(), fis); - } - - public Optional runPMD(String sourceCodeFileName, InputStream inputStream) { - GodClass godClass = null; - try { - // Set the javaVersion you are using. (*1) - // pmd.setJavaVersion(SourceType.JAVA_16); -- MAY NEED TO SPECIFY THIS... - // Get a context and initialize it with The Report that PMD will return - final RuleContext ctx = new RuleContext(); - ctx.setReport(new Report()); - // target filename - ctx.setSourceCodeFile(new File(sourceCodeFileName)); - sourceCodeProcessor.processSourceCode(inputStream, ruleSets, ctx); - - // write results - if (!ctx.getReport().isEmpty()) { - for (final RuleViolation violation : ctx.getReport()) { - godClass = new GodClass( - violation.getClassName(), - sourceCodeFileName, - violation.getPackageName(), - violation.getDescription()); - } - } - } catch (PMDException ignore) { - log.warn("runPMD failed", ignore); - } - return Optional.ofNullable(godClass); - } -} diff --git a/effort-ranker/src/main/java/org/hjug/metrics/rules/CBORule.java b/effort-ranker/src/main/java/org/hjug/metrics/rules/CBORule.java index b54f96db..a2ac8e5a 100644 --- a/effort-ranker/src/main/java/org/hjug/metrics/rules/CBORule.java +++ b/effort-ranker/src/main/java/org/hjug/metrics/rules/CBORule.java @@ -1,24 +1,27 @@ package org.hjug.metrics.rules; import java.util.HashSet; -import java.util.List; import java.util.Set; -import net.sourceforge.pmd.lang.ast.Node; -import net.sourceforge.pmd.lang.java.ast.*; +import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; +import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTFormalParameter; +import net.sourceforge.pmd.lang.java.ast.ASTLocalVariableDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTType; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; -import net.sourceforge.pmd.lang.java.symboltable.ClassScope; +import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol; +import net.sourceforge.pmd.lang.java.types.JTypeMirror; +import net.sourceforge.pmd.properties.NumericConstraints; import net.sourceforge.pmd.properties.PropertyBuilder; import net.sourceforge.pmd.properties.PropertyDescriptor; import net.sourceforge.pmd.properties.PropertyFactory; -import net.sourceforge.pmd.properties.constraints.NumericConstraints; /** * Copy of PMD's CouplingBetweenObjectsRule * but generates the originally intended message containing coupling count */ public class CBORule extends AbstractJavaRule { - private int couplingCount; - private Set typesFoundSoFar; private static final PropertyDescriptor THRESHOLD_DESCRIPTOR = ((PropertyBuilder.GenericPropertyBuilder) ((PropertyBuilder.GenericPropertyBuilder) ((PropertyBuilder.GenericPropertyBuilder) PropertyFactory.intProperty("threshold") @@ -26,110 +29,86 @@ public class CBORule extends AbstractJavaRule { .require(NumericConstraints.positive())) .defaultValue(20)) .build(); + private int couplingCount; + private boolean inInterface; + private final Set typesFoundSoFar = new HashSet(); + + private String message; public CBORule() { this.definePropertyDescriptor(THRESHOLD_DESCRIPTOR); } + @Override + public String getMessage() { + return message; + } + + @Override public Object visit(ASTCompilationUnit cu, Object data) { - this.typesFoundSoFar = new HashSet(); - this.couplingCount = 0; - Object returnObj = super.visit(cu, data); - if (this.couplingCount > (Integer) this.getProperty(THRESHOLD_DESCRIPTOR)) { - // only the line below is different from PMD's CouplingBetweenObjectsRule class - this.addViolationWithMessage( - data, - cu, - "A value of " + this.couplingCount + " may denote a high amount of coupling within the class"); + super.visit(cu, data); + if (this.couplingCount > 20) { // (Integer) this.getProperty(THRESHOLD_DESCRIPTOR)) { + message = "A value of " + this.couplingCount + " may denote a high amount of coupling within the class"; + this.addViolation(data, cu, message); + this.setMessage(message); } - return returnObj; + this.couplingCount = 0; + this.typesFoundSoFar.clear(); + return null; } - public Object visit(ASTResultType node, Object data) { - for (int x = 0; x < node.getNumChildren(); ++x) { - Node tNode = node.getChild(x); - if (tNode instanceof ASTType) { - Node reftypeNode = tNode.getChild(0); - if (reftypeNode instanceof ASTReferenceType) { - Node classOrIntType = reftypeNode.getChild(0); - if (classOrIntType instanceof ASTClassOrInterfaceType) { - this.checkVariableType(classOrIntType, classOrIntType.getImage()); - } - } - } - } + public Object visit(ASTClassOrInterfaceDeclaration node, Object data) { + boolean prev = this.inInterface; + this.inInterface = node.isInterface(); + super.visit(node, data); + this.inInterface = prev; + return null; + } + public Object visit(ASTMethodDeclaration node, Object data) { + ASTType type = node.getResultTypeNode(); + this.checkVariableType(type); return super.visit(node, data); } public Object visit(ASTLocalVariableDeclaration node, Object data) { - this.handleASTTypeChildren(node); + ASTType type = node.getTypeNode(); + this.checkVariableType(type); return super.visit(node, data); } public Object visit(ASTFormalParameter node, Object data) { - this.handleASTTypeChildren(node); + ASTType type = node.getTypeNode(); + this.checkVariableType(type); return super.visit(node, data); } public Object visit(ASTFieldDeclaration node, Object data) { - for (int x = 0; x < node.getNumChildren(); ++x) { - Node firstStmt = node.getChild(x); - if (firstStmt instanceof ASTType) { - ASTType tp = (ASTType) firstStmt; - Node nd = tp.getChild(0); - this.checkVariableType(nd, nd.getImage()); - } - } - + ASTType type = node.getTypeNode(); + this.checkVariableType(type); return super.visit(node, data); } - private void handleASTTypeChildren(Node node) { - for (int x = 0; x < node.getNumChildren(); ++x) { - Node sNode = node.getChild(x); - if (sNode instanceof ASTType) { - Node nameNode = sNode.getChild(0); - this.checkVariableType(nameNode, nameNode.getImage()); + private void checkVariableType(ASTType typeNode) { + if (!this.inInterface && typeNode != null) { + JTypeMirror t = typeNode.getTypeMirror(); + if (!this.ignoreType(typeNode, t) && this.typesFoundSoFar.add(t)) { + ++this.couplingCount; } } } - private void checkVariableType(Node nameNode, String variableType) { - List parentTypes = - nameNode.getParentsOfType(ASTClassOrInterfaceDeclaration.class); - if (!parentTypes.isEmpty()) { - if (!((ASTClassOrInterfaceDeclaration) parentTypes.get(0)).isInterface()) { - ClassScope clzScope = - (ClassScope) ((JavaNode) nameNode).getScope().getEnclosingScope(ClassScope.class); - if (!clzScope.getClassName().equals(variableType) - && !this.filterTypes(variableType) - && !this.typesFoundSoFar.contains(variableType)) { - ++this.couplingCount; - this.typesFoundSoFar.add(variableType); - } - } + private boolean ignoreType(ASTType typeNode, JTypeMirror t) { + if (typeNode.getEnclosingType() != null + && typeNode.getEnclosingType().getSymbol().equals(t.getSymbol())) { + return true; + } else { + JTypeDeclSymbol symbol = t.getSymbol(); + return symbol == null + || "java.lang".equals(symbol.getPackageName()) + || t.isPrimitive() + || t.isBoxedPrimitive(); } } - - private boolean filterTypes(String variableType) { - return variableType != null - && (variableType.startsWith("java.lang.") - || "String".equals(variableType) - || this.filterPrimitivesAndWrappers(variableType)); - } - - private boolean filterPrimitivesAndWrappers(String variableType) { - return "int".equals(variableType) - || "Integer".equals(variableType) - || "char".equals(variableType) - || "Character".equals(variableType) - || "double".equals(variableType) - || "long".equals(variableType) - || "short".equals(variableType) - || "float".equals(variableType) - || "byte".equals(variableType) - || "boolean".equals(variableType); - } } diff --git a/effort-ranker/src/test/java/org/hjug/metrics/CBORuleRunnerTest.java b/effort-ranker/src/test/java/org/hjug/metrics/CBORuleRunnerTest.java deleted file mode 100644 index d3175872..00000000 --- a/effort-ranker/src/test/java/org/hjug/metrics/CBORuleRunnerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.hjug.metrics; - -import java.util.Optional; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class CBORuleRunnerTest { - - private CBORuleRunner CBORuleRunner; - - @BeforeEach - public void setUp() { - CBORuleRunner = new CBORuleRunner(); - } - - @Test - void testRuleRunnerExpectOneClass() throws Exception { - String attributeHandler = "Console.java"; - Optional optionalResult = CBORuleRunner.runCBOClassRule( - attributeHandler, getClass().getClassLoader().getResourceAsStream(attributeHandler)); - - CBOClass result = optionalResult.get(); - - Assertions.assertEquals("Console.java", result.getFileName()); - Assertions.assertEquals("io.confluent.ksql.cli.console", result.getPackageName()); - Assertions.assertEquals(35, result.getCouplingCount().longValue()); - } - - @Test - void testRuleRunnerExpectNoResults() throws Exception { - String attributeHandler = "Attributes.java"; - Optional result = CBORuleRunner.runCBOClassRule( - attributeHandler, getClass().getClassLoader().getResourceAsStream(attributeHandler)); - - Assertions.assertFalse(result.isPresent()); - } -} diff --git a/effort-ranker/src/test/java/org/hjug/metrics/PMDGodClassRuleRunnerTest.java b/effort-ranker/src/test/java/org/hjug/metrics/PMDGodClassRuleRunnerTest.java deleted file mode 100644 index 68931be2..00000000 --- a/effort-ranker/src/test/java/org/hjug/metrics/PMDGodClassRuleRunnerTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.hjug.metrics; - -import java.util.Optional; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class PMDGodClassRuleRunnerTest { - - private PMDGodClassRuleRunner PMDGodClassRuleRunner; - - @BeforeEach - public void setUp() { - PMDGodClassRuleRunner = new PMDGodClassRuleRunner(); - } - - @Test - void testRuleRunnerExpectOneClass() throws Exception { - String attributeHandler = "AttributeHandler.java"; - Optional optionalResult = PMDGodClassRuleRunner.runGodClassRule( - attributeHandler, getClass().getClassLoader().getResourceAsStream(attributeHandler)); - - GodClass result = optionalResult.get(); - - Assertions.assertEquals("AttributeHandler.java", result.getFileName()); - Assertions.assertEquals("org.apache.myfaces.tobago.facelets", result.getPackageName()); - Assertions.assertEquals(77, result.getWmc().longValue()); - Assertions.assertEquals(105, result.getAtfd().longValue()); - Assertions.assertEquals(15.555999755859375, result.getTcc(), 0.001); - } - - @Test - void testRuleRunnerExpectJavaElevenClass() throws Exception { - String attributeHandler = "AttributeHandlerJavaEleven.java"; - Optional optionalResult = PMDGodClassRuleRunner.runGodClassRule( - attributeHandler, getClass().getClassLoader().getResourceAsStream(attributeHandler)); - - GodClass result = optionalResult.get(); - - Assertions.assertEquals("AttributeHandlerJavaEleven.java", result.getFileName()); - Assertions.assertEquals("org.apache.myfaces.tobago.facelets", result.getPackageName()); - Assertions.assertEquals(77, result.getWmc().longValue()); - Assertions.assertEquals(105, result.getAtfd().longValue()); - Assertions.assertEquals(15.555999755859375, result.getTcc(), 0.001); - } - - @Test - void testRuleRunnerExpectNoResults() throws Exception { - String attributeHandler = "Attributes.java"; - Optional result = PMDGodClassRuleRunner.runGodClassRule( - attributeHandler, getClass().getClassLoader().getResourceAsStream(attributeHandler)); - - Assertions.assertFalse(result.isPresent()); - } - - // Only returns one result - @Test - void testRuleRunnerWithTwoClasses() throws Exception { - String attributeHandler = "AttributeHandlerAndSorter.java"; - Optional result = PMDGodClassRuleRunner.runGodClassRule( - attributeHandler, getClass().getClassLoader().getResourceAsStream(attributeHandler)); - - Assertions.assertTrue(result.isPresent()); - } -} diff --git a/graph-data-generator/src/test/java/org/hjug/gdg/GraphDataGeneratorTest.java b/graph-data-generator/src/test/java/org/hjug/gdg/GraphDataGeneratorTest.java index d228cee1..16d4d382 100644 --- a/graph-data-generator/src/test/java/org/hjug/gdg/GraphDataGeneratorTest.java +++ b/graph-data-generator/src/test/java/org/hjug/gdg/GraphDataGeneratorTest.java @@ -97,7 +97,7 @@ void generateBubbleChartDataTwoDataPoints() { String chartData = "[ 'ID', 'Effort', 'Change Proneness', 'Priority', 'Priority (Visual)'], " + "['AttributeHandler.java',0,0,1,0]," - + "['AttributeHandler.java',0,0,2,0]"; + + "['AttributeHandler.java',0,0,2,-1]"; Assertions.assertEquals(chartData, graphDataGenerator.generateGodClassBubbleChartData(rankedDisharmonies, 1)); } } diff --git a/pom.xml b/pom.xml index cae28c07..1da13a12 100644 --- a/pom.xml +++ b/pom.xml @@ -140,7 +140,7 @@ net.sourceforge.pmd pmd-java - 6.55.0 + 7.0.0-rc4 compile @@ -266,6 +266,19 @@ + + org.pitest + pitest-maven + 1.16.1 + + + org.pitest + pitest-junit5-plugin + 1.2.1 + + + + diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstRealMavenReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstRealMavenReport.java index 128c76be..077bfec6 100644 --- a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstRealMavenReport.java +++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstRealMavenReport.java @@ -210,6 +210,12 @@ public void executeReport(Locale locale) throws MavenReportException { } CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(); + try { + costBenefitCalculator.runPmdAnalysis(projectBaseDir); + } catch (IOException e) { + log.error("Error running PMD analysis."); + throw new RuntimeException(e); + } List rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(projectBaseDir); @@ -241,13 +247,9 @@ public void executeReport(Locale locale) throws MavenReportException { }*/ if (!rankedGodClassDisharmonies.isEmpty()) { - rankedGodClassDisharmonies.sort( - Comparator.comparing(RankedDisharmony::getRawPriority).reversed()); - - int godClassPriority = 1; - for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) { - rankedGodClassDisharmony.setPriority(godClassPriority++); - } + int maxGodClassPriority = rankedGodClassDisharmonies + .get(rankedGodClassDisharmonies.size() - 1) + .getPriority(); SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); @@ -260,7 +262,7 @@ public void executeReport(Locale locale) throws MavenReportException { mainSink.section2_(); mainSink.division_(); - writeGodClassGchartJs(rankedGodClassDisharmonies, godClassPriority - 1); + writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1); SinkEventAttributeSet seriesChartDiv = new SinkEventAttributeSet(); seriesChartDiv.addAttribute(SinkEventAttributes.ID, "series_chart_div"); seriesChartDiv.addAttribute(SinkEventAttributes.ALIGN, "center"); @@ -351,13 +353,8 @@ public void executeReport(Locale locale) throws MavenReportException { mainSink.lineBreak(); } - rankedCBODisharmonies.sort( - Comparator.comparing(RankedDisharmony::getRawPriority).reversed()); - - int cboPriority = 1; - for (RankedDisharmony rankedCBODisharmony : rankedCBODisharmonies) { - rankedCBODisharmony.setPriority(cboPriority++); - } + int maxCboPriority = + rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); @@ -375,7 +372,7 @@ public void executeReport(Locale locale) throws MavenReportException { seriesChartDiv.addAttribute(SinkEventAttributes.ALIGN, "center"); mainSink.division(seriesChartDiv); mainSink.division_(); - writeGCBOGchartJs(rankedCBODisharmonies, cboPriority - 1); + writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1); renderGitHubButtons(mainSink); diff --git a/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java b/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java index 29e99eba..756bcb2f 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java @@ -3,6 +3,7 @@ import static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk; import java.io.File; +import java.io.IOException; import java.nio.file.Paths; import java.time.Instant; import java.time.ZoneId; @@ -83,11 +84,16 @@ public void execute( // actual calcualte CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(); + try { + costBenefitCalculator.runPmdAnalysis(projectBaseDir); + } catch (IOException e) { + log.error("Error running PMD analysis."); + throw new RuntimeException(e); + } List rankedDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(projectBaseDir); - rankedDisharmonies.sort( - Comparator.comparing(RankedDisharmony::getRawPriority).reversed()); + rankedDisharmonies.sort(Comparator.comparing(RankedDisharmony::getPriority)); // perfect score: no god classes if (rankedDisharmonies.isEmpty()) { @@ -137,7 +143,7 @@ private DateTimeFormatter createCsvDateTimeFormatter() { private String[] getDataList(RankedDisharmony rankedDisharmony, boolean showDetails) { String[] simpleRankedDisharmonyData = { rankedDisharmony.getFileName(), - rankedDisharmony.getRawPriority().toString(), + rankedDisharmony.getPriority().toString(), rankedDisharmony.getChangePronenessRank().toString(), rankedDisharmony.getEffortRank().toString(), rankedDisharmony.getWmc().toString(), @@ -147,7 +153,7 @@ private String[] getDataList(RankedDisharmony rankedDisharmony, boolean showDeta String[] detailedRankedDisharmonyData = { rankedDisharmony.getFileName(), - rankedDisharmony.getRawPriority().toString(), + rankedDisharmony.getPriority().toString(), rankedDisharmony.getChangePronenessRank().toString(), rankedDisharmony.getEffortRank().toString(), rankedDisharmony.getWmc().toString(), diff --git a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java index 7233b62c..ad9ae8e9 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java @@ -11,7 +11,6 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; -import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -209,6 +208,12 @@ public void execute( } CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(); + try { + costBenefitCalculator.runPmdAnalysis(projectBaseDir); + } catch (IOException e) { + log.error("Error running PMD analysis."); + throw new RuntimeException(e); + } List rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(projectBaseDir); @@ -236,17 +241,13 @@ public void execute( } if (!rankedGodClassDisharmonies.isEmpty()) { - rankedGodClassDisharmonies.sort( - Comparator.comparing(RankedDisharmony::getRawPriority).reversed()); - - int godClassPriority = 1; - for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) { - rankedGodClassDisharmony.setPriority(godClassPriority++); - } + int maxGodClassPriority = rankedGodClassDisharmonies + .get(rankedGodClassDisharmonies.size() - 1) + .getPriority(); stringBuilder.append(""); - writeGodClassGchartJs(rankedGodClassDisharmonies, godClassPriority - 1, outputDirectory); + writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1, outputDirectory); stringBuilder.append("
"); renderGithubButtons(stringBuilder); stringBuilder.append(GOD_CLASS_CHART_LEGEND); @@ -312,19 +313,15 @@ public void execute( stringBuilder.append("
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
"); - rankedCBODisharmonies.sort( - Comparator.comparing(RankedDisharmony::getRawPriority).reversed()); - - int cboPriority = 1; - for (RankedDisharmony rankedCBODisharmony : rankedCBODisharmonies) { - rankedCBODisharmony.setPriority(cboPriority++); - } - stringBuilder.append( ""); stringBuilder.append("
"); - writeGCBOGchartJs(rankedCBODisharmonies, cboPriority - 1, outputDirectory); + + int maxCboPriority = + rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); + + writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1, outputDirectory); renderGithubButtons(stringBuilder); stringBuilder.append(COUPLING_BETWEEN_OBJECT_CHART_LEGEND); diff --git a/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java b/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java index ea431572..e9a0cea0 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; +import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; @@ -31,6 +32,12 @@ public void execute(File baseDir, String outputDirectory) { } final CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(); + try { + costBenefitCalculator.runPmdAnalysis(projectBaseDir); + } catch (IOException e) { + log.error("Error running PMD analysis."); + throw new RuntimeException(e); + } final List rankedDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(projectBaseDir); final List disharmonyEntries = rankedDisharmonies.stream() From 324c4938913af6985e57e4892b8855ea54fe3976 Mon Sep 17 00:00:00 2001 From: Jim Bethancourt Date: Fri, 28 Jun 2024 16:34:22 -0500 Subject: [PATCH 2/3] Add SimpleHtmlReport SimpleHtmlReport generates an HTML report that does not contain any JavaScript --- ...port.java => RefactorFirstHtmlReport.java} | 2 +- .../RefactorFirstSimpleHtmlReport.java | 53 +++ .../hjug/refactorfirst/report/HtmlReport.java | 358 ++-------------- .../report/SimpleHtmlReport.java | 394 ++++++++++++++++++ .../report/SimpleHtmlReportTest.java | 14 + 5 files changed, 497 insertions(+), 324 deletions(-) rename refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/{RefactorFirstMavenReport.java => RefactorFirstHtmlReport.java} (96%) create mode 100644 refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java create mode 100644 report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java create mode 100644 report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java similarity index 96% rename from refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java rename to refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java index 864b7fc7..b7b378f9 100644 --- a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java +++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java @@ -18,7 +18,7 @@ requiresProject = false, threadSafe = true, inheritByDefault = false) -public class RefactorFirstMavenReport extends AbstractMojo { +public class RefactorFirstHtmlReport extends AbstractMojo { @Parameter(property = "showDetails") private boolean showDetails = false; diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java new file mode 100644 index 00000000..40cefb2d --- /dev/null +++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java @@ -0,0 +1,53 @@ +package org.hjug.mavenreport; + +import java.io.File; +import lombok.extern.slf4j.Slf4j; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.hjug.refactorfirst.report.SimpleHtmlReport; + +@Slf4j +@Mojo( + name = "simpleHtmlReport", + defaultPhase = LifecyclePhase.SITE, + requiresDependencyResolution = ResolutionScope.RUNTIME, + requiresProject = false, + threadSafe = true, + inheritByDefault = false) +public class RefactorFirstSimpleHtmlReport extends AbstractMojo { + + @Parameter(property = "showDetails") + private boolean showDetails = false; + + @Parameter(defaultValue = "${project.name}") + private String projectName; + + @Parameter(defaultValue = "${project.version}") + private String projectVersion; + + @Parameter(readonly = true, defaultValue = "${project}") + private MavenProject project; + + @Parameter(property = "project.build.directory") + protected File outputDirectory; + + @Override + public void execute() { + + log.info(outputDirectory.getPath()); + SimpleHtmlReport htmlReport = new SimpleHtmlReport(); + htmlReport.execute( + showDetails, + projectName, + projectVersion, + project.getModel() + .getReporting() + .getOutputDirectory() + .replace("${project.basedir}" + File.separator, ""), + project.getBasedir()); + } +} diff --git a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java index ad9ae8e9..af54f71c 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java @@ -1,33 +1,17 @@ package org.hjug.refactorfirst.report; -import static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk; - import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.nio.file.Paths; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; import java.util.List; import java.util.Locale; -import java.util.Optional; import lombok.extern.slf4j.Slf4j; -import org.hjug.cbc.CostBenefitCalculator; import org.hjug.cbc.RankedDisharmony; import org.hjug.gdg.GraphDataGenerator; -import org.hjug.git.GitLogReader; @Slf4j -public class HtmlReport { - - public static final String THE_BEGINNING = - "\n" + " \n" - + " \n" - + " \n" - + " "; +public class HtmlReport extends SimpleHtmlReport { public static final String GOD_CLASS_CHART_LEGEND = "

God Class Chart Legend:

" + " \n" @@ -51,78 +35,8 @@ public class HtmlReport { + "
" + "
"; - public static final String THE_END = "\n" + " \n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + " Copyright © 2002–2021The Apache Software Foundation.\n" - + ".
\n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + " \n" - + "\n"; - - public void execute( - boolean showDetails, String projectName, String projectVersion, String outputDirectory, File baseDir) { - - final String[] godClassSimpleTableHeadings = { - "Class", - "Priority", - "Change Proneness Rank", - "Effort Rank", - "Method Count", - "Most Recent Commit Date", - "Commit Count" - }; - - final String[] godClassDetailedTableHeadings = { - "Class", - "Priority", - "Raw Priority", - "Change Proneness Rank", - "Effort Rank", - "WMC", - "WMC Rank", - "ATFD", - "ATFD Rank", - "TCC", - "TCC Rank", - "Date of First Commit", - "Most Recent Commit Date", - "Commit Count", - "Full Path" - }; - - final String[] cboTableHeadings = { - "Class", "Priority", "Change Proneness Rank", "Coupling Count", "Most Recent Commit Date", "Commit Count" - }; - - final String[] godClassTableHeadings = - showDetails ? godClassDetailedTableHeadings : godClassSimpleTableHeadings; - - String filename = getOutputName() + ".html"; - - log.info("Generating {} for {} - {}", filename, projectName, projectVersion); - - DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) - .withLocale(Locale.getDefault()) - .withZone(ZoneId.systemDefault()); - - StringBuilder stringBuilder = new StringBuilder(); - - stringBuilder.append(THE_BEGINNING); - - stringBuilder - .append("Refactor First Report for ") - .append(projectName) - .append(" ") - .append(projectVersion) - .append(" "); - + @Override + public void printHead(StringBuilder stringBuilder) { stringBuilder.append("\n" + " \n" + " \n" @@ -131,242 +45,20 @@ public void execute( + "" + "" - + " \n" - + " \n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + "
"); - - stringBuilder - .append("Last Published: ") - .append(formatter.format(Instant.now())) - .append(""); - stringBuilder - .append(" Version: ") - .append(projectVersion) - .append(""); - - stringBuilder.append("
\n" + "
\n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + "
\n" - + "
"); + + " \n"); + } + @Override + public void printTitle(String projectName, String projectVersion, StringBuilder stringBuilder) { stringBuilder - .append("
\n" + "

RefactorFirst Report for ") + .append("Refactor First Report for ") .append(projectName) .append(" ") .append(projectVersion) - .append("</h2>\n"); - - GitLogReader gitLogReader = new GitLogReader(); - String projectBaseDir; - Optional<File> optionalGitDir; - - if (baseDir != null) { - projectBaseDir = baseDir.getPath(); - optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(baseDir)); - } else { - projectBaseDir = Paths.get("").toAbsolutePath().toString(); - optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(new File(projectBaseDir))); - } - - File gitDir; - if (optionalGitDir.isPresent()) { - gitDir = optionalGitDir.get(); - } else { - log.info( - "Done! No Git repository found! Please initialize a Git repository and perform an initial commit."); - stringBuilder - .append("No Git repository found in project ") - .append(projectName) - .append(" ") - .append(projectVersion) - .append(". "); - stringBuilder.append("Please initialize a Git repository and perform an initial commit."); - stringBuilder.append(THE_END); - writeReportToDisk(outputDirectory, filename, stringBuilder); - return; - } - - String parentOfGitDir = gitDir.getParentFile().getPath(); - log.info("Project Base Dir: {} ", projectBaseDir); - log.info("Parent of Git Dir: {}", parentOfGitDir); - - if (!projectBaseDir.equals(parentOfGitDir)) { - log.warn("Project Base Directory does not match Git Parent Directory"); - stringBuilder.append("Project Base Directory does not match Git Parent Directory. " - + "Please refer to the report at the root of the site directory."); - stringBuilder.append(THE_END); - return; - } - - CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(); - try { - costBenefitCalculator.runPmdAnalysis(projectBaseDir); - } catch (IOException e) { - log.error("Error running PMD analysis."); - throw new RuntimeException(e); - } - List<RankedDisharmony> rankedGodClassDisharmonies = - costBenefitCalculator.calculateGodClassCostBenefitValues(projectBaseDir); - - List<RankedDisharmony> rankedCBODisharmonies = - costBenefitCalculator.calculateCBOCostBenefitValues(projectBaseDir); - - if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty()) { - stringBuilder - .append("Congratulations! ") - .append(projectName) - .append(" ") - .append(projectVersion) - .append(" has no God classes or highly coupled classes!"); - renderGithubButtons(stringBuilder); - log.info("Done! No Disharmonies found!"); - stringBuilder.append(THE_END); - writeReportToDisk(outputDirectory, filename, stringBuilder); - return; - } - - if (!rankedGodClassDisharmonies.isEmpty() && !rankedCBODisharmonies.isEmpty()) { - stringBuilder.append("<a href=\"#GOD\">God Classes</a>"); - stringBuilder.append("<br/>"); - stringBuilder.append("<a href=\"#CBO\">Highly Coupled Classes</a>"); - } - - if (!rankedGodClassDisharmonies.isEmpty()) { - int maxGodClassPriority = rankedGodClassDisharmonies - .get(rankedGodClassDisharmonies.size() - 1) - .getPriority(); - - stringBuilder.append("<div style=\"text-align: center;\"><a id=\"GOD\"><h1>God Classes</h1></a></div>"); - - writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1, outputDirectory); - stringBuilder.append("<div id=\"series_chart_div\" align=\"center\"></div>"); - renderGithubButtons(stringBuilder); - stringBuilder.append(GOD_CLASS_CHART_LEGEND); - - stringBuilder.append( - "<h2 align=\"center\">God classes by the numbers: (Refactor Starting with Priority 1)</h2>"); - stringBuilder.append("<table align=\"center\" border=\"5px\">"); - - // Content - stringBuilder.append("<thead><tr>"); - for (String heading : godClassTableHeadings) { - stringBuilder.append("<th>").append(heading).append("</th>"); - } - stringBuilder.append("</tr></thead>"); - - stringBuilder.append("<tbody>"); - for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) { - stringBuilder.append("<tr>"); - - String[] simpleRankedGodClassDisharmonyData = { - rankedGodClassDisharmony.getFileName(), - rankedGodClassDisharmony.getPriority().toString(), - rankedGodClassDisharmony.getChangePronenessRank().toString(), - rankedGodClassDisharmony.getEffortRank().toString(), - rankedGodClassDisharmony.getWmc().toString(), - formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), - rankedGodClassDisharmony.getCommitCount().toString() - }; - - String[] detailedRankedGodClassDisharmonyData = { - rankedGodClassDisharmony.getFileName(), - rankedGodClassDisharmony.getPriority().toString(), - rankedGodClassDisharmony.getRawPriority().toString(), - rankedGodClassDisharmony.getChangePronenessRank().toString(), - rankedGodClassDisharmony.getEffortRank().toString(), - rankedGodClassDisharmony.getWmc().toString(), - rankedGodClassDisharmony.getWmcRank().toString(), - rankedGodClassDisharmony.getAtfd().toString(), - rankedGodClassDisharmony.getAtfdRank().toString(), - rankedGodClassDisharmony.getTcc().toString(), - rankedGodClassDisharmony.getTccRank().toString(), - formatter.format(rankedGodClassDisharmony.getFirstCommitTime()), - formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), - rankedGodClassDisharmony.getCommitCount().toString(), - rankedGodClassDisharmony.getPath() - }; - - final String[] rankedDisharmonyData = - showDetails ? detailedRankedGodClassDisharmonyData : simpleRankedGodClassDisharmonyData; - - for (String rowData : rankedDisharmonyData) { - stringBuilder.append("<td>").append(rowData).append("</td>"); - } - - stringBuilder.append("</tr>"); - } - - stringBuilder.append("</tbody>"); - stringBuilder.append("</table>"); - } - - if (!rankedCBODisharmonies.isEmpty()) { - - stringBuilder.append("<br/>\n" + "<br/>\n" + "<br/>\n" + "<br/>\n" + "<hr/>\n" + "<br/>\n" + "<br/>"); - - stringBuilder.append( - "<div style=\"text-align: center;\"><a id=\"CBO\"><h1>Highly Coupled Classes</h1></a></div>"); - - stringBuilder.append("<div id=\"series_chart_div_2\" align=\"center\"></div>"); - - int maxCboPriority = - rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); - - writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1, outputDirectory); - renderGithubButtons(stringBuilder); - stringBuilder.append(COUPLING_BETWEEN_OBJECT_CHART_LEGEND); - - stringBuilder.append("<h2>Highly Coupled classes by the numbers: (Refactor starting with Priority 1)</h2>"); - stringBuilder.append("<table align=\"center\" border=\"5px\">"); - - // Content - stringBuilder.append("<thead><tr>"); - for (String heading : cboTableHeadings) { - stringBuilder.append("<th>").append(heading).append("</th>"); - } - stringBuilder.append("</tr></thead>"); - - stringBuilder.append("<tbody>"); - for (RankedDisharmony rankedCboClassDisharmony : rankedCBODisharmonies) { - stringBuilder.append("<tr>"); - - String[] rankedCboClassDisharmonyData = { - rankedCboClassDisharmony.getFileName(), - rankedCboClassDisharmony.getPriority().toString(), - rankedCboClassDisharmony.getChangePronenessRank().toString(), - rankedCboClassDisharmony.getEffortRank().toString(), - formatter.format(rankedCboClassDisharmony.getMostRecentCommitTime()), - rankedCboClassDisharmony.getCommitCount().toString() - }; - - for (String rowData : rankedCboClassDisharmonyData) { - stringBuilder.append("<td>").append(rowData).append("</td>"); - } - - stringBuilder.append("</tr>"); - } - - stringBuilder.append("</tbody>"); - } - - stringBuilder.append("</table></section>"); - stringBuilder.append(THE_END); - - log.debug(stringBuilder.toString()); - - writeReportToDisk(outputDirectory, filename, stringBuilder); - log.info("Done! View the report at target/site/{}", filename); + .append(" "); } + @Override void renderGithubButtons(StringBuilder stringBuilder) { stringBuilder.append("
"); stringBuilder.append("Show RefactorFirst some ❤️"); @@ -414,6 +106,7 @@ void writeGodClassGchartJs( } } + @Override void writeGCBOGchartJs(List rankedDisharmonies, int maxPriority, String reportOutputDirectory) { GraphDataGenerator graphDataGenerator = new GraphDataGenerator(); String scriptStart = graphDataGenerator.getCBOScriptStart(); @@ -442,11 +135,6 @@ void writeGCBOGchartJs(List rankedDisharmonies, int maxPriorit } } - public String getOutputName() { - // This report will generate simple-report.html when invoked in a project with `mvn site` - return "refactor-first-report"; - } - public String getName(Locale locale) { // Name of the report when listed in the project-reports.html page of a project return "Refactor First Report"; @@ -457,4 +145,28 @@ public String getDescription(Locale locale) { return "Ranks the disharmonies in a codebase. The classes that should be refactored first " + " have the highest priority values."; } + + @Override + void renderGodClassChart( + String outputDirectory, + List rankedGodClassDisharmonies, + int maxGodClassPriority, + StringBuilder stringBuilder) { + writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1, outputDirectory); + stringBuilder.append("
"); + renderGithubButtons(stringBuilder); + stringBuilder.append(GOD_CLASS_CHART_LEGEND); + } + + @Override + void renderCBOChart( + String outputDirectory, + List rankedCBODisharmonies, + int maxCboPriority, + StringBuilder stringBuilder) { + writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1, outputDirectory); + stringBuilder.append("
"); + renderGithubButtons(stringBuilder); + stringBuilder.append(COUPLING_BETWEEN_OBJECT_CHART_LEGEND); + } } diff --git a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java new file mode 100644 index 00000000..91657c46 --- /dev/null +++ b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java @@ -0,0 +1,394 @@ +package org.hjug.refactorfirst.report; + +import static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.hjug.cbc.CostBenefitCalculator; +import org.hjug.cbc.RankedDisharmony; +import org.hjug.git.GitLogReader; + +/** + * Strictly HTML report that contains no JavaScript + * Generates only tables + */ +@Slf4j +public class SimpleHtmlReport { + + public static final String THE_BEGINNING = + "\n" + " \n" + + " \n" + + " \n" + + " "; + + public static final String THE_END = "
\n" + "

\n" + /*+ "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " Copyright © 2002–2021The Apache Software Foundation.\n" + + ".
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n"*/ + + " \n" + + "\n"; + + public final String[] godClassSimpleTableHeadings = { + "Class", + "Priority", + "Change Proneness Rank", + "Effort Rank", + "Method Count", + "Most Recent Commit Date", + "Commit Count" + }; + + public final String[] godClassDetailedTableHeadings = { + "Class", + "Priority", + "Raw Priority", + "Change Proneness Rank", + "Effort Rank", + "WMC", + "WMC Rank", + "ATFD", + "ATFD Rank", + "TCC", + "TCC Rank", + "Date of First Commit", + "Most Recent Commit Date", + "Commit Count", + "Full Path" + }; + + public final String[] cboTableHeadings = { + "Class", "Priority", "Change Proneness Rank", "Coupling Count", "Most Recent Commit Date", "Commit Count" + }; + + public void execute( + boolean showDetails, String projectName, String projectVersion, String outputDirectory, File baseDir) { + + final String[] godClassTableHeadings = + showDetails ? godClassDetailedTableHeadings : godClassSimpleTableHeadings; + + String filename = getOutputName() + ".html"; + + log.info("Generating {} for {} - {}", filename, projectName, projectVersion); + + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(THE_BEGINNING); + + printTitle(projectName, projectVersion, stringBuilder); + printHead(stringBuilder); + printBreadcrumbs(stringBuilder); + printProjectHeader(projectName, projectVersion, stringBuilder, formatter); + + GitLogReader gitLogReader = new GitLogReader(); + String projectBaseDir; + Optional optionalGitDir; + + if (baseDir != null) { + projectBaseDir = baseDir.getPath(); + optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(baseDir)); + } else { + projectBaseDir = Paths.get("").toAbsolutePath().toString(); + optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(new File(projectBaseDir))); + } + + File gitDir; + if (optionalGitDir.isPresent()) { + gitDir = optionalGitDir.get(); + } else { + log.info( + "Done! No Git repository found! Please initialize a Git repository and perform an initial commit."); + stringBuilder + .append("No Git repository found in project ") + .append(projectName) + .append(" ") + .append(projectVersion) + .append(". "); + stringBuilder.append("Please initialize a Git repository and perform an initial commit."); + stringBuilder.append(THE_END); + writeReportToDisk(outputDirectory, filename, stringBuilder); + return; + } + + String parentOfGitDir = gitDir.getParentFile().getPath(); + log.info("Project Base Dir: {} ", projectBaseDir); + log.info("Parent of Git Dir: {}", parentOfGitDir); + + if (!projectBaseDir.equals(parentOfGitDir)) { + log.warn("Project Base Directory does not match Git Parent Directory"); + stringBuilder.append("Project Base Directory does not match Git Parent Directory. " + + "Please refer to the report at the root of the site directory."); + stringBuilder.append(THE_END); + return; + } + + CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(); + try { + costBenefitCalculator.runPmdAnalysis(projectBaseDir); + } catch (IOException e) { + log.error("Error running PMD analysis."); + throw new RuntimeException(e); + } + List rankedGodClassDisharmonies = + costBenefitCalculator.calculateGodClassCostBenefitValues(projectBaseDir); + + List rankedCBODisharmonies = + costBenefitCalculator.calculateCBOCostBenefitValues(projectBaseDir); + + if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty()) { + stringBuilder + .append("Congratulations! ") + .append(projectName) + .append(" ") + .append(projectVersion) + .append(" has no God classes or highly coupled classes!"); + renderGithubButtons(stringBuilder); + log.info("Done! No Disharmonies found!"); + stringBuilder.append(THE_END); + writeReportToDisk(outputDirectory, filename, stringBuilder); + return; + } + + if (!rankedGodClassDisharmonies.isEmpty() && !rankedCBODisharmonies.isEmpty()) { + stringBuilder.append("God Classes"); + stringBuilder.append("
"); + stringBuilder.append("Highly Coupled Classes"); + } + + if (!rankedGodClassDisharmonies.isEmpty()) { + int maxGodClassPriority = rankedGodClassDisharmonies + .get(rankedGodClassDisharmonies.size() - 1) + .getPriority(); + + stringBuilder.append(""); + + renderGodClassChart(outputDirectory, rankedGodClassDisharmonies, maxGodClassPriority, stringBuilder); + + stringBuilder.append( + "

God classes by the numbers: (Refactor Starting with Priority 1)

"); + stringBuilder.append(""); + + // Content + stringBuilder.append(""); + for (String heading : godClassTableHeadings) { + stringBuilder.append(""); + } + stringBuilder.append(""); + + stringBuilder.append(""); + for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) { + stringBuilder.append(""); + + String[] simpleRankedGodClassDisharmonyData = { + rankedGodClassDisharmony.getFileName(), + rankedGodClassDisharmony.getPriority().toString(), + rankedGodClassDisharmony.getChangePronenessRank().toString(), + rankedGodClassDisharmony.getEffortRank().toString(), + rankedGodClassDisharmony.getWmc().toString(), + formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), + rankedGodClassDisharmony.getCommitCount().toString() + }; + + String[] detailedRankedGodClassDisharmonyData = { + rankedGodClassDisharmony.getFileName(), + rankedGodClassDisharmony.getPriority().toString(), + rankedGodClassDisharmony.getRawPriority().toString(), + rankedGodClassDisharmony.getChangePronenessRank().toString(), + rankedGodClassDisharmony.getEffortRank().toString(), + rankedGodClassDisharmony.getWmc().toString(), + rankedGodClassDisharmony.getWmcRank().toString(), + rankedGodClassDisharmony.getAtfd().toString(), + rankedGodClassDisharmony.getAtfdRank().toString(), + rankedGodClassDisharmony.getTcc().toString(), + rankedGodClassDisharmony.getTccRank().toString(), + formatter.format(rankedGodClassDisharmony.getFirstCommitTime()), + formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), + rankedGodClassDisharmony.getCommitCount().toString(), + rankedGodClassDisharmony.getPath() + }; + + final String[] rankedDisharmonyData = + showDetails ? detailedRankedGodClassDisharmonyData : simpleRankedGodClassDisharmonyData; + + for (String rowData : rankedDisharmonyData) { + drawTableCell(rowData, stringBuilder); + } + + stringBuilder.append(""); + } + + stringBuilder.append(""); + stringBuilder.append("
").append(heading).append("
"); + } + + if (!rankedCBODisharmonies.isEmpty()) { + + stringBuilder.append("
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
"); + + stringBuilder.append( + ""); + + int maxCboPriority = + rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); + + renderCBOChart(outputDirectory, rankedCBODisharmonies, maxCboPriority, stringBuilder); + + stringBuilder.append( + "

Highly Coupled classes by the numbers: (Refactor starting with Priority 1)

"); + stringBuilder.append(""); + + // Content + stringBuilder.append(""); + for (String heading : cboTableHeadings) { + stringBuilder.append(""); + } + stringBuilder.append(""); + + stringBuilder.append(""); + for (RankedDisharmony rankedCboClassDisharmony : rankedCBODisharmonies) { + stringBuilder.append(""); + + String[] rankedCboClassDisharmonyData = { + rankedCboClassDisharmony.getFileName(), + rankedCboClassDisharmony.getPriority().toString(), + rankedCboClassDisharmony.getChangePronenessRank().toString(), + rankedCboClassDisharmony.getEffortRank().toString(), + formatter.format(rankedCboClassDisharmony.getMostRecentCommitTime()), + rankedCboClassDisharmony.getCommitCount().toString() + }; + + for (String rowData : rankedCboClassDisharmonyData) { + drawTableCell(rowData, stringBuilder); + } + + stringBuilder.append(""); + } + + stringBuilder.append(""); + } + + stringBuilder.append("
").append(heading).append("
"); + stringBuilder.append(THE_END); + + log.debug(stringBuilder.toString()); + + writeReportToDisk(outputDirectory, filename, stringBuilder); + log.info("Done! View the report at target/site/{}", filename); + } + + void drawTableCell(String rowData, StringBuilder stringBuilder) { + if (isNumber(rowData) || isDateTime(rowData)) { + stringBuilder.append("").append(rowData).append(""); + } else { + stringBuilder.append("").append(rowData).append(""); + } + } + + boolean isNumber(String rowData) { + return rowData.matches("-?\\d+(\\.\\d+)?"); + } + + boolean isDateTime(String rowData) { + return rowData.contains(", "); + } + + public void printTitle(String projectName, String projectVersion, StringBuilder stringBuilder) { + // empty on purpose + } + + public void printHead(StringBuilder stringBuilder) { + // empty on purpose + } + + public void printBreadcrumbs(StringBuilder stringBuilder) { + stringBuilder.append(" \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
"); + } + + public void printProjectHeader( + String projectName, String projectVersion, StringBuilder stringBuilder, DateTimeFormatter formatter) { + stringBuilder + .append("Last Published: ") + .append(formatter.format(Instant.now())) + .append(""); + stringBuilder + .append(" Version: ") + .append(projectVersion) + .append(""); + + stringBuilder.append("
\n" + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
"); + + stringBuilder + .append("
\n" + "

RefactorFirst Report for ") + .append(projectName) + .append(" ") + .append(projectVersion) + .append("

\n"); + } + + void renderGithubButtons(StringBuilder stringBuilder) { + // empty on purpose + } + + String getOutputName() { + // This report will generate simple-report.html when invoked in a project with `mvn site` + return "refactor-first-report"; + } + + void renderGodClassChart( + String outputDirectory, + List rankedGodClassDisharmonies, + int maxGodClassPriority, + StringBuilder stringBuilder) { + // empty on purpose + } + + void writeGodClassGchartJs( + List rankedDisharmonies, int maxPriority, String reportOutputDirectory) { + // empty on purpose + } + + void writeGCBOGchartJs(List rankedDisharmonies, int maxPriority, String reportOutputDirectory) { + // empty on purpose + } + + void renderCBOChart( + String outputDirectory, + List rankedCBODisharmonies, + int maxCboPriority, + StringBuilder stringBuilder) { + // empty on purpose + } +} diff --git a/report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java b/report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java new file mode 100644 index 00000000..72c7db4a --- /dev/null +++ b/report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java @@ -0,0 +1,14 @@ +package org.hjug.refactorfirst.report; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SimpleHtmlReportTest { + + @Test + void isDateTime() { + HtmlReport htmlReport = new HtmlReport(); + String commitDateTime = "7/22/23, 5:00 AM"; + Assertions.assertTrue(htmlReport.isDateTime(commitDateTime)); + } +} From 29aca7b91eb6fdcaa001b2e432531d337e9a447e Mon Sep 17 00:00:00 2001 From: Jim Bethancourt Date: Sat, 29 Jun 2024 14:51:38 -0500 Subject: [PATCH 3/3] Attempting to publish to SNAPSHOT repository --- .github/workflows/maven.yml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) 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