| /* |
| * 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.hyracks.maven.license; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.io.StringWriter; |
| import java.io.Writer; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedMap; |
| import java.util.SortedSet; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.function.BiConsumer; |
| import java.util.function.Predicate; |
| import java.util.function.UnaryOperator; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.regex.Pattern; |
| |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import com.fasterxml.jackson.databind.SequenceWriter; |
| import freemarker.cache.FileTemplateLoader; |
| import freemarker.template.Configuration; |
| import freemarker.template.Template; |
| import freemarker.template.TemplateException; |
| import org.apache.commons.io.IOUtils; |
| import org.apache.hyracks.maven.license.freemarker.IndentDirective; |
| import org.apache.hyracks.maven.license.freemarker.LoadFileDirective; |
| import org.apache.hyracks.maven.license.project.LicensedProjects; |
| import org.apache.hyracks.maven.license.project.Project; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| 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.ProjectBuildingException; |
| |
| @Mojo(name = "generate", |
| requiresProject = true, |
| requiresDependencyResolution = ResolutionScope.TEST) |
| public class GenerateFileMojo extends LicenseMojo { |
| |
| public static final Pattern FOUNDATION_PATTERN = Pattern.compile("^\\s*This product includes software developed " + |
| "(at|by) The Apache Software Foundation \\(http://www.apache.org/\\).\\s*$".replace(" ", "\\s+"), |
| Pattern.DOTALL | Pattern.MULTILINE); |
| |
| public static final Comparator<String> WHITESPACE_NORMALIZED_COMPARATOR = |
| (o1, o2) -> o1.replaceAll("\\s+", " ").compareTo(o2.replaceAll("\\s+", " ")); |
| |
| @Parameter(required = true) |
| private File templateRootDir; |
| |
| @Parameter(defaultValue = "${project.build.directory}/generated-sources") |
| private File outputDir; |
| |
| @Parameter |
| private List<GeneratedFile> generatedFiles = new ArrayList<>(); |
| |
| @Parameter(defaultValue = "${project.build.sourceEncoding}") |
| private String encoding; |
| |
| @Parameter |
| private File licenseMapOutputFile; |
| |
| @Parameter |
| private List<ExtraLicenseFile> extraLicenseMaps = new ArrayList<>(); |
| |
| @Parameter |
| protected Map<String, String> templateProperties = new HashMap<>(); |
| |
| @Parameter |
| private boolean stripFoundationAssertionFromNotices = true; |
| |
| private SortedMap<String, SortedSet<Project>> noticeMap; |
| |
| @java.lang.Override |
| public void execute() throws MojoExecutionException, MojoFailureException { |
| try { |
| init(); |
| readExtraMaps(); |
| addDependenciesToLicenseMap(); |
| resolveLicenseContent(); |
| resolveNoticeFiles(); |
| resolveLicenseFiles(); |
| rebuildLicenseContentProjectMap(); |
| combineCommonGavs(); |
| SourcePointerResolver.execute(this); |
| persistLicenseMap(); |
| buildNoticeProjectMap(); |
| generateFiles(); |
| } catch (IOException | TemplateException | ProjectBuildingException e) { |
| throw new MojoExecutionException("Unexpected exception: " + e, e); |
| } |
| } |
| |
| |
| private void resolveLicenseContent() throws IOException { |
| Set<LicenseSpec> licenseSpecs = new HashSet<>(); |
| for (LicensedProjects licensedProjects : licenseMap.values()) { |
| licenseSpecs.add(licensedProjects.getLicense()); |
| } |
| licenseSpecs.addAll(urlToLicenseMap.values()); |
| for (LicenseSpec license : licenseSpecs) { |
| resolveLicenseContent(license, true); |
| } |
| } |
| |
| private String resolveLicenseContent(LicenseSpec license, boolean bestEffort) throws IOException { |
| if (license.getContent() == null) { |
| getLog().debug("Resolving content for " + license.getUrl() + " (" + license.getContentFile() + ")"); |
| File cFile = new File(license.getContentFile()); |
| if (!cFile.isAbsolute()) { |
| cFile = new File(licenseDirectory, license.getContentFile()); |
| } |
| if (!cFile.exists()) { |
| if (!bestEffort) { |
| getLog().warn("MISSING: license content file (" + cFile + ") for url: " + license.getUrl()); |
| license.setContent("MISSING: " + license.getContentFile() + " (" + license.getUrl() + ")"); |
| } |
| } else { |
| getLog().info("Reading license content from file: " + cFile); |
| StringWriter sw = new StringWriter(); |
| LicenseUtil.readAndTrim(sw, cFile); |
| license.setContent(sw.toString()); |
| } |
| } |
| return license.getContent(); |
| } |
| |
| private void combineCommonGavs() { |
| for (LicensedProjects licensedProjects : licenseMap.values()) { |
| Map<String, Project> projectMap = new HashMap<>(); |
| for (Iterator<Project> iter = licensedProjects.getProjects().iterator(); iter.hasNext(); ) { |
| Project project = iter.next(); |
| if (projectMap.containsKey(project.gav())) { |
| Project first = projectMap.get(project.gav()); |
| first.setLocation(first.getLocation() + "," + project.getLocation()); |
| iter.remove(); |
| } else { |
| projectMap.put(project.gav(), project); |
| } |
| } |
| } |
| } |
| |
| private void generateFiles() throws TemplateException, IOException { |
| Map<String, Object> props = getProperties(); |
| |
| Configuration config = new Configuration(); |
| config.setTemplateLoader(new FileTemplateLoader(templateRootDir)); |
| for (GeneratedFile generation : generatedFiles) { |
| Template template = config.getTemplate(generation.getTemplate(), StandardCharsets.UTF_8.name()); |
| |
| if (template == null) { |
| throw new IOException("Could not load template " + generation.getTemplate()); |
| } |
| |
| outputDir.mkdirs(); |
| final File file = new File(outputDir, generation.getOutputFile()); |
| getLog().info("Writing " + file + "..."); |
| try (final FileOutputStream fos = new FileOutputStream(file); |
| final Writer writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { |
| template.process(props, writer); |
| } |
| } |
| } |
| |
| protected Map<String, Object> getProperties() { |
| Map<String, Object> props = new HashMap<>(); |
| props.put("indent", new IndentDirective()); |
| props.put("loadfile", new LoadFileDirective()); |
| props.put("project", project); |
| props.put("noticeMap", noticeMap.entrySet()); |
| props.put("licenseMap", licenseMap.entrySet()); |
| props.put("licenses", urlToLicenseMap.values()); |
| props.putAll(templateProperties); |
| return props; |
| } |
| |
| private void readExtraMaps() throws IOException { |
| final ObjectMapper objectMapper = new ObjectMapper(); |
| for (ExtraLicenseFile extraLicenseFile : extraLicenseMaps) { |
| for (LicensedProjects projects : |
| objectMapper.readValue(extraLicenseFile.getFile(), LicensedProjects[].class)) { |
| LicenseSpec spec = urlToLicenseMap.get(projects.getLicense().getUrl()); |
| if (spec != null) { |
| // TODO(mblow): probably we should always favor the extra map... |
| // propagate any license content we may have with what already has been loaded |
| if (projects.getLicense().getContent() != null && |
| spec.getContent() == null) { |
| spec.setContent(projects.getLicense().getContent()); |
| } |
| // propagate any license displayName we may have with what already has been loaded |
| if (projects.getLicense().getDisplayName() != null && |
| spec.getDisplayName() == null) { |
| spec.setDisplayName(projects.getLicense().getDisplayName()); |
| } |
| } |
| for (Project p : projects.getProjects()) { |
| p.setLocation(extraLicenseFile.getLocation()); |
| addProject(p, projects.getLicense(), extraLicenseFile.isAdditive()); |
| } |
| } |
| } |
| } |
| |
| private void persistLicenseMap() throws IOException { |
| if (licenseMapOutputFile != null) { |
| licenseMapOutputFile.getParentFile().mkdirs(); |
| SequenceWriter sw = new ObjectMapper().writerWithDefaultPrettyPrinter() |
| .writeValues(licenseMapOutputFile).init(true); |
| for (LicensedProjects entry : licenseMap.values()) { |
| sw.write(entry); |
| } |
| sw.close(); |
| } |
| } |
| |
| private void rebuildLicenseContentProjectMap() throws IOException { |
| int counter = 0; |
| Map<String, LicensedProjects> licenseMap2 = new TreeMap<>(WHITESPACE_NORMALIZED_COMPARATOR); |
| for (LicensedProjects lps : licenseMap.values()) { |
| for (Project p : lps.getProjects()) { |
| String licenseText = p.getLicenseText(); |
| if (licenseText == null) { |
| getLog().warn("Using license other than from within artifact: " + p.gav()); |
| licenseText = resolveLicenseContent(lps.getLicense(), false); |
| } |
| LicenseSpec spec = lps.getLicense(); |
| if (spec.getDisplayName() == null) { |
| LicenseSpec canonicalLicense = urlToLicenseMap.get(spec.getUrl()); |
| if (canonicalLicense != null) { |
| spec.setDisplayName(canonicalLicense.getDisplayName()); |
| } |
| } |
| if (!licenseMap2.containsKey(licenseText)) { |
| if (!licenseText.equals(lps.getLicense().getContent())) { |
| spec = new LicenseSpec(new ArrayList<>(), licenseText, null, spec.getDisplayName(), |
| spec.getMetric(), spec.getUrl() + (counter++)); |
| } |
| licenseMap2.put(licenseText, new LicensedProjects(spec)); |
| } |
| final LicensedProjects lp2 = licenseMap2.get(licenseText); |
| if (lp2.getLicense().getDisplayName() == null) { |
| lp2.getLicense().setDisplayName(lps.getLicense().getDisplayName()); |
| } |
| lp2.addProject(p); |
| } |
| } |
| licenseMap = licenseMap2; |
| } |
| |
| private Set<Project> getProjects() { |
| Set<Project> projects = new HashSet<>(); |
| licenseMap.values().forEach(p -> projects.addAll(p.getProjects())); |
| return projects; |
| } |
| |
| private void buildNoticeProjectMap() { |
| noticeMap = new TreeMap<>(WHITESPACE_NORMALIZED_COMPARATOR); |
| for (Project p : getProjects()) { |
| prependSourcePointerToNotice(p); |
| final String noticeText = p.getNoticeText(); |
| if (noticeText == null) { |
| continue; |
| } |
| if (!noticeMap.containsKey(noticeText)) { |
| noticeMap.put(noticeText, new TreeSet<>(Project.PROJECT_COMPARATOR)); |
| } |
| noticeMap.get(noticeText).add(p); |
| } |
| } |
| |
| private void prependSourcePointerToNotice(Project project) { |
| if (project.getSourcePointer() != null) { |
| String notice = project.getSourcePointer().replace("\n", "\n "); |
| if (project.getNoticeText() != null) { |
| notice += "\n\n" + project.getNoticeText(); |
| } |
| project.setNoticeText(notice); |
| } |
| } |
| |
| private void resolveNoticeFiles() throws MojoExecutionException, IOException { |
| resolveArtifactFiles("NOTICE", entry -> entry.getName().matches("(.*/|^)" + "NOTICE" + "(.txt)?"), |
| Project::setNoticeText, |
| text -> stripFoundationAssertionFromNotices ? FOUNDATION_PATTERN.matcher(text).replaceAll("") : text); |
| } |
| |
| private void resolveLicenseFiles() throws MojoExecutionException, IOException { |
| resolveArtifactFiles("LICENSE", entry -> entry.getName().matches("(.*/|^)" + "LICENSE" + "(.txt)?"), |
| Project::setLicenseText, UnaryOperator.identity()); |
| } |
| |
| private void resolveArtifactFiles(final String name, Predicate<JarEntry> filter, |
| BiConsumer<Project, String> consumer, UnaryOperator<String> contentTransformer) |
| throws MojoExecutionException, IOException { |
| for (Project p : getProjects()) { |
| File artifactFile = new File(p.getArtifactPath()); |
| if (!artifactFile.exists()) { |
| throw new MojoExecutionException("Artifact file " + artifactFile + " does not exist!"); |
| } else if (!artifactFile.getName().endsWith(".jar")) { |
| getLog().info("Skipping unknown artifact file type: " + artifactFile); |
| continue; |
| } |
| try (JarFile jarFile = new JarFile(artifactFile)) { |
| SortedMap<String, JarEntry> matches = gatherMatchingEntries(jarFile, |
| filter); |
| if (matches.isEmpty()) { |
| getLog().warn("No " + name + " file found for " + p.gav()); |
| } else { |
| if (matches.size() > 1) { |
| getLog().warn("Multiple " + name + " files found for " + p.gav() + ": " + matches.keySet() |
| + "; taking first"); |
| } else { |
| getLog().info(p.gav() + " has " + name + " file: " + matches.keySet()); |
| } |
| resolveContent(p, jarFile, matches.values().iterator().next(), |
| contentTransformer, consumer, name); |
| } |
| } |
| } |
| } |
| |
| private void resolveContent(Project project, JarFile jarFile, JarEntry entry, UnaryOperator<String> transformer, |
| BiConsumer<Project, String> contentConsumer, final String name) throws IOException { |
| String text = IOUtils.toString(jarFile.getInputStream(entry), StandardCharsets.UTF_8); |
| text = transformer.apply(text); |
| text = LicenseUtil.trim(text); |
| if (text.length() == 0) { |
| getLog().warn("Ignoring empty " + name + " file ( " + entry + ") for " + project.gav()); |
| } else { |
| contentConsumer.accept(project, text); |
| getLog().debug("Resolved " + name + " text for " + project.gav() + ": \n" + text); |
| } |
| } |
| |
| private SortedMap<String, JarEntry> gatherMatchingEntries(JarFile jarFile, Predicate<JarEntry> filter) { |
| SortedMap<String, JarEntry> matches = new TreeMap<>(); |
| Enumeration<JarEntry> entries = jarFile.entries(); |
| while (entries.hasMoreElements()) { |
| JarEntry entry = entries.nextElement(); |
| if (filter.test(entry)) { |
| matches.put(entry.getName(), entry); |
| } |
| } |
| return matches; |
| } |
| } |
| |