org.xwiki.extension.repository.internal.DefaultCoreExtensionScanner.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.extension.repository.internal.DefaultCoreExtensionScanner.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.extension.repository.internal;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Developer;
import org.apache.maven.model.License;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.extension.DefaultExtensionDependency;
import org.xwiki.extension.ExtensionId;
import org.xwiki.extension.ExtensionLicense;
import org.xwiki.extension.ExtensionLicenseManager;
import org.xwiki.properties.ConverterManager;

import com.google.common.base.Predicates;

/**
 * Scan jars to find core extensions.
 * 
 * @version $Id: 00cda65d02c2bfbd8e22d0789af6e9c514c89d0b $
 */
@Component
@Singleton
public class DefaultCoreExtensionScanner implements CoreExtensionScanner {
    /**
     * The package containing maven informations in a jar file.
     */
    private static final String MAVENPACKAGE = "META-INF.maven";

    /**
     * Unknown.
     */
    private static final String UNKNOWN = "unknown";

    /**
     * SNAPSHOT suffix in versions.
     */
    private static final String SNAPSHOTSUFFIX = "-SNAPSHOT";

    /**
     * The logger to log.
     */
    @Inject
    private Logger logger;

    /**
     * Used to parse some custom properties.
     */
    @Inject
    private ConverterManager converter;

    /**
     * Used to find proper {@link ExtensionLicense}.
     */
    @Inject
    private ExtensionLicenseManager licenseManager;

    @Override
    public Map<String, DefaultCoreExtension> loadExtensions(DefaultCoreExtensionRepository repository) {
        Set<URL> mavenURLs = ClasspathHelper.forPackage(MAVENPACKAGE);

        // FIXME: remove that as soon as possible
        mavenURLs = filterURLs(mavenURLs);

        ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.setScanners(new ResourcesScanner());
        configurationBuilder.setUrls(mavenURLs);
        configurationBuilder.filterInputsBy(new FilterBuilder.Include(FilterBuilder.prefix(MAVENPACKAGE)));

        Reflections reflections = new Reflections(configurationBuilder);

        Set<String> descriptors = reflections.getResources(Predicates.equalTo("pom.xml"));

        Map<String, DefaultCoreExtension> extensions = new HashMap<String, DefaultCoreExtension>(
                descriptors.size());

        List<Dependency> dependencies = new ArrayList<Dependency>();
        List<Object[]> coreArtefactIds = new ArrayList<Object[]>();

        for (String descriptor : descriptors) {
            URL descriptorUrl = getClass().getClassLoader().getResource(descriptor);

            InputStream descriptorStream = getClass().getClassLoader().getResourceAsStream(descriptor);
            try {
                MavenXpp3Reader reader = new MavenXpp3Reader();
                Model mavenModel = reader.read(descriptorStream);

                String version = resolveVersion(mavenModel.getVersion(), mavenModel, false);
                String groupId = resolveGroupId(mavenModel.getGroupId(), mavenModel, false);

                String extensionURLStr = descriptorUrl.toString();
                extensionURLStr = extensionURLStr.substring(0,
                        descriptorUrl.toString().indexOf(MAVENPACKAGE.replace('.', '/')));
                URL extensionURL = new URL(extensionURLStr);

                DefaultCoreExtension coreExtension = new DefaultCoreExtension(repository, extensionURL,
                        new ExtensionId(groupId + ':' + mavenModel.getArtifactId(), version),
                        packagingToType(mavenModel.getPackaging()));

                coreExtension.setName(mavenModel.getName());
                coreExtension.setDescription(mavenModel.getDescription());
                for (Developer developer : mavenModel.getDevelopers()) {
                    coreExtension.addAuthor(developer.getId());
                }
                coreExtension.setWebsite(mavenModel.getUrl());

                // licenses
                for (License license : mavenModel.getLicenses()) {
                    coreExtension.addLicense(getExtensionLicense(license));
                }

                // features
                String featuresString = mavenModel.getProperties().getProperty("xwiki.extension.features");
                if (StringUtils.isNotBlank(featuresString)) {
                    coreExtension
                            .setFeatures(this.converter.<Collection<String>>convert(List.class, featuresString));
                }

                // dependencies
                for (Dependency mavenDependency : mavenModel.getDependencies()) {
                    if (!mavenDependency.isOptional()
                            && (mavenDependency.getScope() == null || mavenDependency.getScope().equals("compile")
                                    || mavenDependency.getScope().equals("runtime"))) {
                        coreExtension.addDependency(new DefaultExtensionDependency(
                                resolveGroupId(mavenDependency.getGroupId(), mavenModel, true) + ':'
                                        + mavenDependency.getArtifactId(),
                                resolveVersion(mavenDependency.getVersion(), mavenModel, true)));
                    }
                }

                // custom properties
                coreExtension.putProperty("maven.groupId", groupId);
                coreExtension.putProperty("maven.artifactId", mavenModel.getArtifactId());

                extensions.put(coreExtension.getId().getId(), coreExtension);
                coreArtefactIds.add(new Object[] { mavenModel.getArtifactId(), coreExtension });

                for (Dependency dependency : mavenModel.getDependencies()) {
                    if (dependency.getGroupId().equals("${project.groupId}")) {
                        dependency.setGroupId(groupId);
                    }
                    if (dependency.getVersion() == null) {
                        dependency.setVersion(UNKNOWN);
                    } else if (dependency.getVersion().equals("${project.version}")
                            || dependency.getVersion().equals("${pom.version}")) {
                        dependency.setVersion(version);
                    }
                    dependencies.add(dependency);
                }
            } catch (Exception e) {
                this.logger.warn("Failed to parse descriptor [" + descriptorUrl
                        + "], it will be ignored and not found in core extensions.", e);
            } finally {
                try {
                    descriptorStream.close();
                } catch (IOException e) {
                    // Should not happen
                    this.logger.error("Failed to close descriptor stream [" + descriptorUrl + "]", e);
                }
            }

            // Normalize and guess

            Map<String, Object[]> artefacts = new HashMap<String, Object[]>();
            Set<URL> urls = ClasspathHelper.forClassLoader();

            for (URL url : urls) {
                try {
                    String path = url.toURI().getPath();
                    String filename = path.substring(path.lastIndexOf('/') + 1);

                    int extIndex = filename.lastIndexOf('.');
                    if (extIndex != -1) {
                        filename = filename.substring(0, extIndex);
                    }

                    int index;
                    if (!filename.endsWith(SNAPSHOTSUFFIX)) {
                        index = filename.lastIndexOf('-');
                    } else {
                        index = filename.lastIndexOf('-', filename.length() - SNAPSHOTSUFFIX.length());
                    }

                    if (index != -1) {
                        String artefactname = filename.substring(0, index);
                        String version = filename.substring(index + 1);

                        artefacts.put(artefactname, new Object[] { version, url });
                    }
                } catch (Exception e) {
                    this.logger.warn("Failed to parse resource name [" + url + "]", e);
                }
            }

            // Try to resolve version no easy to find from the pom.xml
            try {
                for (Object[] coreArtefactId : coreArtefactIds) {
                    Object[] artefact = artefacts.get(coreArtefactId[0]);

                    DefaultCoreExtension coreExtension = (DefaultCoreExtension) coreArtefactId[1];
                    if (artefact != null && coreExtension.getId().getVersion().charAt(0) == '$') {
                        coreExtension.setId(new ExtensionId(coreExtension.getId().getId(), (String) artefact[0]));
                        coreExtension.setGuessed(true);
                    }
                }

                // Add dependencies that does not provide proper pom.xml resource and can't be found in the classpath
                for (Dependency dependency : dependencies) {
                    String dependencyId = dependency.getGroupId() + ':' + dependency.getArtifactId();

                    Object[] artefact = artefacts.get(dependency.getArtifactId());
                    if (artefact != null) {
                        DefaultCoreExtension coreExtension = extensions.get(dependencyId);
                        if (coreExtension == null) {
                            coreExtension = new DefaultCoreExtension(repository, (URL) artefact[1],
                                    new ExtensionId(dependencyId, (String) artefact[0]),
                                    packagingToType(dependency.getType()));
                            coreExtension.setGuessed(true);

                            extensions.put(dependencyId, coreExtension);
                        }
                    }
                }
            } catch (Exception e) {
                this.logger.warn("Failed to guess extra information about some extensions", e);
            }
        }

        return extensions;
    }

    private String resolveVersion(String modelVersion, Model mavenModel, boolean dependency) {
        String version = modelVersion;

        // TODO: download parents and resolve pom.xml properties using aether ? could be pretty expensive for
        // the init
        if (version == null) {
            if (!dependency) {
                Parent parent = mavenModel.getParent();

                if (parent != null) {
                    version = parent.getVersion();
                }
            }
        } else if (version.startsWith("$")) {
            String propertyName = version.substring(2, version.length() - 1);

            if (propertyName.equals("project.version") || propertyName.equals("pom.version")
                    || propertyName.equals("version")) {
                version = resolveVersion(mavenModel.getVersion(), mavenModel, false);
            } else {
                String value = mavenModel.getProperties().getProperty(propertyName);
                if (value != null) {
                    version = value;
                }
            }
        }

        if (version == null) {
            version = UNKNOWN;
        }

        return version;
    }

    private String resolveGroupId(String modelGroupId, Model mavenModel, boolean dependency) {
        String groupId = modelGroupId;

        // TODO: download parents and resolve pom.xml properties using aether ? could be pretty expensive for
        // the init
        if (groupId == null) {
            if (!dependency) {
                Parent parent = mavenModel.getParent();

                if (parent != null) {
                    groupId = parent.getGroupId();
                }
            }
        } else if (groupId.startsWith("$")) {
            String propertyName = groupId.substring(2, groupId.length() - 1);

            String value = mavenModel.getProperties().getProperty(propertyName);
            if (value != null) {
                groupId = value;
            }
        }

        if (groupId == null) {
            groupId = UNKNOWN;
        }

        return groupId;
    }

    // TODO: download custom licenses content
    private ExtensionLicense getExtensionLicense(License license) {
        if (license.getName() == null) {
            return new ExtensionLicense("noname", null);
        }

        ExtensionLicense extensionLicense = this.licenseManager.getLicense(license.getName());

        return extensionLicense != null ? extensionLicense : new ExtensionLicense(license.getName(), null);
    }

    /**
     * Very nasty hack which unescape invalid characters from the {@link URL} path because
     * {@link Reflections#Reflections(org.reflections.Configuration)} does not do it...
     * 
     * @param urls base URLs to modify
     * @return modified base URLs
     */
    // TODO: remove when http://code.google.com/p/reflections/issues/detail?id=87 is fixed
    private Set<URL> filterURLs(Set<URL> urls) {
        Set<URL> results = new HashSet<URL>(urls.size());
        for (URL url : urls) {
            try {
                results.add(new URL(url.getProtocol(), url.getHost(), url.getPort(),
                        URLDecoder.decode(url.getFile(), "UTF-8")));
            } catch (Exception e) {
                this.logger.error("Failed to convert url [" + url + "]", e);

                results.add(url);
            }
        }

        return results;
    }

    /**
     * Get the extension type from maven packaging.
     * 
     * @param packaging the maven packaging
     * @return the extension type
     */
    private String packagingToType(String packaging) {
        // support bundle packaging
        if (packaging.equals("bundle")) {
            return "jar";
        }

        return packaging;
    }
}