Java tutorial
/* * 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.installed; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.xwiki.component.annotation.Component; import org.xwiki.component.phase.Initializable; import org.xwiki.component.phase.InitializationException; import org.xwiki.extension.CoreExtension; import org.xwiki.extension.ExtensionDependency; import org.xwiki.extension.ExtensionId; import org.xwiki.extension.InstallException; import org.xwiki.extension.InstalledExtension; import org.xwiki.extension.InvalidExtensionException; import org.xwiki.extension.LocalExtension; import org.xwiki.extension.ResolveException; import org.xwiki.extension.UninstallException; import org.xwiki.extension.repository.CoreExtensionRepository; import org.xwiki.extension.repository.DefaultExtensionRepositoryDescriptor; import org.xwiki.extension.repository.InstalledExtensionRepository; import org.xwiki.extension.repository.LocalExtensionRepository; import org.xwiki.extension.version.Version; import org.xwiki.extension.version.VersionConstraint; /** * Default implementation of {@link InstalledExtensionRepository}. * * @version $Id: f533af5b5fa45833d8d0b7f05fbf4eccbc476f69 $ * @since 4.0M2 */ @Component @Singleton // TODO: move all installation related code from local repository to here public class DefaultInstalledExtensionRepository extends AbstractInstalledExtensionRepository<DefaultInstalledExtension> implements InstalledExtensionRepository, Initializable { private static class InstalledRootFeature { public DefaultInstalledExtension extension; public Set<DefaultInstalledExtension> invalidExtensions = new HashSet<DefaultInstalledExtension>(); public String namespace; public Set<DefaultInstalledExtension> backwardDependencies = new HashSet<DefaultInstalledExtension>(); public InstalledRootFeature(String namespace) { this.namespace = namespace; } } private static class InstalledFeature { public InstalledRootFeature root; public String feature; public InstalledFeature(InstalledRootFeature root, String feature) { this.root = root; this.feature = feature; } } /** * Used to access all local extensions. */ @Inject private transient LocalExtensionRepository localRepository; /** * Used to check for existing core extensions. */ @Inject private transient CoreExtensionRepository coreExtensionRepository; /** * The logger to log. */ @Inject private transient Logger logger; /** * The installed extensions sorted by provided feature and namespace. * <p> * <feature, <namespace, extension>> */ private Map<String, Map<String, InstalledFeature>> extensionNamespaceByFeature = new ConcurrentHashMap<String, Map<String, InstalledFeature>>(); /** * Temporary map using only during init. * <p> * <feature, <namespace, extensions>> */ private Map<String, Map<String, Set<LocalExtension>>> localInstalledExtensionsCache; @Override public void initialize() throws InitializationException { setDescriptor(new DefaultExtensionRepositoryDescriptor("installed", "installed", this.localRepository.getDescriptor().getURI())); // Get installed extensions from local repository this.localInstalledExtensionsCache = new HashMap<>(); for (LocalExtension localExtension : this.localRepository.getLocalExtensions()) { if (DefaultInstalledExtension.isInstalled(localExtension)) { addInstalledLocalExtension(localExtension); } } // Validate installed extensions for (LocalExtension localExtension : this.localRepository.getLocalExtensions()) { if (DefaultInstalledExtension.isInstalled(localExtension)) { validateExtension(localExtension); } } // Reset temporary cache this.localInstalledExtensionsCache = null; } private void addInstalledLocalExtension(LocalExtension localExtension) { addInstalledLocalExtension(localExtension.getId().getId(), localExtension); for (String feature : localExtension.getFeatures()) { addInstalledLocalExtension(feature, localExtension); } } private void addInstalledLocalExtension(String feature, LocalExtension localExtension) { Collection<String> namespaces = DefaultInstalledExtension.getNamespaces(localExtension); if (namespaces == null) { addInstalledLocalExtension(feature, null, localExtension); } else { for (String namespace : namespaces) { addInstalledLocalExtension(feature, namespace, localExtension); } } } private void addInstalledLocalExtension(String feature, String namespace, LocalExtension localExtension) { Map<String, Set<LocalExtension>> localInstallExtensionFeature = this.localInstalledExtensionsCache .get(feature); if (localInstallExtensionFeature == null) { localInstallExtensionFeature = new HashMap<>(); this.localInstalledExtensionsCache.put(feature, localInstallExtensionFeature); } Set<LocalExtension> localInstallExtensionNamespace = localInstallExtensionFeature.get(namespace); if (localInstallExtensionNamespace == null) { localInstallExtensionNamespace = new HashSet<LocalExtension>(); localInstallExtensionFeature.put(namespace, localInstallExtensionNamespace); } localInstallExtensionNamespace.add(localExtension); } // Validation /** * Check extension validity and set it as not installed if not. * * @param localExtension the extension to validate * @throws InvalidExtensionException when the passed extension is fond invalid */ private void validateExtension(LocalExtension localExtension) { Collection<String> namespaces = DefaultInstalledExtension.getNamespaces(localExtension); if (namespaces == null) { try { validateExtension(localExtension, null); } catch (InvalidExtensionException e) { if (this.logger.isDebugEnabled()) { this.logger.warn("Invalid extension [{}]", localExtension.getId(), e); } else { this.logger.warn("Invalid extension [{}] ({})", localExtension.getId(), ExceptionUtils.getRootCauseMessage(e)); } addInstalledExtension(localExtension, null, false); } } else { for (String namespace : namespaces) { try { validateExtension(localExtension, namespace); } catch (InvalidExtensionException e) { if (this.logger.isDebugEnabled()) { this.logger.warn("Invalid extension [{}] on namespace [{}]", localExtension.getId(), namespace, e); } else { this.logger.warn("Invalid extension [{}] on namespace [{}] ({})", localExtension.getId(), namespace, ExceptionUtils.getRootCauseMessage(e)); } addInstalledExtension(localExtension, namespace, false); } } } } private LocalExtension getInstalledLocalExtension(ExtensionDependency dependency, String namespace) { Map<String, Set<LocalExtension>> localInstallExtensionFeature = this.localInstalledExtensionsCache .get(dependency.getId()); if (localInstallExtensionFeature != null) { Set<LocalExtension> localInstallExtensionNamespace = localInstallExtensionFeature.get(namespace); if (localInstallExtensionNamespace != null) { for (LocalExtension dependencyVersion : localInstallExtensionNamespace) { if (isCompatible(dependencyVersion.getId().getVersion(), dependency.getVersionConstraint())) { return dependencyVersion; } } } } // Try on root namespace if (namespace != null) { return getInstalledLocalExtension(dependency, null); } return null; } private void validateDependency(ExtensionDependency dependency, String namespace) throws InvalidExtensionException { CoreExtension coreExtension = this.coreExtensionRepository.getCoreExtension(dependency.getId()); if (coreExtension != null) { if (!isCompatible(coreExtension.getId().getVersion(), dependency.getVersionConstraint())) { throw new InvalidExtensionException(String.format( "Dependency [%s] is incompatible with the core extension [%s]", dependency, coreExtension)); } } else { LocalExtension dependencyExtension = this.localInstalledExtensionsCache != null ? getInstalledLocalExtension(dependency, namespace) : getInstalledExtension(dependency.getId(), namespace); if (dependencyExtension == null) { throw new InvalidExtensionException( String.format("No compatible extension is installed for dependency [%s]", dependency)); } else { try { DefaultInstalledExtension installedExtension = validateExtension(dependencyExtension, namespace); if (!installedExtension.isValid(namespace)) { throw new InvalidExtensionException( String.format("Extension dependency [%s] is invalid", installedExtension.getId())); } } catch (InvalidExtensionException e) { if (this.localInstalledExtensionsCache != null) { addInstalledExtension(dependencyExtension, namespace, false); } throw e; } } } } /** * Check extension validity against a specific namespace. * * @param localExtension the extension to validate * @param namespace the namespace * @return the corresponding {@link DefaultInstalledExtension} * @throws InvalidExtensionException when the passed extension is fond invalid */ private DefaultInstalledExtension validateExtension(LocalExtension localExtension, String namespace) throws InvalidExtensionException { InstalledFeature feature = getInstalledFeatureFromCache(localExtension.getId().getId(), namespace); if (feature != null && feature.root.extension != null) { // Already validated return feature.root.extension; } // Actually validate if (namespace != null && DefaultInstalledExtension.getNamespaces(localExtension) == null) { // This extension is supposed to be installed on root namespace only so redirecting to null namespace // initialization return validateExtension(localExtension, null); } if (!DefaultInstalledExtension.isInstalled(localExtension, namespace)) { throw new InvalidExtensionException(String.format("Extension [%s] is not installed", localExtension)); } if (this.coreExtensionRepository.exists(localExtension.getId().getId())) { throw new InvalidExtensionException( String.format("Extension [%s] already exists as a core extension", localExtension)); } // Validate dependencies InvalidExtensionException dependencyException = null; for (ExtensionDependency dependency : localExtension.getDependencies()) { try { validateDependency(dependency, namespace); } catch (InvalidExtensionException e) { // Continue to make sure all extension are validated in the right order if (dependencyException == null) { dependencyException = e; } } } // Throw exception if any issue has been found with dependencies if (dependencyException != null) { throw dependencyException; } // Complete local extension installation return localExtension instanceof DefaultInstalledExtension ? (DefaultInstalledExtension) localExtension : addInstalledExtension(localExtension, namespace, true); } private boolean isValid(DefaultInstalledExtension installedExtension, String namespace) { try { validateExtension(installedExtension, namespace); return true; } catch (InvalidExtensionException e) { this.logger.debug("Invalid extension [{}] on namespace [{}]", installedExtension.getId(), namespace, e); } return false; } private boolean isCompatible(Version existingVersion, VersionConstraint versionConstraint) { boolean compatible = true; if (versionConstraint.getVersion() == null) { compatible = versionConstraint.containsVersion(existingVersion); } else { compatible = existingVersion.compareTo(versionConstraint.getVersion()) >= 0; } return compatible; } // Install/Uninstall /** * Uninstall provided extension. * * @param installedExtension the extension to uninstall * @param namespace the namespace * @see #uninstallExtension(LocalExtension, String) */ private void removeInstalledExtension(DefaultInstalledExtension installedExtension, String namespace) { removeInstalledFeature(installedExtension.getId().getId(), namespace); for (String feature : installedExtension.getFeatures()) { removeInstalledFeature(feature, namespace); } removeFromBackwardDependencies(installedExtension, namespace); if (!installedExtension.isInstalled()) { removeCachedExtension(installedExtension); } } /** * Uninstall provided extension. * * @param feature the feature to uninstall * @param namespace the namespace * @see #uninstallExtension(LocalExtension, String) */ private void removeInstalledFeature(String feature, String namespace) { // Extensions namespaces by feature if (namespace == null) { this.extensionNamespaceByFeature.remove(feature); } else { Map<String, InstalledFeature> namespaceInstalledExtension = this.extensionNamespaceByFeature .get(feature); namespaceInstalledExtension.remove(namespace); } } /** * Install provided extension. * * @param localExtension the extension to install * @param namespace the namespace * @param dependency indicate if the extension is stored as a dependency of another one * @param properties the custom properties to set on the installed extension for the specified namespace * @throws InstallException error when trying to uninstall extension * @see #installExtension(LocalExtension, String) */ private void applyInstallExtension(DefaultInstalledExtension installedExtension, String namespace, boolean dependency, Map<String, Object> properties) throws InstallException { // INSTALLED installedExtension.setInstalled(true, namespace); installedExtension.setInstallDate(new Date(), namespace); // DEPENDENCY installedExtension.setDependency(dependency, namespace); // Add custom install properties for the specified namespace. The map holding the namespace properties should // not be null because it is initialized by the InstalledExtension#setInstalled(true, namespace) call above. installedExtension.getNamespaceProperties(namespace).putAll(properties); // Save properties try { this.localRepository.setProperties(installedExtension.getLocalExtension(), installedExtension.getProperties()); } catch (Exception e) { throw new InstallException("Failed to modify extension descriptor", e); } // VALID installedExtension.setValid(namespace, isValid(installedExtension, namespace)); // Update caches addInstalledExtension(installedExtension, namespace); } private void removeFromBackwardDependencies(DefaultInstalledExtension installedExtension, String namespace) { // Clean provided extension dependencies backward dependencies for (ExtensionDependency dependency : installedExtension.getDependencies()) { if (this.coreExtensionRepository.getCoreExtension(dependency.getId()) == null) { InstalledFeature installedFeature = getInstalledFeatureFromCache(dependency.getId(), namespace); if (installedFeature != null) { installedFeature.root.backwardDependencies.remove(installedExtension); } } } } /** * Register a newly installed extension in backward dependencies map. * * @param localExtension the local extension to register * @param namespace the namespace * @param valid is the extension valid * @return the new {@link DefaultInstalledExtension} */ private DefaultInstalledExtension addInstalledExtension(LocalExtension localExtension, String namespace, boolean valid) { DefaultInstalledExtension installedExtension = this.extensions.get(localExtension.getId()); if (installedExtension == null) { installedExtension = new DefaultInstalledExtension(localExtension, this); } installedExtension.setInstalled(true, namespace); installedExtension.setValid(namespace, valid); addInstalledExtension(installedExtension, namespace); return installedExtension; } /** * Register a newly installed extension in backward dependencies map. * * @param installedExtension the installed extension to register * @param namespace the namespace */ private void addInstalledExtension(DefaultInstalledExtension installedExtension, String namespace) { addCachedExtension(installedExtension); // Register the extension in the installed extensions for the provided namespace addInstalledFeatureToCache(installedExtension.getId().getId(), namespace, installedExtension); // Add virtual extensions for (String feature : installedExtension.getFeatures()) { addInstalledFeatureToCache(feature, namespace, installedExtension); } // Recalculate backward dependencies index updateMissingBackwardDependencies(); } private void updateMissingBackwardDependencies() { for (DefaultInstalledExtension installedExtension : this.extensions.values()) { updateMissingBackwardDependencies(installedExtension); } } private void updateMissingBackwardDependencies(DefaultInstalledExtension installedExtension) { Collection<String> namespaces = installedExtension.getNamespaces(); if (namespaces == null) { if (installedExtension.isValid(null)) { updateMissingBackwardDependencies(installedExtension, null); } } else { for (String namespace : namespaces) { if (installedExtension.isValid(namespace)) { updateMissingBackwardDependencies(installedExtension, namespace); } } } } private void updateMissingBackwardDependencies(DefaultInstalledExtension installedExtension, String namespace) { // Add the extension as backward dependency for (ExtensionDependency dependency : installedExtension.getDependencies()) { if (!this.coreExtensionRepository.exists(dependency.getId())) { // Get the extension for the dependency feature for the provided namespace DefaultInstalledExtension dependencyLocalExtension = (DefaultInstalledExtension) getInstalledExtension( dependency.getId(), namespace); if (dependencyLocalExtension != null) { // Make sure to register backward dependency on the right namespace InstalledFeature dependencyInstalledExtension = addInstalledFeatureToCache(dependency.getId(), namespace, dependencyLocalExtension); dependencyInstalledExtension.root.backwardDependencies.add(installedExtension); } } } } /** * Get extension registered as installed for the provided feature and namespace or can register it if provided. * <p> * Only look at provide namespace and does take into account inheritance. * * @param feature the feature provided by the extension * @param namespace the namespace where the extension is installed * @param localExtension the extension * @return the installed extension informations */ private InstalledFeature addInstalledFeatureToCache(String feature, String namespace, DefaultInstalledExtension localExtension) { Map<String, InstalledFeature> installedExtensionsForFeature = this.extensionNamespaceByFeature.get(feature); if (installedExtensionsForFeature == null) { installedExtensionsForFeature = new HashMap<String, InstalledFeature>(); this.extensionNamespaceByFeature.put(feature, installedExtensionsForFeature); } InstalledFeature installedFeature = installedExtensionsForFeature.get(namespace); if (installedFeature == null) { InstalledRootFeature rootFeature; if (localExtension.getId().getId().equals(feature)) { rootFeature = new InstalledRootFeature(namespace); } else { rootFeature = getInstalledFeatureFromCache(localExtension.getId().getId(), namespace).root; } installedFeature = new InstalledFeature(rootFeature, feature); installedExtensionsForFeature.put(namespace, installedFeature); } if (localExtension.isValid(namespace)) { installedFeature.root.extension = localExtension; } else { installedFeature.root.invalidExtensions.add(localExtension); } return installedFeature; } /** * Get extension registered as installed for the provided feature and namespace (including on root namespace). * * @param feature the feature provided by the extension * @param namespace the namespace where the extension is installed * @return the installed extension informations */ private InstalledFeature getInstalledFeatureFromCache(String feature, String namespace) { if (feature == null) { return null; } Map<String, InstalledFeature> installedExtensionsForFeature = this.extensionNamespaceByFeature.get(feature); if (installedExtensionsForFeature == null) { return null; } InstalledFeature installedExtension = installedExtensionsForFeature.get(namespace); if (installedExtension == null && namespace != null) { installedExtension = getInstalledFeatureFromCache(feature, null); } return installedExtension; } // InstalledExtensionRepository @Override public InstalledExtension getInstalledExtension(String feature, String namespace) { InstalledFeature installedFeature = getInstalledFeatureFromCache(feature, namespace); if (installedFeature != null) { if (installedFeature.root.extension != null) { return installedFeature.root.extension; } return installedFeature.root.invalidExtensions.isEmpty() ? null : installedFeature.root.invalidExtensions.iterator().next(); } return null; } @Override public InstalledExtension installExtension(LocalExtension extension, String namespace, boolean dependency, Map<String, Object> properties) throws InstallException { DefaultInstalledExtension installedExtension = this.extensions.get(extension.getId()); if (installedExtension != null && installedExtension.isInstalled(namespace)) { if (installedExtension.isDependency() == dependency) { throw new InstallException( String.format("The extension [%s] is already installed on namespace [%s]", installedExtension, namespace)); } installedExtension.setDependency(dependency, namespace); try { this.localRepository.setProperties(installedExtension.getLocalExtension(), installedExtension.getProperties()); } catch (Exception e) { throw new InstallException("Failed to modify extension descriptor", e); } } else { LocalExtension localExtension = this.localRepository.getLocalExtension(extension.getId()); if (localExtension == null) { // Should be a very rare use case since we explicitly ask for a LocalExtension throw new InstallException(String.format("The extension [%s] need to be stored first", extension)); } if (installedExtension == null) { installedExtension = new DefaultInstalledExtension(localExtension, this); } applyInstallExtension(installedExtension, namespace, dependency, properties); } return installedExtension; } @Override public void uninstallExtension(InstalledExtension extension, String namespace) throws UninstallException { DefaultInstalledExtension installedExtension = (DefaultInstalledExtension) getInstalledExtension( extension.getId().getId(), namespace); if (installedExtension != null) { applyUninstallExtension(installedExtension, namespace); } } private void applyUninstallExtension(DefaultInstalledExtension installedExtension, String namespace) throws UninstallException { installedExtension.setInstalled(false, namespace); try { this.localRepository.setProperties(installedExtension.getLocalExtension(), installedExtension.getProperties()); } catch (Exception e) { throw new UninstallException("Failed to modify extension descriptor", e); } // Clean caches removeInstalledExtension(installedExtension, namespace); } @Override public Collection<InstalledExtension> getBackwardDependencies(String feature, String namespace) throws ResolveException { if (getInstalledExtension(feature, namespace) == null) { throw new ResolveException( String.format("Extension [%s] is not installed on namespace [%s]", feature, namespace)); } Map<String, InstalledFeature> installedExtensionsByFeature = this.extensionNamespaceByFeature.get(feature); if (installedExtensionsByFeature != null) { InstalledFeature installedExtension = installedExtensionsByFeature.get(namespace); if (installedExtension != null) { Set<DefaultInstalledExtension> backwardDependencies = installedExtension.root.backwardDependencies; // copy the list to allow use cases like uninstalling all backward dependencies without getting a // concurrent issue on the list return backwardDependencies != null ? new ArrayList<InstalledExtension>(backwardDependencies) : Collections.<InstalledExtension>emptyList(); } } return Collections.emptyList(); } @Override public Map<String, Collection<InstalledExtension>> getBackwardDependencies(ExtensionId extensionId) throws ResolveException { Map<String, Collection<InstalledExtension>> result; DefaultInstalledExtension installedExtension = resolve(extensionId); Collection<String> namespaces = installedExtension.getNamespaces(); Map<String, InstalledFeature> featureExtensions = this.extensionNamespaceByFeature .get(installedExtension.getId().getId()); if (featureExtensions != null) { result = new HashMap<String, Collection<InstalledExtension>>(); for (InstalledFeature festureExtension : featureExtensions.values()) { if ((namespaces == null || namespaces.contains(festureExtension.root.namespace)) && !festureExtension.root.backwardDependencies.isEmpty()) { // copy the list to allow use cases like uninstalling all backward dependencies without getting a // concurrent issue on the list result.put(festureExtension.root.namespace, new ArrayList<InstalledExtension>(festureExtension.root.backwardDependencies)); } } } else { result = Collections.emptyMap(); } return result; } }