| /* |
| * 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.felix.bundleplugin; |
| |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOError; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.Writer; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.jar.Manifest; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| import java.util.zip.ZipFile; |
| |
| import aQute.bnd.osgi.Analyzer; |
| import aQute.bnd.osgi.Builder; |
| import aQute.bnd.osgi.Constants; |
| import aQute.bnd.osgi.Instructions; |
| import aQute.bnd.osgi.Jar; |
| import aQute.bnd.osgi.Resource; |
| import aQute.lib.collections.ExtList; |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| import org.apache.maven.plugin.logging.Log; |
| import org.apache.maven.plugins.annotations.Component; |
| 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.codehaus.plexus.util.Scanner; |
| import org.osgi.service.metatype.MetaTypeService; |
| import org.sonatype.plexus.build.incremental.BuildContext; |
| |
| import static aQute.lib.strings.Strings.join; |
| |
| |
| /** |
| * Generate an OSGi manifest for this project |
| */ |
| @Mojo( name = "manifest", requiresDependencyResolution = ResolutionScope.TEST, |
| threadSafe = true, |
| defaultPhase = LifecyclePhase.PROCESS_CLASSES) |
| public class ManifestPlugin extends BundlePlugin |
| { |
| /** |
| * When true, generate the manifest by rebuilding the full bundle in memory |
| */ |
| @Parameter( property = "rebuildBundle" ) |
| protected boolean rebuildBundle; |
| |
| /** |
| * When true, manifest generation on incremental builds is supported in IDEs like Eclipse. |
| * Please note that the underlying BND library does not support incremental build, which means |
| * always the whole manifest and SCR metadata is generated. |
| */ |
| @Parameter( property = "supportIncrementalBuild" ) |
| private boolean supportIncrementalBuild; |
| |
| /** |
| * When true, show stale files in the log at info level else at debug level. |
| */ |
| @Parameter( property = "showStaleFiles" ) |
| private boolean showStaleFiles; |
| |
| @Component |
| private BuildContext buildContext; |
| |
| @Override |
| protected void execute( Map<String, String> instructions, ClassPathItem[] classpath ) |
| throws MojoExecutionException |
| { |
| File outputFile = new File( manifestLocation, "MANIFEST.MF" ); |
| boolean metadataUpToDate = isMetadataUpToDate(outputFile, project); |
| |
| if (supportIncrementalBuild && metadataUpToDate && isUpToDate(project)) { |
| return; |
| } |
| // in incremental build execute manifest generation only when explicitly activated |
| // and when any java file was touched since last build |
| if (buildContext.isIncremental() && (!supportIncrementalBuild // |
| || (metadataUpToDate && !anyJavaSourceFileTouchedSinceLastBuild()))) |
| { |
| getLog().debug("Skipping manifest generation because no java source file was added, updated or removed since last build."); |
| return; |
| } |
| |
| Analyzer analyzer; |
| try |
| { |
| analyzer = getAnalyzer(project, instructions, classpath); |
| } |
| catch ( FileNotFoundException e ) |
| { |
| throw new MojoExecutionException( "Cannot find " + e.getMessage() |
| + " (manifest goal must be run after compile phase)", e ); |
| } |
| catch ( IOException e ) |
| { |
| throw new MojoExecutionException( "Error trying to generate Manifest", e ); |
| } |
| catch ( MojoFailureException e ) |
| { |
| getLog().error( e.getLocalizedMessage() ); |
| throw new MojoExecutionException( "Error(s) found in manifest configuration", e ); |
| } |
| catch ( Exception e ) |
| { |
| getLog().error( "An internal error occurred", e ); |
| throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e ); |
| } |
| |
| try |
| { |
| writeManifest( analyzer, outputFile, niceManifest, exportScr, scrLocation, buildContext, getLog() ); |
| |
| if (supportIncrementalBuild) { |
| writeIncrementalInfo(project); |
| } |
| } |
| catch ( Exception e ) |
| { |
| throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e ); |
| } |
| finally |
| { |
| try |
| { |
| analyzer.close(); |
| } |
| catch ( IOException e ) |
| { |
| throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e ); |
| } |
| } |
| } |
| |
| /** |
| * Checks if any *.java file was added, updated or removed since last build in any source directory. |
| */ |
| private boolean anyJavaSourceFileTouchedSinceLastBuild() { |
| @SuppressWarnings("unchecked") |
| List<String> sourceDirectories = project.getCompileSourceRoots(); |
| for (String sourceDirectory : sourceDirectories) { |
| File directory = new File(sourceDirectory); |
| Scanner scanner = buildContext.newScanner(directory); |
| Scanner deleteScanner = buildContext.newDeleteScanner(directory); |
| if (containsJavaFile(scanner) || containsJavaFile(deleteScanner)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| private boolean containsJavaFile(Scanner scanner) { |
| String[] includes = new String[] { "**/*.java" }; |
| scanner.setIncludes(includes); |
| scanner.scan(); |
| return scanner.getIncludedFiles().length > 0; |
| } |
| |
| public Manifest getManifest( MavenProject project, ClassPathItem[] classpath ) throws IOException, MojoFailureException, |
| MojoExecutionException, Exception |
| { |
| return getManifest( project, new LinkedHashMap<String, String>(), classpath, buildContext ); |
| } |
| |
| |
| public Manifest getManifest( MavenProject project, Map<String, String> instructions, ClassPathItem[] classpath, |
| BuildContext buildContext ) throws IOException, MojoFailureException, MojoExecutionException, Exception |
| { |
| Analyzer analyzer = getAnalyzer(project, instructions, classpath); |
| |
| Jar jar = analyzer.getJar(); |
| Manifest manifest = jar.getManifest(); |
| |
| if (exportScr) |
| { |
| exportScr(analyzer, jar, scrLocation, buildContext, getLog() ); |
| } |
| |
| // cleanup... |
| analyzer.close(); |
| |
| return manifest; |
| } |
| |
| private static void exportScr(Analyzer analyzer, Jar jar, File scrLocation, BuildContext buildContext, Log log ) throws Exception { |
| log.debug("Export SCR metadata to: " + scrLocation.getPath()); |
| scrLocation.mkdirs(); |
| |
| // export SCR metadata files from OSGI-INF/ |
| Map<String, Resource> scrDir = jar.getDirectories().get("OSGI-INF"); |
| if (scrDir != null) { |
| for (Map.Entry<String, Resource> entry : scrDir.entrySet()) { |
| String path = entry.getKey(); |
| Resource resource = entry.getValue(); |
| writeSCR(resource, new File(scrLocation, path), buildContext, |
| log); |
| } |
| } |
| |
| // export metatype files from OSGI-INF/metatype |
| Map<String,Resource> metatypeDir = jar.getDirectories().get(MetaTypeService.METATYPE_DOCUMENTS_LOCATION); |
| if (metatypeDir != null) { |
| for (Map.Entry<String, Resource> entry : metatypeDir.entrySet()) |
| { |
| String path = entry.getKey(); |
| Resource resource = entry.getValue(); |
| writeSCR(resource, new File(scrLocation, path), buildContext, log); |
| } |
| } |
| |
| } |
| |
| private static void writeSCR(Resource resource, File destination, BuildContext buildContext, Log log ) throws Exception |
| { |
| log.debug("Write SCR file: " + destination.getPath()); |
| destination.getParentFile().mkdirs(); |
| OutputStream os = buildContext.newFileOutputStream(destination); |
| try |
| { |
| resource.write(os); |
| } |
| finally |
| { |
| os.close(); |
| } |
| } |
| |
| protected Analyzer getAnalyzer( MavenProject project, ClassPathItem[] classpath ) throws IOException, MojoExecutionException, |
| Exception |
| { |
| return getAnalyzer( project, new LinkedHashMap<>(), classpath ); |
| } |
| |
| |
| protected Analyzer getAnalyzer( MavenProject project, Map<String, String> instructions, ClassPathItem[] classpath ) |
| throws IOException, MojoExecutionException, Exception |
| { |
| if ( rebuildBundle && supportedProjectTypes.contains( project.getArtifact().getType() ) ) |
| { |
| return buildOSGiBundle( project, instructions, classpath ); |
| } |
| |
| File file = getOutputDirectory(); |
| if ( file == null ) |
| { |
| file = project.getArtifact().getFile(); |
| } |
| |
| if ( !file.exists() ) |
| { |
| if ( file.equals( getOutputDirectory() ) ) |
| { |
| file.mkdirs(); |
| } |
| else |
| { |
| throw new FileNotFoundException( file.getPath() ); |
| } |
| } |
| |
| Builder analyzer = getOSGiBuilder( project, instructions, classpath ); |
| |
| analyzer.setJar( file ); |
| |
| // calculateExportsFromContents when we have no explicit instructions defining |
| // the contents of the bundle *and* we are not analyzing the output directory, |
| // otherwise fall-back to addMavenInstructions approach |
| |
| boolean isOutputDirectory = file.equals( getOutputDirectory() ); |
| |
| if ( analyzer.getProperty( Analyzer.EXPORT_PACKAGE ) == null |
| && analyzer.getProperty( Analyzer.EXPORT_CONTENTS ) == null |
| && analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) == null && !isOutputDirectory ) |
| { |
| String export = calculateExportsFromContents( analyzer.getJar() ); |
| analyzer.setProperty( Analyzer.EXPORT_PACKAGE, export ); |
| } |
| |
| addMavenInstructions( project, analyzer ); |
| |
| // if we spot Embed-Dependency and the bundle is "target/classes", assume we need to rebuild |
| if ( analyzer.getProperty( DependencyEmbedder.EMBED_DEPENDENCY ) != null && isOutputDirectory ) |
| { |
| analyzer.build(); |
| } |
| else |
| { |
| // FELIX-6495: workaround BND inconsistency: internal jar does not take "-reproducible" flag into account... |
| analyzer.getJar().setReproducible( "true".equals( analyzer.getProperties().getProperty( Constants.REPRODUCIBLE ) ) ); |
| |
| analyzer.mergeManifest( analyzer.getJar().getManifest() ); |
| analyzer.getJar().setManifest( analyzer.calcManifest() ); |
| } |
| |
| mergeMavenManifest( project, analyzer ); |
| |
| boolean hasErrors = reportErrors( "Manifest " + project.getArtifact(), analyzer ); |
| if ( hasErrors ) |
| { |
| String failok = analyzer.getProperty( "-failok" ); |
| if ( null == failok || "false".equalsIgnoreCase( failok ) ) |
| { |
| throw new MojoFailureException( "Error(s) found in manifest configuration" ); |
| } |
| } |
| |
| Jar jar = analyzer.getJar(); |
| |
| if ( unpackBundle ) |
| { |
| File outputFile = getOutputDirectory(); |
| for ( Entry<String, Resource> entry : jar.getResources().entrySet() ) |
| { |
| File entryFile = new File( outputFile, entry.getKey() ); |
| if ( !entryFile.exists() || entry.getValue().lastModified() == 0 ) |
| { |
| entryFile.getParentFile().mkdirs(); |
| OutputStream os = buildContext.newFileOutputStream( entryFile ); |
| entry.getValue().write( os ); |
| os.close(); |
| } |
| } |
| } |
| |
| return analyzer; |
| } |
| |
| private void writeIncrementalInfo(MavenProject project) throws MojoExecutionException { |
| try { |
| Path cacheData = getIncrementalDataPath(project); |
| String curdata = getIncrementalData(); |
| Files.createDirectories(cacheData.getParent()); |
| try (Writer w = Files.newBufferedWriter(cacheData)) { |
| w.append(curdata); |
| } |
| } catch (IOException e) { |
| throw getManifestUptodateCheckException(e); |
| } |
| } |
| |
| private boolean isUpToDate(MavenProject project) throws MojoExecutionException { |
| try { |
| Path cacheData = getIncrementalDataPath(project); |
| String prvdata; |
| if (Files.isRegularFile(cacheData)) { |
| prvdata = new String(Files.readAllBytes(cacheData), StandardCharsets.UTF_8); |
| } else { |
| prvdata = null; |
| } |
| String curdata = getIncrementalData(); |
| if (curdata.equals(prvdata)) { |
| long lastmod = lastModified(cacheData); |
| Set<String> stale = Stream.concat(Stream.of(new File(project.getBuild().getOutputDirectory())), |
| project.getArtifacts().stream().map(Artifact::getFile)) |
| .flatMap(f -> newer(lastmod, f)) |
| .collect(Collectors.toSet()); |
| if (!stale.isEmpty()) { |
| getLog().info("Stale files detected, re-generating manifest."); |
| if (showStaleFiles) { |
| getLog().info("Stale files: " + join(", ", stale)); |
| } else if (getLog().isDebugEnabled()) { |
| getLog().debug("Stale files: " + join(", ", stale)); |
| } |
| } else { |
| // everything is in order, skip |
| getLog().info("Skipping manifest generation, everything is up to date."); |
| return true; |
| } |
| } else { |
| if (prvdata == null) { |
| getLog().info("No previous run data found, generating manifest."); |
| } else { |
| getLog().info("Configuration changed, re-generating manifest."); |
| } |
| } |
| } catch (IOException e) { |
| throw getManifestUptodateCheckException(e); |
| } |
| return false; |
| } |
| |
| private boolean isMetadataUpToDate(File outputFile, MavenProject project) |
| throws MojoExecutionException |
| { |
| if (!outputFile.isFile()) // does MANIFEST.MF exist? |
| { |
| getLog().info("No MANIFEST.MF file found, generating manifest."); |
| return false; |
| } |
| try |
| { // has this project's or a parent's pom.xml been modified after the manifest was |
| // generated last? |
| Path cacheData = getIncrementalDataPath(project); |
| if(!Files.isRegularFile(cacheData)) { |
| getLog().debug("No cache data file found at '" + cacheData + "', generating manifest."); |
| return false; |
| } |
| long manifestLastModified = lastModified(cacheData); |
| while (project != null) |
| { |
| if (project.getFile() != null) { |
| Path pom = project.getFile().toPath(); |
| if (manifestLastModified < lastModified(pom)) |
| { |
| getLog().debug("POM file at '" + pom + "' newer than cache data file, generating manifest."); |
| return false; |
| } |
| } else { |
| if (project.getVersion().endsWith("-SNAPSHOT")) { // is it mutable? |
| getLog().debug("POM file not accessible for SNAPSHOT project '" + project + "', assume modification."); |
| return false; |
| } else { |
| getLog().debug("POM file not accessible for non-SNAPSHOT project '" + project + "', assume no modification."); |
| } |
| } |
| project = project.getParent(); |
| } |
| } |
| catch (IOException e) |
| { |
| throw getManifestUptodateCheckException(e); |
| } |
| return true; |
| } |
| |
| private static MojoExecutionException getManifestUptodateCheckException(IOException e) |
| { |
| return new MojoExecutionException("Error checking manifest uptodate status", e); |
| } |
| |
| private String getIncrementalData() { |
| return getInstructions().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()) |
| .collect(Collectors.joining("\n", "", "\n")); |
| } |
| |
| private Path getIncrementalDataPath(MavenProject project) { |
| return Paths.get(project.getBuild().getDirectory(), "maven-bundle-plugin", |
| "org.apache.felix_maven-bundle-plugin_manifest_xx"); |
| } |
| |
| private long lastmod(Path p) { |
| try { |
| return lastModified(p); |
| } catch (IOException e) { |
| return 0; |
| } |
| } |
| |
| private static long lastModified(Path p) throws IOException |
| { |
| return Files.getLastModifiedTime(p).toMillis(); |
| } |
| |
| private Stream<String> newer(long lastmod, File file) { |
| try { |
| if (file.isDirectory()) { |
| return Files.walk(file.toPath()) |
| .filter(Files::isRegularFile) |
| .filter(p -> lastmod(p) > lastmod) |
| .map(Path::toString); |
| } else if (file.isFile()) { |
| if (lastmod(file.toPath()) > lastmod) { |
| if (file.getName().endsWith(".jar")) { |
| try (ZipFile zf = new ZipFile(file)) { |
| return zf.stream() |
| .filter(ze -> !ze.isDirectory()) |
| .filter(ze -> ze.getLastModifiedTime().toMillis() > lastmod) |
| .map(ze -> file.toString() + "!" + ze.getName()) |
| .collect(Collectors.toList()) |
| .stream(); |
| } |
| } else { |
| return Stream.of(file.toString()); |
| } |
| } else { |
| return Stream.empty(); |
| } |
| } else { |
| return Stream.empty(); |
| } |
| } catch (IOException e) { |
| throw new IOError(e); |
| } |
| } |
| |
| |
| public static void writeManifest( Analyzer analyzer, File outputFile, boolean niceManifest, |
| boolean exportScr, File scrLocation, BuildContext buildContext, Log log ) throws Exception |
| { |
| Properties properties = analyzer.getProperties(); |
| Jar jar = analyzer.getJar(); |
| Manifest manifest = jar.getManifest(); |
| if ( outputFile.exists() && properties.containsKey( "Merge-Headers" ) ) |
| { |
| Manifest analyzerManifest = manifest; |
| manifest = new Manifest(); |
| try( InputStream inputStream = new FileInputStream( outputFile ) ) |
| { |
| manifest.read( inputStream ); |
| } |
| Instructions instructions = new Instructions( ExtList.from( analyzer.getProperty("Merge-Headers") ) ); |
| mergeManifest( instructions, manifest, analyzerManifest ); |
| } |
| else |
| { |
| File parentFile = outputFile.getParentFile(); |
| parentFile.mkdirs(); |
| } |
| writeManifest( manifest, outputFile, niceManifest, buildContext, log ); |
| |
| if (exportScr) |
| { |
| exportScr(analyzer, jar, scrLocation, buildContext, log); |
| } |
| } |
| |
| |
| public static void writeManifest( Manifest manifest, File outputFile, boolean niceManifest, |
| BuildContext buildContext, Log log ) throws IOException |
| { |
| log.info("Writing manifest: " + outputFile.getPath()); |
| outputFile.getParentFile().mkdirs(); |
| |
| try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) |
| { |
| ManifestWriter.outputManifest( manifest, baos, niceManifest ); |
| baos.flush(); |
| byte[] newdata = baos.toByteArray(); |
| byte[] curdata = new byte[0]; |
| if (outputFile.exists()) |
| { |
| curdata = Files.readAllBytes( outputFile.toPath() ); |
| } |
| if ( !Arrays.equals( newdata, curdata ) ) |
| { |
| try ( OutputStream os = buildContext.newFileOutputStream( outputFile ) ) |
| { |
| os.write( newdata ); |
| } |
| } |
| } |
| } |
| |
| |
| /* |
| * Patched version of bnd's Analyzer.calculateExportsFromContents |
| */ |
| public static String calculateExportsFromContents( Jar bundle ) |
| { |
| String ddel = ""; |
| StringBuffer sb = new StringBuffer(); |
| Map<String, Map<String, Resource>> map = bundle.getDirectories(); |
| for ( Iterator<Entry<String, Map<String, Resource>>> i = map.entrySet().iterator(); i.hasNext(); ) |
| { |
| //---------------------------------------------------- |
| // should also ignore directories with no resources |
| //---------------------------------------------------- |
| Entry<String, Map<String, Resource>> entry = i.next(); |
| if ( entry.getValue() == null || entry.getValue().isEmpty() ) |
| continue; |
| //---------------------------------------------------- |
| String directory = entry.getKey(); |
| if ( directory.equals( "META-INF" ) || directory.startsWith( "META-INF/" ) ) |
| continue; |
| if ( directory.equals( "OSGI-OPT" ) || directory.startsWith( "OSGI-OPT/" ) ) |
| continue; |
| if ( directory.equals( "/" ) ) |
| continue; |
| |
| if ( directory.endsWith( "/" ) ) |
| directory = directory.substring( 0, directory.length() - 1 ); |
| |
| directory = directory.replace( '/', '.' ); |
| sb.append( ddel ); |
| sb.append( directory ); |
| ddel = ","; |
| } |
| return sb.toString(); |
| } |
| } |