/* * Copyright (C) 2014 Red Hat, Inc. and/or its affiliates. * * 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. */ package org.jboss.errai.forge.facet.aggregate; import org.jboss.errai.forge.config.ProjectConfig; import org.jboss.errai.forge.config.ProjectProperty; import org.jboss.errai.forge.config.SerializableSet; import org.jboss.errai.forge.facet.base.AbstractBaseFacet; import org.jboss.forge.addon.facets.Facet; import org.jboss.forge.addon.facets.MutableFacet; import org.jboss.forge.addon.facets.MutableFaceted; import org.jboss.forge.addon.facets.constraints.FacetConstraint; import org.jboss.forge.addon.projects.Project; import org.jboss.forge.addon.projects.ProjectFacet; import javax.inject.Inject; import java.util.*; /** * Acts as top-level aggregator for pulling in other facet dependencies. * Concrete subclasses should be used to simplify the process of installing * complex combinations of facets. * * @author Max Barkley <mbarkley@redhat.com> */ public abstract class BaseAggregatorFacet implements ProjectFacet, MutableFacet<Project> { @Override public void setFaceted(Project origin) { project = origin; } protected Project project; @Override public Project getFaceted() { return project; } @SuppressWarnings("serial") public static class UninstallationExecption extends Exception { private UninstallationExecption(final Class<? extends ProjectFacet> dependentFacetType, final Project project, final BaseAggregatorFacet toUninstall) { super(generateMessage(dependentFacetType, project, toUninstall)); } private static String generateMessage(final Class<? extends ProjectFacet> facetType, final Project project, final BaseAggregatorFacet toUninstall) { if (BaseAggregatorFacet.class.isAssignableFrom(facetType) && project.hasFacet(facetType)) { final BaseAggregatorFacet facet = BaseAggregatorFacet.class.cast(project.getFacet(facetType)); return String.format("%s (%s) still requires %s.", facet.getFeatureName(), facet.getShortName(), toUninstall.getFeatureName()); } else { return String.format("The facet %s still requires %s.", facetType.getSimpleName(), toUninstall.getFeatureName()); } } } @Inject private AggregatorFacetReflections reflections; @Override public boolean install() { return true; } protected Project getProject() { return project; } @SuppressWarnings("unchecked") @Override public boolean isInstalled() { /* * An aggregator facet is installed if all of its required facets are * installed. There is no need to do a recursive traversal, as the presence * of direct dependencies in the project means that forge has already * verified the installation of transitively required facets. */ @SuppressWarnings("rawtypes") final Class<? extends Facet>[] constraints = getClass().getAnnotation(FacetConstraint.class).value(); for (int i = 0; i < constraints.length; i++) { if (!getProject().hasFacet((Class<? extends ProjectFacet>) constraints[i])) return false; } return true; } @Override public boolean uninstall() { return true; } /** * Uninstall this facet and all required facets which will not be otherwise * required after this facet is removed. * * @return True on successful uninstallation. * @throws UninstallationExecption * Thrown if this class is still required by another facet. */ @SuppressWarnings("unchecked") public boolean uninstallRequirements() throws UninstallationExecption, IllegalStateException { final ProjectConfig config = getProject().getFacet(ProjectConfig.class); final SerializableSet installedFeatureNames = config.getProjectProperty(ProjectProperty.INSTALLED_FEATURES, SerializableSet.class); final Set<Class<? extends ProjectFacet>> directlyInstalled = new HashSet<Class<? extends ProjectFacet>>(); for (final String featureName : installedFeatureNames) { directlyInstalled.add(reflections.getFeatureShort(featureName).getFeatureClass()); } directlyInstalled.remove(getClass()); directlyInstalled.add(CoreFacet.class); directlyInstalled.addAll(Arrays.asList(CoreFacet.coreFacets)); final Set<Class<? extends ProjectFacet>> toUninstall = traverseUninstallable(directlyInstalled); keepRequired(directlyInstalled, toUninstall); for (final Class<? extends ProjectFacet> facetType : toUninstall) { if (getProject().hasFacet(facetType)) { if (getProject() instanceof MutableFaceted) ((MutableFaceted<ProjectFacet>) getProject()).uninstall(getProject().getFacet(facetType)); else throw new IllegalStateException(String.format( "Cannot uninstall facets from project type %s that does not implement %s", getProject().getClass() .getCanonicalName(), MutableFaceted.class.getCanonicalName())); } } return true; } /** * Traverse the required facets of the featureClasses, and remove all of the * traversed facets from the removable set. * * @throws UninstallationExecption * Thrown if this feature is still required by another facet. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private void keepRequired(final Collection<Class<? extends ProjectFacet>> featureClasses, final Set<Class<? extends ProjectFacet>> removable) throws UninstallationExecption { final Set<Class<? extends ProjectFacet>> traversed = new HashSet<Class<? extends ProjectFacet>>(); final Queue<Class<? extends ProjectFacet>> toVisit = new LinkedList<Class<? extends ProjectFacet>>(); toVisit.addAll(featureClasses); while (!toVisit.isEmpty()) { final Class<? extends ProjectFacet> cur = toVisit.poll(); if (!traversed.contains(cur)) { traversed.add(cur); if (cur.isAnnotationPresent(FacetConstraint.class)) { final Class<? extends Facet>[] requirements = cur.getAnnotation(FacetConstraint.class).value(); for (int i = 0; i < requirements.length; i++) { if (!traversed.contains(requirements[i])) { // Some other feature still depends on this class... if (requirements[i].equals(getClass())) throw new UninstallationExecption(cur, getProject(), this); toVisit.add((Class<? extends ProjectFacet>) requirements[i]); removable.remove(requirements[i]); } } } } } } /** * Traverse the required facets of this class and add them to collection. But * ignore required facets in the intentionally installed facet. */ @SuppressWarnings({ "unchecked", "rawtypes" }) private Set<Class<? extends ProjectFacet>> traverseUninstallable( final Set<Class<? extends ProjectFacet>> intentionallyInstalled) { final Set<Class<? extends ProjectFacet>> traversed = new HashSet<Class<? extends ProjectFacet>>(); final Queue<Class<? extends ProjectFacet>> toVisit = new LinkedList<Class<? extends ProjectFacet>>(); toVisit.add((Class<? extends ProjectFacet>) getClass()); while (!toVisit.isEmpty()) { final Class<? extends ProjectFacet> cur = toVisit.poll(); if (!traversed.contains(cur) && !intentionallyInstalled.contains(cur) // Only add errai facets to be uninstalled && (BaseAggregatorFacet.class.isAssignableFrom(cur) || AbstractBaseFacet.class.isAssignableFrom(cur))) { traversed.add(cur); if (cur.isAnnotationPresent(FacetConstraint.class)) { final Class<? extends Facet>[] requirements = cur.getAnnotation(FacetConstraint.class).value(); for (int i = 0; i < requirements.length; i++) { if (!traversed.contains(requirements[i])) { toVisit.add((Class<? extends ProjectFacet>) requirements[i]); } } } } } return traversed; } /** * @return The name of the feature managed by this facet. */ public abstract String getFeatureName(); /** * @return The short name of the feature managed by this facet, used for * referencing it through the shell. */ public abstract String getShortName(); /** * @return A short description of the feature managed by this facet. */ public abstract String getFeatureDescription(); }