uriPrefixPair = resolveToUriAndPrefix(parentPrefixedName);
qname = QName.createQName(uriPrefixPair.getFirst(), parentLocalName);
classDefinition = (isAspect) ? dictionaryService.getAspect(qname) : dictionaryService.getType(qname);
}
if (classDefinition == null)
{
String msgId = (isAspect) ? "cmm.rest_api.aspect_parent_not_exist" : "cmm.rest_api.type_parent_not_exist";
throw new ConstraintViolatedException(I18NUtil.getMessage(msgId, parentPrefixedName));
}
else
{
checkCircularDependency(classDefinition.getModel(), existingModel, parentPrefixedName);
}
}
/**
* Validates models circular dependencies
* E.g. if {@literal B -> A} denotes model B depends on model A, then {@link ConstraintViolatedException} must be thrown for following:
*
if {@literal B -> A}, then {@literal A -> B} must throw exception
* if {@literal B -> A} and {@literal C -> B}, then {@literal A -> C} must throw exception
* if {@literal B -> A} and {@literal C -> B} and {@literal D -> C}, then {@literal A -> D} must throw exception
* @param modelDefinition the model which has a reference to the model containing the {@code parentPrefixedName}
* @param existingModel the model being updated
* @param parentPrefixedName the type/aspect parent name
*/
private void checkCircularDependency(ModelDefinition modelDefinition, CustomModel existingModel, String parentPrefixedName)
{
for (NamespaceDefinition importedNamespace : modelDefinition.getImportedNamespaces())
{
ModelDefinition md = null;
if ((md = customModelService.getCustomModelByUri(importedNamespace.getUri())) != null)
{
if (existingModel.getNamespaceUri().equals(importedNamespace.getUri()))
{
String msg = I18NUtil.getMessage("cmm.rest_api.circular_dependency_err", parentPrefixedName, existingModel.getName());
throw new ConstraintViolatedException(msg);
}
checkCircularDependency(md, existingModel, parentPrefixedName);
}
}
}
/**
* Returns the qualified name of the following format
* prefix:localName
, as a pair of (prefix, localName)
*
* @param prefixedQName the prefixed name. E.g. prefix:localName
* @return {@link Pair} of (prefix, localName)
*/
private Pair splitPrefixedQName(String prefixedQName)
{
// index 0 => prefix and index 1 => local name
String[] prefixLocalName = QName.splitPrefixedQName(prefixedQName);
if (NamespaceService.DEFAULT_PREFIX.equals(prefixLocalName[0]))
{
throw new InvalidArgumentException("cmm.rest_api.prefixed_qname_invalid_format", new Object[] { prefixedQName });
}
return new Pair(prefixLocalName[0], prefixLocalName[1]);
}
private CustomModelDefinition updateModel(ModelDetails modelDetails, String errorMsg)
{
M2Model m2Model = convertToM2Model(modelDetails);
try
{
CustomModelDefinition modelDef = customModelService.updateCustomModel(modelDetails.getModel().getName(), m2Model, modelDetails.isActive());
return modelDef;
}
catch (CustomModelConstraintException mce)
{
throw new ConstraintViolatedException(mce.getMessage());
}
catch (InvalidCustomModelException iex)
{
throw new InvalidArgumentException(iex.getMessage());
}
catch (Exception ex)
{
if (ex.getMessage() != null)
{
errorMsg = ex.getMessage();
}
throw new ApiException(errorMsg, ex);
}
}
private void mergeProperties(AbstractClassModel existingDetails, AbstractClassModel newDetails, Parameters parameters, boolean isModelActive)
{
validateList(newDetails.getProperties(), "cmm.rest_api.properties_empty_null");
// Transform existing properties into a map
Map existingProperties = transformToMap(existingDetails.getProperties(), toNameFunction());
// Transform new properties into a map
Map newProperties = transformToMap(newDetails.getProperties(), toNameFunction());
String propName = parameters.getParameter(PARAM_UPDATE_PROP);
// (propName == null) => property create request
if (propName == null)
{
// As this is a create request, check for duplicate properties
for (String name : newProperties.keySet())
{
if (existingProperties.containsKey(name))
{
throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.property_create_name_already_in_use", name));
}
}
}
else
{// Update request
CustomModelProperty existingProp = existingProperties.get(propName);
if (existingProp == null)
{
throw new EntityNotFoundException(propName);
}
CustomModelProperty modifiedProp = newProperties.get(propName);
if (modifiedProp == null)
{
throw new InvalidArgumentException("cmm.rest_api.property_update_prop_not_found", new Object[] { propName });
}
existingProp.setTitle(modifiedProp.getTitle());
existingProp.setDescription(modifiedProp.getDescription());
existingProp.setDefaultValue(modifiedProp.getDefaultValue());
existingProp.setConstraintRefs(modifiedProp.getConstraintRefs());
existingProp.setConstraints(modifiedProp.getConstraints());
if (isModelActive)
{
validateActivePropertyUpdate(existingProp, modifiedProp);
}
existingProp.setDataType(modifiedProp.getDataType());
existingProp.setMandatory(modifiedProp.isMandatory());
existingProp.setMandatoryEnforced(modifiedProp.isMandatoryEnforced());
existingProp.setMultiValued(modifiedProp.isMultiValued());
}
// Override properties
existingProperties.putAll(newProperties);
existingDetails.setProperties(new ArrayList<>(existingProperties.values()));
}
/**
* A helper method to throw a more informative exception (for an active model) rather than depending on the
* {@link org.alfresco.repo.dictionary.ModelValidatorImpl#validateModel}
* generic exception.
*/
private void validateActivePropertyUpdate(CustomModelProperty existingProp, CustomModelProperty newProp)
{
if (!StringUtils.equals(existingProp.getDataType(), newProp.getDataType()))
{
throw new ConstraintViolatedException("cmm.rest_api.property_change_datatype_err");
}
if (existingProp.isMandatory() != newProp.isMandatory())
{
throw new ConstraintViolatedException("cmm.rest_api.property_change_mandatory_opt_err");
}
if (existingProp.isMandatoryEnforced() != newProp.isMandatoryEnforced())
{
throw new ConstraintViolatedException("cmm.rest_api.property_change_mandatory_enforced_opt_err");
}
if (existingProp.isMultiValued() != newProp.isMultiValued())
{
throw new ConstraintViolatedException("cmm.rest_api.property_change_multi_valued_opt_err");
}
}
private void deleteProperty(AbstractClassModel existingClassModel, String propertyName)
{
// Transform existing properties into a map
Map existingProperties = transformToMap(existingClassModel.getProperties(), toNameFunction());
if (!existingProperties.containsKey(propertyName))
{
throw new EntityNotFoundException(propertyName);
}
existingProperties.remove(propertyName);
existingClassModel.setProperties(new ArrayList<>(existingProperties.values()));
}
private void validateList(List> list, String errorMsg)
{
if (CollectionUtils.isEmpty(list))
{
throw new InvalidArgumentException(errorMsg);
}
}
private static Map transformToMap(Collection collection, Function super V, K> function)
{
Map map = new HashMap<>(collection.size());
for (V item : collection)
{
map.put(function.apply(item), item);
}
return map;
}
private static Map removeRightEntries(Map leftMap, Map rightMap)
{
Map result = new HashMap<>(leftMap);
for (K key : rightMap.keySet())
{
result.remove(key);
}
return result;
}
private void validateTypeAspectDelete(Collection extends AbstractClassModel> list, String classPrefixedName)
{
for(AbstractClassModel acm : list)
{
if(classPrefixedName.equals(acm.getParentName()))
{
throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.aspect_type_cannot_delete", classPrefixedName, acm.getPrefixedName()));
}
}
}
private boolean hasSelectProperty(Parameters parameters, String param)
{
return parameters.getSelectedProperties().contains(param);
}
private static Function toNameFunction()
{
return new Function()
{
@Override
public String apply(AbstractCommonDetails details)
{
return details.getName();
}
};
}
private T getObjectByName(Collection collection, String name)
{
for (T details : collection)
{
if (details.getName().equals(name))
{
return details;
}
}
return null;
}
public class ModelDetails
{
private CustomModel model;
private boolean active;
private List types;
private List aspects;
private List modelDefinedConstraints;
public ModelDetails(CustomModelDefinition modelDefinition)
{
this.model = new CustomModel(modelDefinition);
this.active = modelDefinition.isActive();
this.types = convertToCustomTypes(modelDefinition.getTypeDefinitions(), false);
this.aspects = convertToCustomAspects(modelDefinition.getAspectDefinitions(), false);
this.modelDefinedConstraints = convertToCustomModelConstraints(modelDefinition.getModelDefinedConstraints());
}
public CustomModel getModel()
{
return this.model;
}
public void setModel(CustomModel model)
{
this.model = model;
}
public List getTypes()
{
return this.types;
}
public void setTypes(List types)
{
this.types = types;
}
public List getAspects()
{
return this.aspects;
}
public void setAspects(List aspects)
{
this.aspects = aspects;
}
public List getModelDefinedConstraints()
{
return this.modelDefinedConstraints;
}
public void setModelDefinedConstraints(List modelDefinedConstraints)
{
this.modelDefinedConstraints = modelDefinedConstraints;
}
public boolean isActive()
{
return this.active;
}
}
/**
* Constraint validator
*
* @author Jamal Kaabi-Mofrad
*/
public enum ConstraintValidator
{
REGEX
{
@Override
public void validate(String parameterName, String value)
{
if ("expression".equals(parameterName))
{
try
{
Pattern.compile(value);
}
catch (Exception ex)
{
throw new InvalidArgumentException("cmm.rest_api.regex_constraint_invalid_expression", new Object[] { value });
}
}
}
},
MINMAX
{
@Override
public void validate(String parameterName, String value)
{
double parsedValue;
try
{
parsedValue = Double.parseDouble(value);
}
catch (Exception ex)
{
throw new InvalidArgumentException("cmm.rest_api.minmax_constraint_invalid_parameter", new Object[] { value, parameterName });
}
// SHA-1126. We check for the Double.MIN_VALUE to be consistent with NumericRangeConstraint.minValue
if("maxValue".equalsIgnoreCase(parameterName) && parsedValue < Double.MIN_VALUE)
{
throw new InvalidArgumentException("cmm.rest_api.minmax_constraint_invalid_max_value");
}
}
@Override
public void validateUsage(QName propDataType)
{
if (propDataType != null && !(DataTypeDefinition.INT.equals(propDataType)
|| DataTypeDefinition.LONG.equals(propDataType)
|| DataTypeDefinition.FLOAT.equals(propDataType)
|| DataTypeDefinition.DOUBLE.equals(propDataType)))
{
throw new InvalidArgumentException("cmm.rest_api.minmax_constraint_invalid_use");
}
}
},
LENGTH
{
@Override
public void validate(String parameterName, String value)
{
try
{
Integer.parseInt(value);
}
catch (Exception ex)
{
throw new InvalidArgumentException("cmm.rest_api.length_constraint_invalid_parameter", new Object[] { value, parameterName });
}
}
@Override
public void validateUsage(QName propDataType)
{
if (propDataType != null && !(DataTypeDefinition.TEXT.equals(propDataType)
|| DataTypeDefinition.MLTEXT.equals(propDataType)
|| DataTypeDefinition.CONTENT.equals(propDataType)))
{
throw new InvalidArgumentException("cmm.rest_api.length_constraint_invalid_use");
}
}
},
DUMMY_CONSTRAINT
{
@Override
public void validate(String parameterName, String value)
{
// nothing to do
}
};
public abstract void validate(String parameterName, String value);
public void validateUsage(QName propDataType)
{
return; // nothing to do
}
public static ConstraintValidator findByType(String constraintType)
{
for (ConstraintValidator c : values())
{
if (c.name().equals(constraintType))
{
return c;
}
}
return DUMMY_CONSTRAINT;
}
}
@Override
public CustomModel createCustomModel(M2Model m2Model)
{
// Check the current user is authorised to import the custom model
validateCurrentUser();
validateImportedM2Model(m2Model);
CompiledModel compiledModel = null;
try
{
compiledModel = customModelService.compileModel(m2Model);
}
catch (CustomModelConstraintException mce)
{
throw new ConstraintViolatedException(mce.getMessage());
}
catch (InvalidCustomModelException iex)
{
throw new InvalidArgumentException(iex.getMessage());
}
ModelDefinition modelDefinition = compiledModel.getModelDefinition();
CustomModel customModel = new CustomModel();
customModel.setName(modelDefinition.getName().getLocalName());
customModel.setAuthor(modelDefinition.getAuthor());
customModel.setDescription(modelDefinition.getDescription(dictionaryService));
customModel.setStatus(ModelStatus.DRAFT);
NamespaceDefinition nsd = modelDefinition.getNamespaces().iterator().next();
customModel.setNamespaceUri(nsd.getUri());
customModel.setNamespacePrefix(nsd.getPrefix());
List customTypes = convertToCustomTypes(compiledModel.getTypes(), false);
List customAspects = convertToCustomAspects(compiledModel.getAspects(), false);
List constraintDefinitions = CustomModelDefinitionImpl.removeInlineConstraints(compiledModel);
List customModelConstraints = convertToCustomModelConstraints(constraintDefinitions);
customModel.setTypes(customTypes);
customModel.setAspects(customAspects);
customModel.setConstraints(customModelConstraints);
return createCustomModelImpl(customModel, false);
}
private void validateImportedM2Model(M2Model m2Model)
{
List namespaces = m2Model.getNamespaces();
if (namespaces.size() > 1)
{
throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.model.import_namespace_multiple_found", namespaces.size()));
}
else if (namespaces.isEmpty())
{
throw new ConstraintViolatedException("cmm.rest_api.model.import_namespace_undefined");
}
checkUnsupportedModelElements(m2Model.getTypes());
checkUnsupportedModelElements(m2Model.getAspects());
}
private void checkUnsupportedModelElements(Collection extends M2Class> m2Classes)
{
for (M2Class cls : m2Classes)
{
if (cls.getAssociations().size() > 0)
{
throw new ConstraintViolatedException("cmm.rest_api.model.import_associations_unsupported");
}
if (cls.getPropertyOverrides().size() > 0)
{
throw new ConstraintViolatedException("cmm.rest_api.model.import_overrides_unsupported");
}
if (cls.getMandatoryAspects().size() > 0)
{
throw new ConstraintViolatedException("cmm.rest_api.model.import_mandatory_aspects_unsupported");
}
if(cls.getArchive() != null)
{
throw new ConstraintViolatedException("cmm.rest_api.model.import_archive_unsupported");
}
if(cls.getIncludedInSuperTypeQuery() != null)
{
throw new ConstraintViolatedException("cmm.rest_api.model.import_includedInSuperTQ_unsupported");
}
}
}
}