/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* 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:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.project.server.type;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.model.project.type.Attribute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Registry for Project Type definitions on server
* <p>All the Project Types definitions should be registered here
*
* @author gazarenkov
*/
@Singleton
public class ProjectTypeRegistry {
private static final Logger LOG = LoggerFactory.getLogger(ProjectTypeRegistry.class);
public static final ProjectTypeDef BASE_TYPE = new BaseProjectType();
public static final ChildToParentComparator CHILD_TO_PARENT_COMPARATOR = new ChildToParentComparator();
private static final Pattern NAME_PATTERN = Pattern.compile("[^a-zA-Z0-9-_.]");
private final Map<String, ProjectTypeDef> projectTypes;
private final Map<String, ProjectTypeDef> validatedData;
/**
* Initialises Set of Project Type definitions
*
* @param types
*/
@Inject
public ProjectTypeRegistry(Set<ProjectTypeDef> types) {
projectTypes = new HashMap<>();
validatedData = new HashMap<>();
validate(types);
for (ProjectTypeDef type : validatedData.values()) {
try {
init(type);
} catch (ProjectTypeConstraintException e) {
LOG.error(e.getMessage());
}
}
}
/**
* Registers single projectType
* May be deprecated in future
*
* @param projectType
* @throws ProjectTypeConstraintException
*/
public void registerProjectType(ProjectTypeDef projectType) throws ProjectTypeConstraintException {
if (isNameValid(projectType) && isParentValid(projectType, validatedData)) {
validatedData.put(projectType.getId(), projectType);
init(projectType);
}
}
/**
* @param id
* @return project type by id
*/
public ProjectTypeDef getProjectType(String id) throws NotFoundException {
final ProjectTypeDef pt = projectTypes.get(id);
if (pt == null) {
throw new NotFoundException("Project Type not found: " + id);
}
return pt;
}
/**
* @return all project types
*/
public Collection<ProjectTypeDef> getProjectTypes() {
return projectTypes.values();
}
/**
* @param comparator
* @return all project types sorted with specified comparator
* @see ProjectTypeRegistry.ChildToParentComparator
*/
public List<ProjectTypeDef> getProjectTypes(Comparator<ProjectTypeDef> comparator) {
List<ProjectTypeDef> list = projectTypes.values().stream().collect(Collectors.toList());
Collections.sort(list, comparator);
return list;
}
/**
* project type comparator which sorts collection of project types in child-to-parent order
*/
public static class ChildToParentComparator implements Comparator<ProjectTypeDef> {
@Override
public int compare(ProjectTypeDef o1, ProjectTypeDef o2) {
if (o1.isTypeOf(o2.getId())) {
return -1;
}
if (o2.isTypeOf(o1.getId())) {
return 1;
}
return 0;
}
}
/**
* Validates incoming set of Project Type definitions
* and forms preliminary collection of validated data to be initialized
*
* @param types
*/
protected final void validate(Collection<? extends ProjectTypeDef> types) {
Map<String, ProjectTypeDef> pass1 = new HashMap<>();
if (!types.contains(BASE_TYPE)) {
pass1.put(BASE_TYPE.getId(), BASE_TYPE);
}
types.stream()
.filter(this::isNameValid)
.forEach(type -> pass1.put(type.getId(), type));
// look for parents
pass1.values()
.stream()
.filter(type -> isParentValid(type, pass1))
.forEach(type -> validatedData.put(type.getId(), type));
}
/**
* Checks if incoming Project Type definition has valid ID (Pattern.compile("[^a-zA-Z0-9-_.]")
* and display name (should not be null or empty)
*
* @param type
* @return true if valid
*/
private boolean isNameValid(ProjectTypeDef type) {
boolean valid = true;
if (type.getId() == null || type.getId().isEmpty() || NAME_PATTERN.matcher(type.getId()).find()) {
LOG.error("Could not register Project Type ID is null or invalid (only Alphanumeric, dash, point and underscore allowed): "
+ type.getClass().getName());
valid = false;
}
if (type.getDisplayName() == null || type.getDisplayName().isEmpty()) {
LOG.error("Could not register Project Type with null or empty display name: " + type.getId());
valid = false;
}
for (Attribute attr : type.getAttributes()) {
// ID spelling (no spaces, only alphanumeric)
if (NAME_PATTERN.matcher(attr.getName()).find()) {
LOG.error("Could not register Project Type with invalid attribute Name (only Alphanumeric, dash and underscore allowed): " +
attr.getClass().getName() + " ID: '" + attr.getId() + "'");
valid = false;
}
}
return valid;
}
/**
* Validates if Project Tye definition has parent names which are already registered
*
* @param type
* @param pass1
* @return
*/
private boolean isParentValid(ProjectTypeDef type, Map<String, ProjectTypeDef> pass1) {
boolean contains = true;
for (String parent : type.getParents()) {
if (!pass1.keySet().contains(parent)) {
LOG.error("Could not register Project Type: " + type.getId() + " : Unregistered parent Type: " + parent);
contains = false;
}
}
// add Base Type as a parent if not pointed
if (type.getParents() != null && type.getParents().isEmpty() && !type.getId().equals(BASE_TYPE.getId())) {
type.addParent(BASE_TYPE.getId());
}
return contains;
}
/**
* validates and initializes concrete project type
*
* @param type
* @throws ProjectTypeConstraintException
*/
protected final void init(ProjectTypeDef type) throws ProjectTypeConstraintException {
initRecursively(type, type.getId());
if (!type.factoriesToOverride.isEmpty()) {
overrideFactories(type);
}
this.projectTypes.put(type.getId(), type);
LOG.debug("Project Type registered: " + type.getId());
}
/**
* initializes all the attributes defined in myType and its ancestors recursively
*
* @param myType
* @param typeId
* temporary type for recursive (started with initial type)
* @throws ProjectTypeConstraintException
*/
private final void initRecursively(ProjectTypeDef myType, String typeId) throws ProjectTypeConstraintException {
ProjectTypeDef type = validatedData.get(typeId);
for (String superTypeId : type.getParents()) {
myType.addAncestor(superTypeId);
ProjectTypeDef supertype = validatedData.get(superTypeId);
for (Attribute attr : supertype.getAttributes()) {
// check attribute names
for (Attribute attr2 : myType.getAttributes()) {
if (attr.getName().equals(attr2.getName()) && !attr.getProjectType().equals(attr2.getProjectType())) {
throw new ProjectTypeConstraintException("Attribute name conflict. Project type " +
myType.getId() + " could not be registered as attribute declaration " +
attr.getName() +
" is duplicated in its ancestor(s).");
}
}
myType.addAttributeDefinition(attr);
}
initRecursively(myType, superTypeId);
}
}
private final void overrideFactories(ProjectTypeDef myType) throws ProjectTypeConstraintException {
for (Map.Entry<String, ValueProviderFactory> entry : myType.factoriesToOverride.entrySet()) {
Attribute old = myType.getAttribute(entry.getKey());
if (old == null || !old.isVariable()) {
throw new ProjectTypeConstraintException("Can not override Value Provider Factory. Variable not defined: "
+ myType.getId() + ":" + entry.getKey());
}
myType.attributes.put(old.getName(), new Variable(old.getId(),
old.getName(),
old.getDescription(),
old.isRequired(),
entry.getValue()));
}
}
}