/******************************************************************************* * Copyright (c) 2010-2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.core.project; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.UUID; import org.apache.commons.lang.StringUtils; import org.eclipse.skalli.commons.CollectionUtils; import org.eclipse.skalli.commons.UUIDUtils; import org.eclipse.skalli.model.ByProjectIdComparator; import org.eclipse.skalli.model.EntityBase; import org.eclipse.skalli.model.ExtensibleEntityBase; import org.eclipse.skalli.model.ExtensionEntityBase; import org.eclipse.skalli.model.Issue; import org.eclipse.skalli.model.Member; import org.eclipse.skalli.model.Project; import org.eclipse.skalli.model.ProjectNature; import org.eclipse.skalli.model.Severity; import org.eclipse.skalli.model.ValidationException; import org.eclipse.skalli.model.ext.commons.PeopleExtension; import org.eclipse.skalli.services.entity.EntityServiceBase; import org.eclipse.skalli.services.extension.ExtensionService; import org.eclipse.skalli.services.extension.ExtensionServices; import org.eclipse.skalli.services.extension.ExtensionValidator; import org.eclipse.skalli.services.extension.PropertyValidator; import org.eclipse.skalli.services.persistence.PersistenceService; import org.eclipse.skalli.services.project.InvalidParentChainException; import org.eclipse.skalli.services.project.ProjectService; import org.eclipse.skalli.services.role.RoleProvider; import org.eclipse.skalli.services.template.NoSuchTemplateException; import org.eclipse.skalli.services.template.ProjectTemplate; import org.eclipse.skalli.services.template.ProjectTemplateService; import org.eclipse.skalli.services.user.UserServices; import org.osgi.service.component.ComponentConstants; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ProjectComponent extends EntityServiceBase<Project> implements ProjectService { private static final Logger LOG = LoggerFactory.getLogger(ProjectComponent.class); // TODO "central" versioning together with extensibility sucks! /* * Alternative idea: Instead of a version number just use unique IDs. A * migration then defines a set of other migration's ids which have to be * applied as a prerequisite. The Migrator then first collects all registered * migrations an then sorts them by their prerequisites. Hypothetis: A * migration logically cannot depend on another migration if the unique id of * that migration is not known. Open issues: - What should be used as a * "version" information in the persisted xml? => Maybe something like a * checksum (must be stable with respect to the order of calculation) */ private static final int CURRENT_MODEL_VERISON = 23; private ProjectTemplateService projectTemplateService; private Set<RoleProvider> roleProviders = new HashSet<RoleProvider>(); protected void activate(ComponentContext context) { LOG.info(MessageFormat.format("[ProjectService] {0} : activated", (String) context.getProperties().get(ComponentConstants.COMPONENT_NAME))); } protected void deactivate(ComponentContext context) { LOG.info(MessageFormat.format("[ProjectService] {0} : deactivated", (String) context.getProperties().get(ComponentConstants.COMPONENT_NAME))); } protected void bindProjectTemplateService(ProjectTemplateService projectTemplateService) { this.projectTemplateService = projectTemplateService; LOG.info(MessageFormat.format("bindProjectTemplateService({0})", projectTemplateService)); //$NON-NLS-1$ } protected void unbindProjectTemplateService(ProjectTemplateService projectTemplateService) { LOG.info(MessageFormat.format("unbindProjectTemplateService({0})", projectTemplateService)); //$NON-NLS-1$ this.projectTemplateService = null; } protected void bindRoleProvider(RoleProvider roleProvider) { roleProviders.add(roleProvider); } protected void unbindRoleProvider(RoleProvider roleProvider) { roleProviders.remove(roleProvider); } @Override public Class<Project> getEntityClass() { return Project.class; } @Override public int getModelVersion() { return CURRENT_MODEL_VERISON; } @Override public ProjectNature getProjectNature(UUID uuid) { ProjectTemplate projectTemplate = getProjectTemplate(uuid); return projectTemplate != null ? projectTemplate.getProjectNature() : ProjectNature.PROJECT; } private ProjectTemplate getProjectTemplate(UUID uuid) { Project project = getByUUID(uuid); if (project == null || projectTemplateService == null) { return null; } return projectTemplateService.getProjectTemplateById(project.getProjectTemplateId()); } @Override public Project getProject(String id) { return UUIDUtils.isUUID(id)? getByUUID(UUID.fromString(id)) : getProjectByProjectId(id); } @Override public Project loadProject(String id) { Project project = getProject(id); return project != null ? loadEntity(Project.class, project.getUuid()) : null; } @Override public Project getProjectByProjectId(String projectId) { for (Project p : getAll()) { if (p.getProjectId().equalsIgnoreCase(projectId)) { return p; } } return null; } @Override public List<Project> getProjects(Comparator<Project> c) { List<Project> projects = getAll(); if (c != null) { Collections.sort(projects, c); } return projects; } @Override public List<Project> getProjects(List<UUID> uuids) { List<Project> result = new ArrayList<Project>(); PersistenceService persistence = getPersistenceService(); for (UUID uuid : uuids) { Project project = persistence.getEntity(Project.class, uuid); if (project != null) { result.add(project); } } return result; } @Override public Map<UUID, List<Project>> getSubProjects() { Map<UUID, List<Project>> result = new HashMap<UUID, List<Project>>(); List<UUID> uuids = new ArrayList<UUID>(keySet()); for (UUID uuid : uuids) { Project project = getByUUID(uuid); if (project == null) { continue; } Project parent = project.getParentProject(); if (parent == null) { continue; } List<Project> subprojects = result.get(parent.getUuid()); if (subprojects == null) { subprojects = new ArrayList<Project>(); result.put(parent.getUuid(), subprojects); } subprojects.add(project); } return result; } @Override public SortedSet<Project> getSubProjects(UUID uuid) { return getSubProjects(uuid, null, 1); } @Override public SortedSet<Project> getSubProjects(UUID uuid, Comparator<Project> c) { return getSubProjects(uuid, c, 1); } @Override public SortedSet<Project> getSubProjects(UUID uuid, Comparator<Project> c, int depth) { depth = depth < 0 ? Integer.MAX_VALUE : depth; if (depth == 0) { return CollectionUtils.emptySortedSet(); } Project project = getByUUID(uuid); if (project == null) { return CollectionUtils.emptySortedSet(); } if (depth == 1) { return project.getSubProjects(c); } SortedSet<Project> subprojects = c != null ? new TreeSet<Project>(c) : new TreeSet<Project>(new ByProjectIdComparator()); addSubProjects(project, subprojects, 1, depth); return subprojects; } private void addSubProjects(EntityBase parent, SortedSet<Project> subprojects, int currentDepth, int depth) { if (currentDepth > depth) { return; } EntityBase next = (Project)parent.getFirstChild(); while (next != null) { subprojects.add((Project)next); addSubProjects(next, subprojects, currentDepth + 1, depth); next = next.getNextSibling(); } } @Override public List<Project> getParentChain(UUID uuid) { Project project = getByUUID(uuid); if (project == null) { project = getDeletedProject(uuid); if (project == null) { return Collections.emptyList(); } } return getParentChain(project); } private List<Project> getParentChain(Project project) { List<Project> result = new LinkedList<Project>(); result.add(project); UUID parentUUID = project.getParentEntityId(); while (parentUUID != null) { Project parent = getByUUID(parentUUID); if (parent == null) { parent = getDeletedProject(parentUUID); if (parent == null) { throw new InvalidParentChainException(project.getUuid(), parentUUID); } } result.add(parent); parentUUID = parent.getParentEntityId(); } return result; } @Override public Project getNearestParent(UUID uuid, ProjectNature nature) { UUID parentUUID = uuid; while (parentUUID != null) { Project parent = getByUUID(parentUUID); if (parent == null) { parent = getDeletedProject(parentUUID); if (parent == null) { throw new InvalidParentChainException(uuid, parentUUID); } } String templateId = parent.getProjectTemplateId(); ProjectTemplate template = projectTemplateService.getProjectTemplateById(templateId); if (template == null) { throw new NoSuchTemplateException(parentUUID, templateId); } if (nature.equals(template.getProjectNature())) { return parent; } parentUUID = parent.getParentEntityId(); } return null; } @Override public Set<UUID> deletedSet() { return getPersistenceService().deletedSet(Project.class); } @Override public List<Project> getDeletedProjects() { return getPersistenceService().getDeletedEntities(Project.class); } @Override public List<Project> getDeletedProjects(Comparator<Project> c) { List<Project> projects = getDeletedProjects(); Collections.sort(projects, c); return projects; } @Override public Project getDeletedProject(UUID uuid) { return getPersistenceService().getDeletedEntity(Project.class, uuid); } @Override public List<Project> getRootProjects(Comparator<Project> c) { List<Project> rootProjects = new ArrayList<Project>(); List<UUID> uuids = new ArrayList<UUID>(keySet()); for (UUID uuid : uuids) { Project project = getByUUID(uuid); if (project == null) { continue; } if (project.getParentEntityId() == null) { rootProjects.add(project); } } return rootProjects; } @Override protected void validateEntity(Project entity) throws ValidationException { SortedSet<Issue> issues = validate(entity, Severity.FATAL); if (issues.size() > 0) { throw new ValidationException("Project could not be saved due to the following reasons:", issues); } } /** * Validates the given project. * Checks basic data like project ID, template, project lead etc. * Furthermore, validates the project with the default property/extension validators provided by * <code>ExtensionServiceCore</code> and the extension services of the assigned extensions and * with the custom validators provided by the {@link ProjectTemplate project template}. */ @Override protected SortedSet<Issue> validateEntity(Project project, Severity minSeverity) { SortedSet<Issue> issues = new TreeSet<Issue>(); issues.addAll(validateProjectId(project)); issues.addAll(validateProjectName(project)); issues.addAll(validatePeopleExtension(project)); // ensure that the entity service exists UUID projectUUID = project.getUuid(); ExtensionService<?> extensionService = validateExtensionService(projectUUID, project, issues); // ensure that the project template exists ProjectTemplate projectTemplate = validateProjectTemplate(project, issues); if (extensionService != null && projectTemplate != null) { // use the validators provided by the project template/extension services to validate the project validateExtension(projectUUID, project, extensionService, projectTemplate, issues, minSeverity); // check that all extensions are compatible with the template and vice versa if (minSeverity.compareTo(Severity.ERROR) >= 0) { validateCompatibility(project, projectTemplate, extensionService, issues); } } return issues; } private SortedSet<Issue> validatePeopleExtension(Project project) { // ensure that the project has a PeopleProjectExt and a project lead SortedSet<Issue> issues = new TreeSet<Issue>(); PeopleExtension peopleExtension = project.getExtension(PeopleExtension.class); if (peopleExtension == null) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), PeopleExtension.class, null, "Project must have a Project Members extension or inherit it from a parent")); } else if (peopleExtension.getLeads().isEmpty()) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), PeopleExtension.class, PeopleExtension.PROPERTY_LEADS, "Project must have a least one Project Lead")); } return issues; } private SortedSet<Issue> validateProjectId(Project project) { SortedSet<Issue> issues = new TreeSet<Issue>(); String projectId = project.getProjectId(); if (StringUtils.isBlank(projectId)) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), Project.class, Project.PROPERTY_PROJECTID, 1, "Project must have a Project ID")); } else { if (projectId.trim().length() != projectId.length()) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), Project.class, Project.PROPERTY_PROJECTID, 2, "Project ID must not have leading or trailing whitespaces")); } else { for (Project anotherProject : getAll()) { String anotherProjectId = anotherProject.getProjectId(); if (projectId.equals(anotherProjectId) && !anotherProject.getUuid().equals(project.getUuid())) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), Project.class, Project.PROPERTY_PROJECTID, 3, MessageFormat.format("Project with Project ID ''{0}'' already exists", projectId))); break; } } } } return issues; } private SortedSet<Issue> validateProjectName(Project project) { SortedSet<Issue> issues = new TreeSet<Issue>(); String name = project.getName(); if (StringUtils.isBlank(name)) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), Project.class, Project.PROPERTY_NAME, "Projects must have a Display Name")); } return issues; } private ProjectTemplate validateProjectTemplate(Project project, Set<Issue> issues) { ProjectTemplate projectTemplate = projectTemplateService.getProjectTemplateById(project.getProjectTemplateId()); if (projectTemplate != null) { validateDirectParent(projectTemplate, project, issues); validateAllowedParents(projectTemplate, project, issues); } else { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), Project.class, Project.PROPERTY_TEMPLATEID, MessageFormat.format( "Project references project template ''{0}'' but such a template is not registered", project.getProjectTemplateId()))); } return projectTemplate; } private void validateDirectParent(ProjectTemplate projectTemplate, Project project, Set<Issue> issues) { UUID directParent = projectTemplate.getDirectParent(); if (directParent != null && !directParent.equals(project.getParentEntityId())) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), Project.class, Project.PROPERTY_PARENT_ENTITY_ID, MessageFormat.format( "Project assigned to project template ''{0}'' must be direct subproject of project ''{1}''", project.getProjectTemplateId(), directParent))); } } private void validateAllowedParents(ProjectTemplate projectTemplate, Project project, Set<Issue> issues) { Set<UUID> allowedParents = projectTemplate.getAllowedParents(); if (CollectionUtils.isNotBlank(allowedParents)) { boolean hasAllowedParent = false; for (Project parent: getParentChain(project.getUuid())) { if (allowedParents.contains(parent.getUuid())) { hasAllowedParent = true; break; } } if (!hasAllowedParent) { issues.add(new Issue(Severity.FATAL, ProjectService.class, project.getUuid(), Project.class, Project.PROPERTY_PARENT_ENTITY_ID, MessageFormat.format( "Project assigned to project template ''{0}'' must be subproject of any of ''{1}''", projectTemplate.getId(), CollectionUtils.toString(allowedParents, ',')))); } } } private ExtensionService<?> validateExtensionService(UUID projectUUID, ExtensionEntityBase ext, Set<Issue> issues) { Class<? extends ExtensionEntityBase> extensionClass = ext.getClass(); ExtensionService<?> extensionService = ExtensionServices.getByExtensionClass(extensionClass); if (extensionService == null) { issues.add(new Issue(Severity.FATAL, ProjectService.class, projectUUID, MessageFormat.format("Project references model extension ''{0}'' but there is no " + "corresponding extension service registered", extensionClass.getName()))); } return extensionService; } private void validateCompatibility(Project project, ProjectTemplate projectTemplate, ExtensionService<?> extensionService, Set<Issue> issues) { Set<String> allowed = extensionService.getProjectTemplateIds(); Set<String> included = projectTemplate.getIncludedExtensions(); Set<String> excluded = projectTemplate.getExcludedExtensions(); if (allowed != null || included != null || excluded != null) { UUID projectUUID = project.getUuid(); for (ExtensionEntityBase extension : project.getAllExtensions()) { String extensionClassName = extension.getClass().getName(); if (allowed != null && !allowed.contains(projectTemplate.getId())) { issues.add(new Issue( Severity.ERROR, ProjectTemplate.class, projectUUID, extension.getClass(), null, MessageFormat.format("{0} projects are not compatible with ''{1}'' extensions. " + "Disable the extension or select another project template.", projectTemplate.getDisplayName(), extensionClassName))); } if (excluded != null && excluded.contains(extensionClassName) || included != null && !included.contains(extensionClassName)) { issues.add(new Issue( Severity.ERROR, ProjectTemplate.class, projectUUID, extension.getClass(), null, MessageFormat.format("''{0}'' extensions are not appropriate for {1} projects. " + "Disable the extension or select another project template.", extensionClassName, projectTemplate.getDisplayName()))); } } } } private void validateExtension(UUID projectUUID, ExtensionEntityBase ext, ProjectTemplate projectTemplate, Set<Issue> issues, Severity minSeverity) { ExtensionService<?> extensionService = validateExtensionService(projectUUID, ext, issues); if (extensionService != null) { validateExtension(projectUUID, ext, extensionService, projectTemplate, issues, minSeverity); } } private void validateExtension(UUID projectUUID, ExtensionEntityBase ext, ExtensionService<?> extensionService, ProjectTemplate projectTemplate, Set<Issue> issues, Severity minSeverity) { String extensionClassName = extensionService.getExtensionClass().getName(); Map<String, String> captions = new HashMap<String, String>(); Set<String> propertyNames = ext.getPropertyNames(); for (String propertyName : propertyNames) { // determine a suitable caption for the property, either from the template or the // extension service; if neither is availablle, pass null to the validators String caption = projectTemplate.getCaption(extensionClassName, propertyName); if (StringUtils.isBlank(caption)) { caption = extensionService.getCaption(propertyName); } captions.put(propertyName, caption); List<PropertyValidator> propertyValidators = new ArrayList<PropertyValidator>(); List<PropertyValidator> defaultValidators = extensionService.getPropertyValidators(propertyName, caption); if (defaultValidators == null) { LOG.warn(MessageFormat.format( "{0}#getPropertyValidators({1}) returned null, but is expected to return an empty set", extensionService.getClass().getName(), propertyName)); } else { propertyValidators.addAll(defaultValidators); } List<PropertyValidator> customValidators = projectTemplate.getPropertyValidators(extensionClassName, propertyName); if (customValidators == null) { LOG.warn(MessageFormat.format( "{0}#getPropertyValidators({1}, {2}) returned null, but is expected to return an empty set", projectTemplate.getClass().getName(), extensionClassName, propertyName)); } else { propertyValidators.addAll(customValidators); } for (PropertyValidator propertyValidator : propertyValidators) { try { issues.addAll(propertyValidator.validate(projectUUID, ext.getProperty(propertyName), minSeverity)); } catch (RuntimeException e) { LOG.error(MessageFormat.format("{0}#validate on project {1} threw an exception", propertyValidator .getClass().getName(), projectUUID), e); } } } issues.addAll(validateExtensionServiceExtensionValidators(projectUUID, ext, extensionService, projectTemplate, minSeverity, captions)); issues.addAll(validateProjectTemplateExtensionValidators(projectUUID, ext, projectTemplate, minSeverity)); // if this extension is extensible, recursively validate all extensions if (ext instanceof ExtensibleEntityBase) { for (ExtensionEntityBase extension : ((ExtensibleEntityBase) ext).getAllExtensions()) { validateExtension(projectUUID, extension, projectTemplate, issues, minSeverity); } } } private Set<Issue> validateExtensionServiceExtensionValidators(UUID projectUUID, ExtensionEntityBase ext, ExtensionService<?> extensionService, ProjectTemplate projectTemplate, Severity minSeverity, Map<String, String> captions) { Set<Issue> issues = new TreeSet<Issue>(); List<? extends ExtensionValidator<?>> extensionValidators = extensionService.getExtensionValidators(captions); if (extensionValidators == null) { LOG.warn(MessageFormat.format( "{0}#getExtensionValidators() returned null, but is expected to return an empty set", projectTemplate.getClass().getName())); } else { for (ExtensionValidator<?> extensionValidator : extensionValidators) { try { issues.addAll(extensionValidator.validate(projectUUID, ext, minSeverity)); } catch (RuntimeException e) { LOG.error(MessageFormat.format("{0}#validate on project {1} threw an exception", extensionValidator .getClass().getName(), projectUUID), e); } } } return issues; } private Set<Issue> validateProjectTemplateExtensionValidators(UUID uuid, ExtensionEntityBase ext, ProjectTemplate projectTemplate, Severity minSeverity) { Set<Issue> issues = new TreeSet<Issue>(); List<ExtensionValidator<?>> extentionValidators = projectTemplate.getExtensionValidators(ext.getClass() .getName()); if (extentionValidators == null) { LOG.warn(MessageFormat.format( "{0}#getExtensionValidators({1}) returned null, but is expected to return an empty set", projectTemplate.getClass().getName(), ext.getClass().getName())); } else { for (ExtensionValidator<?> extensionValidator : extentionValidators) { try { issues.addAll(extensionValidator.validate(uuid, ext, minSeverity)); } catch (RuntimeException e) { LOG.error(MessageFormat.format("{0}#validate on project {1} threw an exception", extensionValidator .getClass().getName(), uuid), e); } } } return issues; } @Override public SortedSet<Member> getMembers(UUID uuid) { TreeSet<Member> ret = new TreeSet<Member>(); Project project = getByUUID(uuid); if (project != null) { for (RoleProvider roleProvider : roleProviders) { ret.addAll(roleProvider.getMembers(project)); } } return ret; } @Override public SortedSet<Member> getMembers(UUID uuid, String... roles) { TreeSet<Member> ret = new TreeSet<Member>(); Project project = getByUUID(uuid); if (project != null) { for (RoleProvider roleProvider : roleProviders) { ret.addAll(roleProvider.getMembers(project, roles)); } } return ret; } @Override public Map<String, SortedSet<Member>> getMembersByRole(UUID uuid) { Map<String, SortedSet<Member>> ret = new HashMap<String, SortedSet<Member>>(); Project project = getByUUID(uuid); if (project != null) { for (RoleProvider roleProvider : roleProviders) { ret.putAll(roleProvider.getMembersByRole(project)); } } return ret; } @Override public Project createProject(final String templateId, final String userId) { final Project project = (StringUtils.isNotBlank(templateId)) ? new Project(templateId) : new Project(); project.setUuid(UUID.randomUUID()); if (UserServices.getUser(userId) != null) { PeopleExtension peopleExt = new PeopleExtension(); peopleExt.getLeads().add(new Member(userId)); project.addExtension(peopleExt); } final ProjectTemplate template = projectTemplateService.getProjectTemplateById(templateId); if (template != null) { for (Class<? extends ExtensionEntityBase> extensionClass : projectTemplateService.getSelectableExtensions( template, null)) { String extensionClassName = extensionClass.getName(); if (template.isEnabled(extensionClassName)) { try { if (project.getExtension(extensionClass) == null) { project.addExtension(extensionClass.cast(extensionClass.newInstance())); } } catch (InstantiationException e) { LOG.warn(MessageFormat.format("Extension ''{0}'' could not be instantiated: {1}", extensionClassName, e.getMessage())); } catch (IllegalAccessException e) { LOG.warn(MessageFormat.format("Extension ''{0}'' could not be instantiated: {1}", extensionClassName, e.getMessage())); } } } } return project; } }