Java tutorial
package org.ops4j.pax.construct.lifecycle; /* * Copyright 2007 Stuart McCulloch * * Licensed 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. */ import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.regex.Pattern; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.eclipse.EclipsePlugin; import org.apache.maven.plugin.eclipse.EclipseSourceDir; import org.apache.maven.plugin.eclipse.writers.EclipseClasspathWriter; import org.apache.maven.plugin.eclipse.writers.EclipseProjectWriter; import org.apache.maven.plugin.eclipse.writers.EclipseSettingsWriter; import org.apache.maven.plugin.eclipse.writers.EclipseWriterConfig; import org.apache.maven.plugin.ide.IdeDependency; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectBuilder; import org.apache.maven.project.ProjectBuildingException; import org.apache.maven.project.artifact.InvalidDependencyVersionException; import org.apache.maven.project.path.PathTranslator; import org.apache.maven.shared.osgi.Maven2OsgiConverter; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.codehaus.plexus.util.xml.Xpp3DomBuilder; import org.codehaus.plexus.util.xml.Xpp3DomWriter; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.ops4j.pax.construct.util.DirUtils; import org.ops4j.pax.construct.util.DirUtils.EntryFilter; import org.ops4j.pax.construct.util.PomUtils; import org.ops4j.pax.construct.util.ReflectMojo; import org.ops4j.pax.construct.util.StreamFactory; /** * Extends <a href="http://maven.apache.org/plugins/maven-eclipse-plugin/eclipse-mojo.html">EclipsePlugin</a> to * provide customized Eclipse project files for Pax-Construct projects.<br/>Inherited parameters can still be used, but * unfortunately don't appear in the generated docs. * * @extendsPlugin eclipse * @goal eclipse * @phase package * * @execute phase="none" */ public class EclipseOSGiMojo extends EclipsePlugin { /** * Component factory for Maven projects * * @component */ private MavenProjectBuilder m_mavenProjectBuilder; /** * Component to convert between Maven and OSGi versions * * @component */ private Maven2OsgiConverter m_maven2OsgiConverter; /** * Project path translation (needed for backwards compatibility) * * @component */ private PathTranslator m_pathTranslator; /** * Switch to turn on/off fixing of dependency entries in the classpath file. * * @parameter expression="${fixDependencies}" default-value="EXTERNAL" */ private String fixDependencies; /** * Provide access to the private fields of the Eclipse mojo */ private ReflectMojo m_eclipseMojo; /** * The main project when generating Eclipse project files for imported bundles */ private MavenProject m_provisionProject; /** * IDE dependencies that might be embedded inside the bundle */ private List m_embeddableDependencies; /** * {@inheritDoc} */ public boolean setup() throws MojoExecutionException { // we don't fork eclipse goal setExecutedProject(project); if (null != m_provisionProject) { enablePDE(); // imported OSGi bundle } else if (PomUtils.isBundleProject(executedProject)) { enablePDE(); // compiled OSGi bundle setUseProjectReferences(false); } else if (ProvisionMojo.isProvisioningPom(executedProject)) { try { /* * unpack imported bundles and generate Eclipse files one-by-one */ setupImportedBundles(); } catch (InvalidDependencyVersionException e) { getLog().warn("Unable to generate Eclipse files for project " + executedProject.getId()); } /* * don't create Eclipse files for the provisioning POM itself! */ return false; } // default to normal behaviour return super.setup(); } /** * Enable PDE support */ private void enablePDE() { if (null == m_eclipseMojo) { // by default enable creation of PDE project files for OSGi m_eclipseMojo = new ReflectMojo(this, EclipsePlugin.class); } m_eclipseMojo.setField("pde", Boolean.TRUE); setWtpversion("none"); List containers = getClasspathContainers(); if (null != containers && !containers.contains(REQUIRED_PLUGINS_CONTAINER)) { containers.add(REQUIRED_PLUGINS_CONTAINER); } } /** * {@inheritDoc} */ public void writeConfiguration(IdeDependency[] deps) throws MojoExecutionException { if (!isPdeProject()) { // non-OSGi project super.writeConfiguration(deps); } else { m_embeddableDependencies = new ArrayList(); if (null == m_provisionProject) { // compiled OSGi bundle / wrapper writeBundleConfiguration(deps); } else { // imported (external) OSGi bundle writeImportedConfiguration(); } } } /** * Customize Eclipse project files for Pax-Construct generated bundles * * @param deps resolved project dependencies, potentially with sources and javadocs * @throws MojoExecutionException */ private void writeBundleConfiguration(IdeDependency[] deps) throws MojoExecutionException { for (int i = 0; i < deps.length; i++) { if (deps[i].isAddedToClasspath()) { // cache here so we can attach sources to embedded entries if (!deps[i].isTestDependency() && !deps[i].isProvided()) { m_embeddableDependencies.add(deps[i]); } /* * Change potential test dependencies to be non-OSGi otherwise they won't appear as Required Libraries. * We include provided dependencies here because PDE might not add them as Plug-in Dependencies, it all * depends on the manifest (which won't necessarily import packages used during testing) */ deps[i] = fixOSGiTestDependency(deps[i]); } } EclipseWriterConfig config = createEclipseWriterConfig(deps); config.setEclipseProjectName(getEclipseProjectName(executedProject, true)); new EclipseSettingsWriter().init(getLog(), config).write(); new EclipseClasspathWriter().init(getLog(), config).write(); new EclipseProjectWriter().init(getLog(), config).write(); /* * copy bundle manifest to where PDE expects it, but tweak it to fix embedded paths */ refactorForEclipse(getBundleFile(executedProject)); writeAdditionalConfig(); } /** * @param bundleProject bundle project * @return recently built bundle */ private File getBundleFile(MavenProject bundleProject) { Artifact artifact = bundleProject.getArtifact(); File bundleFile = artifact.getFile(); if (null == bundleFile || !bundleFile.exists()) { // no file attached in this cycle, so check local build String name = bundleProject.getBuild().getFinalName() + ".jar"; bundleFile = new File(bundleProject.getBuild().getDirectory(), name); } if (!bundleFile.exists()) { // last chance: see if it is already has been installed locally PomUtils.getFile(artifact, artifactResolver, localRepository); bundleFile = artifact.getFile(); } return bundleFile; } /** * The maven-eclipse-plugin has a bug where test dependencies that are also OSGi bundles are excluded from the * .classpath because they are expected to be on the Bundle-ClassPath. However, this is not a valid assumption * because the Maven test-cases and their dependencies do not necessarily end-up packaged inside the bundle. * * @param dependency an IDE test dependency * @return fixed IDE test dependency */ private IdeDependency fixOSGiTestDependency(IdeDependency dependency) { if ("FALSE".equalsIgnoreCase(fixDependencies)) { return dependency; } // by default, don't fix dependencies pointing to local projects found in the same tree if ("EXTERNAL".equalsIgnoreCase(fixDependencies) && isReactorDependency(dependency)) { return dependency; } // previous behaviour in 1.3 was exhaustive search of the poms in the local file-system if ("CUSTOM".equalsIgnoreCase(fixDependencies)) { String id = dependency.getGroupId() + ':' + dependency.getArtifactId(); File baseDir = executedProject.getBasedir(); if (DirUtils.findPom(baseDir, id) != null) { return dependency; } } // unfortunately there's no setIsOsgiBundle() method, so we have to replace the whole dependency... IdeDependency testDependency = new IdeDependency(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), dependency.getClassifier(), dependency.isReferencedProject(), true, false, false, dependency.isAddedToClasspath(), dependency.getFile(), dependency.getType(), false, null, 0, dependency.getEclipseProjectName()); testDependency.setSourceAttachment(dependency.getSourceAttachment()); testDependency.setJavadocAttachment(dependency.getJavadocAttachment()); return testDependency; } private boolean isReactorDependency(IdeDependency dependency) { // check current reactor... if (reactorProjects != null) { for (Iterator i = reactorProjects.iterator(); i.hasNext();) { MavenProject reactorProject = (MavenProject) i.next(); if (reactorProject.getGroupId().equals(dependency.getGroupId()) && reactorProject.getArtifactId().equals(dependency.getArtifactId())) { return true; } } } return false; } /** * Provide better naming for Pax-Construct generated OSGi bundles * * @param project current Maven project * @param addVersion when true, add the project version to the name * @return an Eclipse friendly name for the bundle */ private static String getEclipseProjectName(MavenProject project, boolean addVersion) { String projectName = project.getProperties().getProperty("bundle.symbolicName"); if (null == projectName) { // fall back to standard "groupId.artifactId" but try to eliminate duplicate segments projectName = PomUtils.getCompoundId(project.getGroupId(), project.getArtifactId()); } if (addVersion) { // check for wrapper version, which reflects the version of the wrapped contents String projectVersion = project.getProperties().getProperty("wrapped.version"); if (null == projectVersion) { projectVersion = project.getVersion(); } return projectName + " [" + projectVersion + ']'; } return projectName; } /** * Select files for unpacking that don't exist locally under target/classes */ private static class IncludedContentFilter implements EntryFilter { private final File m_outputDir; /** * @param outputDir build output directory */ public IncludedContentFilter(File outputDir) { m_outputDir = outputDir; } /** * {@inheritDoc} */ public boolean accept(String name) { // always select metadata folders as we may need to refactor them if (name.startsWith("META-INF") || name.startsWith("OSGI-INF")) { return true; } // also select any embedded jars else if (name.endsWith(".jar")) { return true; } // do we already have this file locally? return new File(m_outputDir, name).exists() == false; } } /** * Copy OSGi metadata to where Eclipse PDE expects it, but adjust the Bundle-ClassPath so Eclipse can find any * embedded jars or directories in the unpacked bundle contents under the temporary directory * * @param bundleFile the packaged bundle */ private void refactorForEclipse(File bundleFile) { // temporary location in the output folder String tempPath = "target/pax-eclipse"; boolean refactorManifest = false; // make relative to the provisioning POM File baseDir = executedProject.getBasedir(); File unpackDir = new File(baseDir, tempPath); if (bundleFile == null || !bundleFile.exists()) { getLog().warn("Bundle has not been built, reverting to basic behaviour"); } else { DirUtils.unpackBundle(bundleFile, unpackDir, new IncludedContentFilter(getBuildOutputDirectory())); moveMetadata(unpackDir, "META-INF", baseDir); moveMetadata(unpackDir, "OSGI-INF", baseDir); // test to see if it's empty unpackDir.delete(); refactorManifest = unpackDir.exists(); } File manifestFile = new File(baseDir, "META-INF/MANIFEST.MF"); Manifest manifest = getBundleManifest(manifestFile); Attributes mainAttributes = manifest.getMainAttributes(); if (mainAttributes.getValue("Bundle-SymbolicName") == null) { // Eclipse mis-behaves if the bundle has no symbolic name :( String name = getEclipseProjectName(executedProject, false); mainAttributes.putValue("Bundle-SymbolicName", name.replace('-', '_')); } String bundleClassPath = mainAttributes.getValue("Bundle-ClassPath"); /* * refactor Bundle-ClassPath to help Eclipse find the unpacked contents */ if (refactorManifest) { bundleClassPath = ".," + DirUtils.rebasePaths(bundleClassPath, tempPath, ','); mainAttributes.putValue("Bundle-ClassPath", bundleClassPath); // add the embedded entries back to the Eclipse classpath addEmbeddedEntriesToEclipseClassPath(tempPath, bundleClassPath); } try { manifestFile.getParentFile().mkdirs(); FileOutputStream out = new FileOutputStream(manifestFile); manifest.write(out); IOUtil.close(out); } catch (IOException e) { getLog().warn("Unable to update Eclipse manifest: " + manifestFile); } createBuildProperties(baseDir, tempPath); } /** * Create simple build.properties file so the export functionality from Eclipse/PDE works * * @param baseDir project base directory * @param unpackPath path where the additional bundle contents were unpacked */ private void createBuildProperties(File baseDir, String unpackPath) { File buildPropertiesFile = new File(baseDir, "build.properties"); if (!buildPropertiesFile.exists()) { BufferedWriter writer = null; try { writer = new BufferedWriter(new FileWriter(buildPropertiesFile)); /* compiled/wrapped bundle */ if (null != unpackPath) { File sourceDir = new File(baseDir, "src/main/java"); if (sourceDir.exists()) { writer.write("source.. = src/main/java/,src/main/resources/"); writer.newLine(); } writer.write("output.. = target/classes/"); writer.newLine(); writer.write("bin.includes = META-INF/,."); if (new File(baseDir, unpackPath).exists()) { writer.write(',' + unpackPath + '/'); } writer.newLine(); } /* imported bundle */ else { File bundleFile = executedProject.getArtifact().getFile(); if (null != bundleFile && bundleFile.isFile()) { writer.write("install.location = " + bundleFile.toURI()); writer.newLine(); } writer.write("source.. = ."); writer.newLine(); writer.write("output.. = ."); writer.newLine(); writer.write("bin.includes = META-INF/,."); writer.newLine(); } } catch (IOException e) { getLog().warn("Unable to create build.properties file"); } finally { IOUtil.close(writer); } } } /** * @param manifestFile path to the local bundle manifest * @return the bundle manifest, or sane defaults if the manifest couldn't be opened */ private Manifest getBundleManifest(File manifestFile) { Manifest manifest = new Manifest(); try { FileInputStream in = new FileInputStream(manifestFile); manifest.read(in); IOUtil.close(in); } catch (IOException e) { Attributes mainAttributes = manifest.getMainAttributes(); String osgiVersion = m_maven2OsgiConverter.getVersion(project.getVersion()); // standard OSGi entries mainAttributes.putValue("Manifest-Version", "1"); mainAttributes.putValue("Bundle-ManifestVersion", "2"); mainAttributes.putValue("Bundle-Name", project.getName()); mainAttributes.putValue("Bundle-Version", osgiVersion); // some basic OSGi dependencies, to help people get their code compiling... mainAttributes.putValue("Import-Package", "org.osgi.framework,org.osgi.util.tracker"); } return manifest; } /** * @param fromDir source directory * @param metadata metadata folder * @param toDir target directory */ private void moveMetadata(File fromDir, String metadata, File toDir) { File metadataDir = new File(fromDir, metadata); if (metadataDir.exists()) { try { FileUtils.copyDirectoryStructure(metadataDir, new File(toDir, metadata)); FileUtils.deleteDirectory(metadataDir); } catch (IOException e) { getLog().warn("Unable to copy " + metadata + " contents to base directory"); } } } /** * Add any embedded Bundle-ClassPath entries to the Eclipse classpath and re-attach sources/javadocs * * @param bundleLocation relative path to the unpacked bundle * @param bundleClassPath the refactored Bundle-ClassPath */ private void addEmbeddedEntriesToEclipseClassPath(String bundleLocation, String bundleClassPath) { String[] classPath = bundleClassPath.split(","); File basedir = executedProject.getBasedir(); try { File classPathFile = new File(basedir, ".classpath"); Reader reader = StreamFactory.newXmlReader(classPathFile); Xpp3Dom classPathXML = Xpp3DomBuilder.build(reader); IOUtil.close(reader); for (int i = 0; i < classPath.length; i++) { String binaryPath = classPath[i].trim(); if (!".".equals(binaryPath) && new File(basedir, binaryPath).exists()) { // embedded jar/directory needs to be a 'lib' entry Xpp3Dom classPathEntry = new Xpp3Dom("classpathentry"); classPathEntry.setAttribute("exported", "true"); classPathEntry.setAttribute("kind", "lib"); classPathEntry.setAttribute("path", binaryPath); // find attached sources using the previously cached IDE dependencies File sourcePath = findAttachedSource(bundleLocation, binaryPath); if (sourcePath != null) { classPathEntry.setAttribute("sourcepath", sourcePath.getPath()); } classPathXML.addChild(classPathEntry); } } Writer writer = StreamFactory.newXmlWriter(classPathFile); Xpp3DomWriter.write(new PrettyPrintXMLWriter(writer), classPathXML); IOUtil.close(writer); } catch (IOException e) { getLog().warn("Unable to find Eclipse .classpath file"); } catch (XmlPullParserException e) { getLog().warn("Unable to parse Eclipse .classpath file"); } } /** * Search cached IDE dependencies for an entry matching the given classpath element * * @param bundleLocation relative path to the unpacked bundle * @param classPathEntry classpath element * @return path to the attached sources */ private File findAttachedSource(String bundleLocation, String classPathEntry) { for (Iterator i = m_embeddableDependencies.iterator(); i.hasNext();) { IdeDependency dependency = (IdeDependency) i.next(); // equivalent to '.' - source is first in list if (bundleLocation.equals(classPathEntry)) { return dependency.getSourceAttachment(); } else if (Pattern.matches("^.*[/\\\\]" + dependency.getArtifactId() + "[-.][^/\\\\]*$", classPathEntry)) { return dependency.getSourceAttachment(); } } return null; } /** * Unpack each imported bundle in turn and generate the relevant Eclipse project files * * @throws InvalidDependencyVersionException * @throws MojoExecutionException */ private void setupImportedBundles() throws InvalidDependencyVersionException, MojoExecutionException { // don't process dependencies of imported bundles m_provisionProject = getExecutedProject(); setResolveDependencies(false); Set artifacts = m_provisionProject.createArtifacts(artifactFactory, null, null); for (Iterator i = artifacts.iterator(); i.hasNext();) { Artifact artifact = (Artifact) i.next(); // store project locally underneath the provisioning POM's directory File groupDir = new File(m_provisionProject.getBasedir(), "target/" + artifact.getGroupId()); File baseDir = new File(groupDir, artifact.getArtifactId() + '-' + artifact.getVersion()); // download and unpack the bundle if (!PomUtils.downloadFile(artifact, artifactResolver, remoteArtifactRepositories, localRepository)) { getLog().warn("Skipping missing bundle " + artifact); continue; } DirUtils.unpackBundle(artifact.getFile(), baseDir, null); // download the bundle POM and store locally MavenProject dependencyProject = writeProjectPom(baseDir, artifact); if (null == dependencyProject) { getLog().warn("Skipping missing bundle " + artifact); continue; } dependencyProject.setArtifact(artifact); setExecutedProject(dependencyProject); setProject(dependencyProject); // trick Eclipse plugin to do the right thing setBuildOutputDirectory(new File(baseDir, ".ignore")); setEclipseProjectDir(baseDir); try { // call the Eclipse plugin getLog().info("Generating Eclipse project for bundle " + artifact); execute(); } catch (MojoFailureException e) { getLog().warn("Problem generating Eclipse files for artifact " + artifact); } } } /** * Download and save the Maven POM for the given artifact * * @param baseDir base directory * @param artifact Maven artifact * @return the downloaded project */ private MavenProject writeProjectPom(File baseDir, Artifact artifact) { MavenProject pom = null; String groupId = artifact.getGroupId(); String artifactId = artifact.getArtifactId(); String version = artifact.getVersion(); Artifact pomArtifact = artifactFactory.createProjectArtifact(groupId, artifactId, version); try { pom = m_mavenProjectBuilder.buildFromRepository(pomArtifact, remoteArtifactRepositories, localRepository); // need this when using Maven 2.1 which doesn't do any alignment m_pathTranslator.alignToBaseDirectory(pom.getModel(), baseDir); File pomFile = new File(baseDir, "pom.xml"); Writer writer = StreamFactory.newXmlWriter(pomFile); pom.writeModel(writer); pom.setFile(pomFile); IOUtil.close(writer); } catch (ProjectBuildingException e) { getLog().warn("Unable to build POM for artifact " + pomArtifact); } catch (IOException e) { getLog().warn("Unable to write POM for artifact " + pomArtifact); } return pom; } /** * Customize Eclipse project files for imported (unpacked) bundles * * @throws MojoExecutionException */ private void writeImportedConfiguration() throws MojoExecutionException { EclipseWriterConfig config = createEclipseWriterConfig(new IdeDependency[0]); config.setEclipseProjectName(getEclipseProjectName(executedProject, true)); config.setClasspathContainers(Collections.EMPTY_LIST); config.setSourceDirs(new EclipseSourceDir[0]); // not compiling, so just need project and classpath files new EclipseClasspathWriter().init(getLog(), config).write(); new EclipseProjectWriter().init(getLog(), config).write(); Artifact sourceArtifact = artifactFactory.createArtifactWithClassifier(executedProject.getGroupId(), executedProject.getArtifactId(), executedProject.getVersion(), "java-source", "sources"); if (downloadSources) { PomUtils.downloadFile(sourceArtifact, artifactResolver, remoteArtifactRepositories, localRepository); } else { // check to see if we already have the source downloaded... PomUtils.getFile(sourceArtifact, artifactResolver, localRepository); } // set PDE classpath to point to unpacked bundle attachImportedContent(sourceArtifact.getFile()); String baseDir = executedProject.getBasedir().getPath(); File manifestFile = new File(baseDir, "META-INF/MANIFEST.MF"); Manifest manifest = getBundleManifest(manifestFile); Attributes mainAttributes = manifest.getMainAttributes(); String bundleClassPath = mainAttributes.getValue("Bundle-ClassPath"); if (null != bundleClassPath) { // add any embedded entries to the default Eclipse classpath addEmbeddedEntriesToEclipseClassPath(baseDir, bundleClassPath); } createBuildProperties(new File(baseDir), null); } /** * Add a classpath entry for the unpacked imported bundle and attach it to the given source * * @param sources attached bundle sources */ private void attachImportedContent(File sources) { try { File classPathFile = new File(executedProject.getBasedir(), ".classpath"); Reader reader = StreamFactory.newXmlReader(classPathFile); Xpp3Dom classPathXML = Xpp3DomBuilder.build(reader); IOUtil.close(reader); Xpp3Dom classPathEntry = new Xpp3Dom("classpathentry"); classPathEntry.setAttribute("exported", "true"); classPathEntry.setAttribute("kind", "lib"); classPathEntry.setAttribute("path", "."); if (sources != null && sources.exists()) { classPathEntry.setAttribute("sourcepath", sources.getPath()); } classPathXML.addChild(classPathEntry); Writer writer = StreamFactory.newXmlWriter(classPathFile); Xpp3DomWriter.write(new PrettyPrintXMLWriter(writer), classPathXML); IOUtil.close(writer); } catch (IOException e) { getLog().warn("Unable to find Eclipse .classpath file"); } catch (XmlPullParserException e) { getLog().warn("Unable to parse Eclipse .classpath file"); } } }