diff --git a/config/alfresco/messages/custommodel-restapi-messages.properties b/config/alfresco/messages/custommodel-restapi-messages.properties
new file mode 100644
index 0000000000..d66fdf8fab
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=Model name can't be null.
+cmm.rest_api.model_invalid=Invalid custom model.
+cmm.rest_api.model_status_null=Model status can't be null.
+cmm.rest_api.model_name_cannot_update=You can't update the model name.
+cmm.rest_api.model_update_failure=We couldn't update the model.
+cmm.rest_api.model_download_failure=We couldn't create a download node.
+cmm.rest_api.model_namespace_uri_null=The model namespace URI can't be null.
+cmm.rest_api.model_namespace_uri_invalid=The namespace URI is invalid. Use letters, numbers and URI characters only.
+cmm.rest_api.model_namespace_prefix_null=Model namespace prefix can't be null.
+# Type
+cmm.rest_api.type_name_null=Type name can't be null.
+cmm.rest_api.type_create_failure=We couldn't create the type.
+cmm.rest_api.type_update_failure=We couldn't update the type.
+cmm.rest_api.type_parent_cannot_update=You can't update the type's parent in an active custom model.
+cmm.rest_api.type_parent_not_exist=You can''t set the type''s parent to ''{0}'', because the type ''{0}'' doesn''t exist.
+cmm.rest_api.type_cannot_delete=You can't delete a type in an active model.
+cmm.rest_api.type_delete_failure=We couldn't delete the type.
+# Aspect
+cmm.rest_api.aspect_name_null=Aspect name can't be null.
+cmm.rest_api.aspect_create_failure=We couldn't create the aspect.
+cmm.rest_api.aspect_update_failure=We couldn't update the aspect.
+cmm.rest_api.aspect_parent_cannot_update=You can't update the aspect's parent in an active custom model.
+cmm.rest_api.aspect_parent_not_exist=You can''t set the aspect''s parent to ''{0}'', because the aspect ''{0}'' doesn''t exist.
+cmm.rest_api.aspect_cannot_delete=You can't delete an aspect in an active model.
+cmm.rest_api.aspect_delete_failure=We couldn't delete the aspect.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=You can''t delete ''{0}'' as ''{1}'' depends on it.
+# Constraint
+cmm.rest_api.constraint_name_null=Constraint name can't be null.
+cmm.rest_api.constraint_create_failure=We couldn't create constraint.
+cmm.rest_api.constraint_parameter_name_null=Constraint parameter name can't be null.
+cmm.rest_api.constraint_type_null=Custom constraint must have a 'type' attribute.
+cmm.rest_api.constraint_ref_not_defined=Constraint reference ''{0}'' isn'' t defined by this model.
+cmm.rest_api.regex_constraint_invalid_expression=REGEX expression ''{0}'' isn''t valid.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' isn''t a valid ''double'' value for MINMAX parameter ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=Maximum value of the MINMAX constraint must be greater than zero.
+cmm.rest_api.minmax_constraint_invalid_use=MINMAX constraint can only be used with numeric data type.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' isn''t a valid ''int'' value for LENGTH parameter ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=You can only use the LENGTH constraint with TEXT, CONTENT and MLTEXT data types.
+# Property
+cmm.rest_api.property_name_null=Property name can't be null.
+cmm.rest_api.property_create_update_failure=We couldn't create/update property.
+cmm.rest_api.property_delete_failure=We couldn't delete the property.
+cmm.rest_api.property_datatype_invalid=''{0}'' isn''t a valid data type format. A valid format should consist of a namespace prefix, a colon, and a name, for example: d:text.
+cmm.rest_api.properties_empty_null=Properties can't be empty or null.
+cmm.rest_api.property_create_name_already_in_use=We couldn''t create the property as the property name ''{0}'' is already in use.
+cmm.rest_api.property_update_prop_not_found=We couldn''t find a property that matches ''{0}''.
+cmm.rest_api.property_change_datatype_err=You can't change the data type of a property in an active model.
+cmm.rest_api.property_change_mandatory_opt_err=You can't change the mandatory option of a property in an active model.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=You can't change the mandatory-enforced option of a property in an active model.
+cmm.rest_api.property_change_multi_valued_opt_err=You can't change the multi-valued option of a property in an active model.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' isn''t a valid value. Use numbers, letters, hyphens (-) and underscores (_) only.
+cmm.rest_api.prefix_not_registered=There isn''t a namespace prefix registered for the URI ''{0}''. Make sure the model is active.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' isn''t a valid prefixed QName value. {1}
+cmm.rest_api.circular_dependency_err=A circular dependency was detected. You can''t set parent ''{0}'', as it''s model already depends on ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' isn''t a valid prefixed QName format. A valid format should consist of a namespace prefix, a colon, and a name, for example: cm:content.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=The custom model can only have one namespace. We found ''{0}''.
+cmm.rest_api.model.import_namespace_undefined=The custom model must define a namespace.
+cmm.rest_api.model.import_associations_unsupported=The custom model doesn't support the 'associations' element.
+cmm.rest_api.model.import_overrides_unsupported=The custom model doesn't support the 'overrides' element.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=The custom model doesn't support the 'mandatory-aspects' element.
+cmm.rest_api.model.import_archive_unsupported=The custom model doesn't support the 'archive' element.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=The custom model doesn't support the 'includedInSuperTypeQuery' element.
+cmm.rest_api.model.import_not_multi_part_req=Request isn't a multi-part form data.
+cmm.rest_api.model.import_not_zip_format=''{0}'' isn''t a valid zip file.
+cmm.rest_api.model.import_process_zip_file_failure=We couldn't process the zip file.
+cmm.rest_api.model.import_no_zip_file_uploaded=We couldn't upload the zip file.
+cmm.rest_api.model.import_invalid_zip_package=The zip file can't have more than two files. There should be one Model file and one Extension module file.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' isn''t a model or a Share extension module file.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' isn''t a valid model file.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' isn''t a valid Share extension module file.
+cmm.rest_api.model.import_failure=We couldn't import the model.
+cmm.rest_api.model.import_process_ext_module_file_failure=We couldn't process the Share extension module file.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_de.properties b/config/alfresco/messages/custommodel-restapi-messages_de.properties
new file mode 100644
index 0000000000..5d9bb5cf76
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_de.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=Modellname darf nicht Null sein.
+cmm.rest_api.model_invalid=Ung\u00fcltiges parametriertes Modell.
+cmm.rest_api.model_status_null=Modellstatus darf nicht Null sein.
+cmm.rest_api.model_name_cannot_update=Sie k\u00f6nnen den Modellnamen nicht aktualisieren.
+cmm.rest_api.model_update_failure=Wir konnten das Modell nicht aktualisieren.
+cmm.rest_api.model_download_failure=Wir konnten keinen Download-Knoten erstellen.
+cmm.rest_api.model_namespace_uri_null=Das Modell-Namespace-URI darf nicht Null sein.
+cmm.rest_api.model_namespace_uri_invalid=Das Namespace-URI ist ung\u00fcltig. Verwenden Sie nur Buchstaben, Zahlen und URI-Zeichen.
+cmm.rest_api.model_namespace_prefix_null=Modell-Namespace-Pr\u00e4fix darf nicht Null sein.
+# Type
+cmm.rest_api.type_name_null=Typenname darf nicht Null sein.
+cmm.rest_api.type_create_failure=Wir konnten den Typ nicht erstellen.
+cmm.rest_api.type_update_failure=Wir konnten den Typ nicht aktualisieren.
+cmm.rest_api.type_parent_cannot_update=Sie k\u00f6nnen den Elterntyp in einem aktiven parametrierten Modell nicht aktualisieren.
+cmm.rest_api.type_parent_not_exist=Sie k\u00f6nnen den Elterntyp nicht auf ''{0}'' setzen, weil es den Typ ''{0}'' nicht gibt.
+cmm.rest_api.type_cannot_delete=Sie k\u00f6nnen einen Typ in einem aktiven Modell nicht l\u00f6schen.
+cmm.rest_api.type_delete_failure=Wir konnten den Typ nicht l\u00f6schen.
+# Aspect
+cmm.rest_api.aspect_name_null=Aspektname darf nicht Null sein.
+cmm.rest_api.aspect_create_failure=Wir konnten den Aspekt nicht erstellen.
+cmm.rest_api.aspect_update_failure=Wir konnten den Aspekt nicht aktualisieren.
+cmm.rest_api.aspect_parent_cannot_update=Sie k\u00f6nnen den Elternaspekt in einem aktiven parametrierten Modell nicht aktualisieren.
+cmm.rest_api.aspect_parent_not_exist=Sie k\u00f6nnen den Elternaspekt nicht auf ''{0}'' setzen, weil es den Aspekt ''{0}'' nicht gibt.
+cmm.rest_api.aspect_cannot_delete=Sie k\u00f6nnen einen Aspekt in einem aktiven Modell nicht l\u00f6schen.
+cmm.rest_api.aspect_delete_failure=Wir konnten den Aspekt nicht l\u00f6schen.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=Sie k\u00f6nnen ''{0}'' nicht l\u00f6schen, weil ''{1}'' davon abh\u00e4ngt.
+# Constraint
+cmm.rest_api.constraint_name_null=Constraint-Name darf nicht Null sein.
+cmm.rest_api.constraint_create_failure=Wir konnten den Constraint nicht erstellen.
+cmm.rest_api.constraint_parameter_name_null=Constraint-Parametername darf nicht Null sein.
+cmm.rest_api.constraint_type_null=Parametrierter Constraint muss ein Typenattribut haben.
+cmm.rest_api.constraint_ref_not_defined=Constraint-Verweis ''{0}'' ist von diesem Modell nicht definiert.
+cmm.rest_api.regex_constraint_invalid_expression=REGEX-Ausdruck ''{0}'' ist nicht zul\u00e4ssig.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' ist kein zul\u00e4ssiger Doppelparameter f\u00fcr MINMAX-Parameter ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=H\u00f6chstwert des MINMAX-Constraints muss gr\u00f6\u00dfer als Null sein.
+cmm.rest_api.minmax_constraint_invalid_use=MINMAX-Constraint kann nur mit numerischem Datentyp verwendet werden.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' ist kein zul\u00e4ssiger Integer-Wert f\u00fcr LENGTH-Parameter ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=Sie k\u00f6nnen den LENGTH-Constraint nur mit den Datentypen TEXT, CONTENT und MLTEXT verwenden.
+# Property
+cmm.rest_api.property_name_null=Eigenschaftenname darf nicht Null sein.
+cmm.rest_api.property_create_update_failure=Wir konnten die Eigenschaft nicht erstellen/aktualisieren.
+cmm.rest_api.property_delete_failure=Wir konnten die Eigenschaft nicht l\u00f6schen.
+cmm.rest_api.property_datatype_invalid=''{0}'' ist kein zul\u00e4ssiges Datentyp-Format. Ein zul\u00e4ssiges Format besteht aus einem Namespace-Pr\u00e4fix, einem Doppelpunkt und einem Namen (zum Beispiel d:text).
+cmm.rest_api.properties_empty_null=Eigenschaften d\u00fcrfen nicht leer oder Null sein.
+cmm.rest_api.property_create_name_already_in_use=Wir konnten die Eigenschaft nicht erstellen, weil der Eigenschaftenname ''{0}'' bereits verwendet wird.
+cmm.rest_api.property_update_prop_not_found=Wir konnten keine Eigenschaft finden, die mit ''{0}'' \u00fcbereinstimmt.
+cmm.rest_api.property_change_datatype_err=Der Datentyp einer Eigenschaft in einem aktiven Modell kann nicht ge\u00e4ndert werden.
+cmm.rest_api.property_change_mandatory_opt_err=Die Option ''Obligatorisch'' einer Eigenschaft in einem aktiven Modell kann nicht ge\u00e4ndert werden.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=Die Option ''Obligatorisch (erzwungen)'' einer Eigenschaft in einem aktiven Modell kann nicht ge\u00e4ndert werden.
+cmm.rest_api.property_change_multi_valued_opt_err=Die Option f\u00fcr mehrere Werte einer Eigenschaft in einem aktiven Modell kann nicht ge\u00e4ndert werden.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' ist kein zul\u00e4ssiger Wert. Verwenden Sie nur Zahlen, Buchstaben, Bindestriche (-) und Unterstriche (_).
+cmm.rest_api.prefix_not_registered=F\u00fcr das URI ''{0}'' ist kein Namespace-Pr\u00e4fix registriert. Stellen Sie sicher, dass das Modell aktiv ist.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' ist kein zul\u00e4ssiger QName-Wert mit Pr\u00e4fix. {1}
+cmm.rest_api.circular_dependency_err=Es wurde eine Ringabh\u00e4ngigkeit entdeckt. Eltern-''{0}'' kann nicht eingerichtet werden, weil sein Modell bereits von ''{1}'' abh\u00e4ngt.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' ist kein zul\u00e4ssiges QName-Format mit Pr\u00e4fix. Ein zul\u00e4ssiges Format besteht aus einem Namespace-Pr\u00e4fix, einem Doppelpunkt und einem Namen (zum Beispiel cm:content).
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=Das parametrierte Modell darf nur \u00fcber einen Namespace verf\u00fcgen. Wir haben ''{0}'' gefunden.
+cmm.rest_api.model.import_namespace_undefined=Das parametrierte Modell muss einen Namespace definieren.
+cmm.rest_api.model.import_associations_unsupported=Das parametrierte Modell unterst\u00fctzt das Element 'associations' nicht.
+cmm.rest_api.model.import_overrides_unsupported=Das parametrierte Modell unterst\u00fctzt das Element 'overrides' nicht.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=Das parametrierte Modell unterst\u00fctzt das Element 'mandatory-aspects' nicht.
+cmm.rest_api.model.import_archive_unsupported=Das parametrierte Modell unterst\u00fctzt das Element 'archive' nicht.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=Das parametrierte Modell unterst\u00fctzt das Element 'includedInSuperTypeQuery' nicht.
+cmm.rest_api.model.import_not_multi_part_req=Bei der Anfrage handelt es sich nicht um mehrteilige Formulardaten.
+cmm.rest_api.model.import_not_zip_format=''{0}'' ist keine zul\u00e4ssige ZIP-Datei.
+cmm.rest_api.model.import_process_zip_file_failure=Wir konnten die ZIP-Datei nicht verarbeiten.
+cmm.rest_api.model.import_no_zip_file_uploaded=Wir konnten die ZIP-Datei nicht hochladen.
+cmm.rest_api.model.import_invalid_zip_package=Die ZIP-Datei darf nicht mehr als zwei Dateien beinhalten. Es sollte eine Modelldatei und eine Erweiterungsmodul-Datei geben.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' ist keine Modell- oder Share-Erweiterungs-Modul-Datei.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' ist keine zul\u00e4ssige Modelldatei.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' ist keine zul\u00e4ssige Share-Erweiterungs-Modul-Datei.
+cmm.rest_api.model.import_failure=Wir konnten das Modell nicht importieren.
+cmm.rest_api.model.import_process_ext_module_file_failure=Wir konnten die Share-Erweiterungs-Modul-Datei nicht verarbeiten.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_es.properties b/config/alfresco/messages/custommodel-restapi-messages_es.properties
new file mode 100644
index 0000000000..1a695575d5
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_es.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=El nombre del modelo no puede ser nulo.
+cmm.rest_api.model_invalid=Modelo personalizado no v\u00e1lido.
+cmm.rest_api.model_status_null=El estado del modelo no puede ser nulo.
+cmm.rest_api.model_name_cannot_update=No puede actualizar el nombre del modelo.
+cmm.rest_api.model_update_failure=No se ha podido actualizar el modelo.
+cmm.rest_api.model_download_failure=No se ha podido crear un nodo de descarga.
+cmm.rest_api.model_namespace_uri_null=La URI de espacio de nombres del modelo no puede ser nula.
+cmm.rest_api.model_namespace_uri_invalid=La URI de espacio de nombres no es v\u00e1lida. Utilice n\u00fameros, letras y caracteres URI solamente.
+cmm.rest_api.model_namespace_prefix_null=El prefijo de espacio de nombres no puede ser nulo.
+# Type
+cmm.rest_api.type_name_null=El nombre del tipo no puede ser nulo.
+cmm.rest_api.type_create_failure=No se ha podido crear el tipo.
+cmm.rest_api.type_update_failure=No se ha podido actualizar el tipo.
+cmm.rest_api.type_parent_cannot_update=No puede actualizar el padre del tipo en un modelo personalizado activo.
+cmm.rest_api.type_parent_not_exist=No puede establecer el padre del tipo en ''{0}'', porque el tipo ''{0}'' no existe.
+cmm.rest_api.type_cannot_delete=No puede eliminar un tipo en un modelo activo.
+cmm.rest_api.type_delete_failure=No se ha podido eliminar el tipo.
+# Aspect
+cmm.rest_api.aspect_name_null=El nombre del aspecto no puede ser nulo.
+cmm.rest_api.aspect_create_failure=No se ha podido crear el aspecto.
+cmm.rest_api.aspect_update_failure=No se ha podido actualizar el aspecto.
+cmm.rest_api.aspect_parent_cannot_update=No puede actualizar el padre del aspecto en un modelo personalizado activo.
+cmm.rest_api.aspect_parent_not_exist=No puede establecer el padre del aspecto en ''{0}'', porque el aspecto ''{0}'' no existe.
+cmm.rest_api.aspect_cannot_delete=No puede eliminar un aspecto en un modelo activo.
+cmm.rest_api.aspect_delete_failure=No se ha podido eliminar el aspecto.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=No puede eliminar ''{0}'' ya que ''{1}'' depende de \u00e9l.
+# Constraint
+cmm.rest_api.constraint_name_null=El nombre de la restricci\u00f3n no puede ser nulo.
+cmm.rest_api.constraint_create_failure=No se ha podido crear la restricci\u00f3n.
+cmm.rest_api.constraint_parameter_name_null=El nombre del par\u00e1metro de la restricci\u00f3n no puede ser nulo.
+cmm.rest_api.constraint_type_null=La restricci\u00f3n personalizada debe tener un atributo 'tipo'.
+cmm.rest_api.constraint_ref_not_defined=La referencia de la restricci\u00f3n ''{0}'' no la ha definido este modelo.
+cmm.rest_api.regex_constraint_invalid_expression=La expresi\u00f3n REGEX ''{0}'' no es v\u00e1lida.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' no es un valor doble v\u00e1lido para el par\u00e1metro MINMAX ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=El valor m\u00e1ximo de la restricci\u00f3n MINMAX debe ser superior a cero.
+cmm.rest_api.minmax_constraint_invalid_use=La restricci\u00f3n MINMAX solo puede usarse con un tipo de datos num\u00e9ricos.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' no es un valor ''entero'' v\u00e1lido para el par\u00e1metro LENGTH ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=Solo puede usar la restricci\u00f3n LENGTH con los tipos de datos TEXT, CONTENT y MLTEXT.
+# Property
+cmm.rest_api.property_name_null=El nombre de la propiedad no puede ser nulo.
+cmm.rest_api.property_create_update_failure=No se ha podido crear/actualizar la propiedad.
+cmm.rest_api.property_delete_failure=No se ha podido eliminar la propiedad.
+cmm.rest_api.property_datatype_invalid=''{0}'' no es un formato de tipo de datos v\u00e1lido. Un formato v\u00e1lido debe incluir un prefijo de espacio de nombres, dos puntos y un nombre, por ejemplo, d:text.
+cmm.rest_api.properties_empty_null=Las propiedades no pueden estar vac\u00edas o ser nulas.
+cmm.rest_api.property_create_name_already_in_use=No se ha podido crear la propiedad porque el nombre de propiedad ''{0}'' ya se est\u00e1 usando.
+cmm.rest_api.property_update_prop_not_found=No se ha podido encontrar una propiedad que coincida con ''{0}''.
+cmm.rest_api.property_change_datatype_err=No puede cambiar el tipo de datos de una propiedad en un modelo activo.
+cmm.rest_api.property_change_mandatory_opt_err=No puede cambiar la opci\u00f3n obligatoria de una propiedad en un modelo activo.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=No puede cambiar la opci\u00f3n obligatoria (exigida) de una propiedad en un modelo activo.
+cmm.rest_api.property_change_multi_valued_opt_err=No puede cambiar la opci\u00f3n de m\u00faltiples valores de una propiedad en un modelo activo.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' no es un valor v\u00e1lido. Utilice n\u00fameros, letras, guiones (-) y guiones bajos (_) solamente.
+cmm.rest_api.prefix_not_registered=No existe un prefijo de espacio de nombres registrado para la URI ''{0}''. Aseg\u00farese de que el modelo est\u00e1 activo.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' no es un valor QName con prefijo v\u00e1lido. {1}
+cmm.rest_api.circular_dependency_err=Se ha detectado una dependencia circular. No puede establecer el padre ''{0}'', ya que su modelo ya depende de ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' no es un formato QName con prefijo v\u00e1lido. Un formato v\u00e1lido debe incluir un prefijo de espacio de nombres, dos puntos y un nombre, por ejemplo, cm:content.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=El modelo personalizado solo puede tener un espacio de nombres. Se encontr\u00f3 ''{0}''.
+cmm.rest_api.model.import_namespace_undefined=El modelo personalizado debe definir un espacio de nombres.
+cmm.rest_api.model.import_associations_unsupported=El modelo personalizado no es compatible con el elemento 'associations'.
+cmm.rest_api.model.import_overrides_unsupported=El modelo personalizado no es compatible con el elemento 'overrides'.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=El modelo personalizado no es compatible con el elemento 'mandatory-aspects'.
+cmm.rest_api.model.import_archive_unsupported=El modelo personalizado no es compatible con el elemento 'archive'.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=El modelo personalizado no es compatible con el elemento 'includedInSuperTypeQuery'.
+cmm.rest_api.model.import_not_multi_part_req=La solicitud no equivale a datos de formulario de varias partes.
+cmm.rest_api.model.import_not_zip_format=''{0}'' no es un fichero zip v\u00e1lido.
+cmm.rest_api.model.import_process_zip_file_failure=No se ha podido procesar el fichero zip.
+cmm.rest_api.model.import_no_zip_file_uploaded=No se ha podido subir el fichero zip.
+cmm.rest_api.model.import_invalid_zip_package=El fichero zip no puede tener m\u00e1s de dos ficheros. Debe hacer un fichero de modelo y un fichero de m\u00f3dulo de extensi\u00f3n.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' no es un modelo ni un fichero de m\u00f3dulo de extensi\u00f3n de Share.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' no es un fichero de modelo v\u00e1lido.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' no es un fichero de m\u00f3dulo de extensi\u00f3n de Share v\u00e1lido.
+cmm.rest_api.model.import_failure=No se ha podido importar el modelo.
+cmm.rest_api.model.import_process_ext_module_file_failure=No se ha podido procesar el fichero de m\u00f3dulo de extensi\u00f3n de Share.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_fr.properties b/config/alfresco/messages/custommodel-restapi-messages_fr.properties
new file mode 100644
index 0000000000..92890ec665
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_fr.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=Le nom du mod\u00e8le ne peut pas \u00eatre null.
+cmm.rest_api.model_invalid=Mod\u00e8le personnalis\u00e9 non valide.
+cmm.rest_api.model_status_null=Le statut du mod\u00e8le ne peut pas \u00eatre null.
+cmm.rest_api.model_name_cannot_update=Impossible de mettre \u00e0 jour le nom du mod\u00e8le.
+cmm.rest_api.model_update_failure=Impossible de mettre \u00e0 jour le mod\u00e8le.
+cmm.rest_api.model_download_failure=Impossible de cr\u00e9er un n\u0153ud de t\u00e9l\u00e9chargement.
+cmm.rest_api.model_namespace_uri_null=L'URI d'espace de nom du mod\u00e8le ne peut pas \u00eatre null.
+cmm.rest_api.model_namespace_uri_invalid=L'URI d'espace de nom n'est pas valide. Utilisez uniquement des lettres, des chiffres et des caract\u00e8res URI.
+cmm.rest_api.model_namespace_prefix_null=Le pr\u00e9fixe d'espace de nom du mod\u00e8le ne peut pas \u00eatre null.
+# Type
+cmm.rest_api.type_name_null=Le nom du type ne peut pas \u00eatre null.
+cmm.rest_api.type_create_failure=Impossible de cr\u00e9er le type.
+cmm.rest_api.type_update_failure=Impossible de mettre \u00e0 jour le type.
+cmm.rest_api.type_parent_cannot_update=Impossible de mettre \u00e0 jour le parent du type dans un mod\u00e8le personnalis\u00e9 actif.
+cmm.rest_api.type_parent_not_exist=Impossible de d\u00e9finir le parent du type sur ''{0}'', car le type ''{0}'' n''existe pas.
+cmm.rest_api.type_cannot_delete=Impossible de supprimer un type dans un mod\u00e8le actif.
+cmm.rest_api.type_delete_failure=Impossible de supprimer le type.
+# Aspect
+cmm.rest_api.aspect_name_null=Le nom de l'aspect ne peut pas \u00eatre null.
+cmm.rest_api.aspect_create_failure=Impossible de cr\u00e9er l'aspect.
+cmm.rest_api.aspect_update_failure=Impossible de mettre \u00e0 jour l'aspect.
+cmm.rest_api.aspect_parent_cannot_update=Impossible de mettre \u00e0 jour le parent de l'aspect dans un mod\u00e8le personnalis\u00e9 actif.
+cmm.rest_api.aspect_parent_not_exist=Impossible de d\u00e9finir le parent de l''aspect sur ''{0}'', car l''aspect ''{0}'' n''existe pas.
+cmm.rest_api.aspect_cannot_delete=Impossible de supprimer un aspect dans un mod\u00e8le actif.
+cmm.rest_api.aspect_delete_failure=Impossible de supprimer l'aspect.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=Impossible de supprimer ''{0}'' car ''{1}'' en d\u00e9pend.
+# Constraint
+cmm.rest_api.constraint_name_null=Le nom de la contrainte ne peut pas \u00eatre null.
+cmm.rest_api.constraint_create_failure=Impossible de cr\u00e9er la contrainte.
+cmm.rest_api.constraint_parameter_name_null=Le nom du param\u00e8tre de la contrainte ne peut pas \u00eatre null.
+cmm.rest_api.constraint_type_null=La contrainte personnalis\u00e9e doit avoir un attribut 'type'.
+cmm.rest_api.constraint_ref_not_defined=La r\u00e9f\u00e9rence de la contrainte ''{0}'' n''est pas d\u00e9finie par ce mod\u00e8le.
+cmm.rest_api.regex_constraint_invalid_expression=L''expression REGEX ''{0}'' n''est pas valide.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' n''est pas une valeur ''double'' valide pour le param\u00e8tre MINMAX ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=La valeur maximum de la contrainte MINMAX doit \u00eatre sup\u00e9rieure \u00e0 z\u00e9ro.
+cmm.rest_api.minmax_constraint_invalid_use=La contrainte MINMAX peut \u00eatre utilis\u00e9e uniquement avec un type de donn\u00e9es num\u00e9rique.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' n''est pas une valeur ''entier'' valide pour le param\u00e8tre LENGTH ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=La contrainte LENGTH peut \u00eatre utilis\u00e9e uniquement avec les types de donn\u00e9es TEXT, CONTENT et MLTEXT.
+# Property
+cmm.rest_api.property_name_null=Le nom de la propri\u00e9t\u00e9 ne peut pas \u00eatre null.
+cmm.rest_api.property_create_update_failure=Impossible de cr\u00e9er/mettre \u00e0 jour la propri\u00e9t\u00e9.
+cmm.rest_api.property_delete_failure=Impossible de supprimer la propri\u00e9t\u00e9.
+cmm.rest_api.property_datatype_invalid=''{0}'' n''est pas un format de type de donn\u00e9es valide. Un format valide se compose d''un pr\u00e9fixe d''espace de nom, suivi de deux-points et d''un nom, par exemple\u00a0: d:text.
+cmm.rest_api.properties_empty_null=Les propri\u00e9t\u00e9s ne peuvent pas \u00eatre vides ou null.
+cmm.rest_api.property_create_name_already_in_use=Impossible de cr\u00e9er la propri\u00e9t\u00e9 car le nom de propri\u00e9t\u00e9 ''{0}'' est d\u00e9j\u00e0 utilis\u00e9.
+cmm.rest_api.property_update_prop_not_found=Impossible de trouver une propri\u00e9t\u00e9 qui correspond \u00e0 ''{0}''.
+cmm.rest_api.property_change_datatype_err=Vous ne pouvez pas modifier le type de donn\u00e9es d'une propri\u00e9t\u00e9 dans un mod\u00e8le actif.
+cmm.rest_api.property_change_mandatory_opt_err=Vous ne pouvez pas modifier l'option obligatoire d'une propri\u00e9t\u00e9 dans un mod\u00e8le actif.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=Vous ne pouvez pas modifier l'option obligatoire impos\u00e9e d'une propri\u00e9t\u00e9 dans un mod\u00e8le actif.
+cmm.rest_api.property_change_multi_valued_opt_err=Vous ne pouvez pas modifier l'option \u00e0 valeurs multiples d'une propri\u00e9t\u00e9 dans un mod\u00e8le actif.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' n''est pas une valeur valide. Utilisez des chiffres, des lettres, des tirets (-) et des traits de soulignement (_) uniquement.
+cmm.rest_api.prefix_not_registered=Aucun pr\u00e9fixe d''espace de nom enregistr\u00e9 pour l''URI ''{0}''. Assurez-vous que le mod\u00e8le est actif.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' n''est pas une valeur QName pr\u00e9fix\u00e9e valide. {1}
+cmm.rest_api.circular_dependency_err=Une d\u00e9pendance circulaire a \u00e9t\u00e9 d\u00e9tect\u00e9e. Impossible de d\u00e9finir le parent ''{0}'' car son mod\u00e8le d\u00e9pend d\u00e9j\u00e0 de ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' n''est pas un format QName pr\u00e9fix\u00e9 valide. Un format valide se compose d''un pr\u00e9fixe d''espace de nom, suivi de deux-points et d''un nom, par exemple\u00a0: cm:content.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=Le mod\u00e8le personnalis\u00e9 ne peut avoir qu''un seul espace de nom. ''{0}'' a \u00e9t\u00e9 trouv\u00e9.
+cmm.rest_api.model.import_namespace_undefined=Le mod\u00e8le personnalis\u00e9 doit d\u00e9finir un espace de nom.
+cmm.rest_api.model.import_associations_unsupported=Le mod\u00e8le personnalis\u00e9 ne prend pas en charge l'\u00e9l\u00e9ment 'associations'.
+cmm.rest_api.model.import_overrides_unsupported=Le mod\u00e8le personnalis\u00e9 ne prend pas en charge l'\u00e9l\u00e9ment 'overrides'.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=Le mod\u00e8le personnalis\u00e9 ne prend pas en charge l'\u00e9l\u00e9ment 'mandatory-aspects'.
+cmm.rest_api.model.import_archive_unsupported=Le mod\u00e8le personnalis\u00e9 ne prend pas en charge l'\u00e9l\u00e9ment 'archive'.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=Le mod\u00e8le personnalis\u00e9 ne prend pas en charge l'\u00e9l\u00e9ment 'includedInSuperTypeQuery'.
+cmm.rest_api.model.import_not_multi_part_req=La requ\u00eate n'est pas une donn\u00e9e de formulaire \u00e0 parties multiples.
+cmm.rest_api.model.import_not_zip_format=''{0}'' n''est pas un fichier zip valide.
+cmm.rest_api.model.import_process_zip_file_failure=Impossible de traiter le fichier zip.
+cmm.rest_api.model.import_no_zip_file_uploaded=Impossible d'importer le fichier zip.
+cmm.rest_api.model.import_invalid_zip_package=Le fichier zip ne peut pas comporter plus de deux fichiers. Il doit comporter un fichier de mod\u00e8le et un fichier de module d'extension.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' n''est pas un fichier de mod\u00e8le ou de module d''extension Share.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' n''est pas un fichier de mod\u00e8le valide.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' n''est pas un fichier de module d''extension Share valide.
+cmm.rest_api.model.import_failure=Impossible d'importer le mod\u00e8le.
+cmm.rest_api.model.import_process_ext_module_file_failure=Impossible de traiter le fichier de module d'extension Share.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_it.properties b/config/alfresco/messages/custommodel-restapi-messages_it.properties
new file mode 100644
index 0000000000..10192f6180
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_it.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=Il nome modello non pu\u00f2 essere nullo.
+cmm.rest_api.model_invalid=Modello personalizzato non valido.
+cmm.rest_api.model_status_null=Lo stato del modello non pu\u00f2 essere nullo.
+cmm.rest_api.model_name_cannot_update=Impossibile aggiornare il nome modello.
+cmm.rest_api.model_update_failure=Impossibile aggiornare il modello.
+cmm.rest_api.model_download_failure=Impossibile creare un nodo di download.
+cmm.rest_api.model_namespace_uri_null=L'URI dello spazio dei nomi del modello non pu\u00f2 essere nullo.
+cmm.rest_api.model_namespace_uri_invalid=L'URI dello spazio dei nomi non \u00e8 valido. Utilizzare solo lettere, numeri e caratteri URI.
+cmm.rest_api.model_namespace_prefix_null=Il prefisso dello spazio dei nomi del modello non pu\u00f2 essere nullo.
+# Type
+cmm.rest_api.type_name_null=Il nome tipo non pu\u00f2 essere nullo.
+cmm.rest_api.type_create_failure=Impossibile creare il tipo.
+cmm.rest_api.type_update_failure=Impossibile aggiornare il tipo.
+cmm.rest_api.type_parent_cannot_update=Impossibile aggiornare il genitore del tipo in un modello personalizzato attivo.
+cmm.rest_api.type_parent_not_exist=Impossibile impostare il genitore del tipo su ''{0}'' perch\u00e9 il tipo ''{0}'' non esiste.
+cmm.rest_api.type_cannot_delete=Impossibile eliminare un tipo in un modello attivo.
+cmm.rest_api.type_delete_failure=Impossibile eliminare il tipo.
+# Aspect
+cmm.rest_api.aspect_name_null=Il nome aspetto non pu\u00f2 essere nullo.
+cmm.rest_api.aspect_create_failure=Impossibile creare l'aspetto.
+cmm.rest_api.aspect_update_failure=Impossibile aggiornare l'aspetto.
+cmm.rest_api.aspect_parent_cannot_update=Impossibile aggiornare il genitore dell'aspetto in un modello personalizzato attivo.
+cmm.rest_api.aspect_parent_not_exist=Impossibile impostare il genitore dell''aspetto su ''{0}'' perch\u00e9 l''aspetto ''{0}'' non esiste.
+cmm.rest_api.aspect_cannot_delete=Impossibile eliminare un aspetto in un modello attivo.
+cmm.rest_api.aspect_delete_failure=Impossibile eliminare l'aspetto.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=Impossibile eliminare ''{0}'' perch\u00e9 ''{1}'' dipende da questo.
+# Constraint
+cmm.rest_api.constraint_name_null=Il nome vincolo non pu\u00f2 essere nullo.
+cmm.rest_api.constraint_create_failure=Impossibile creare il vincolo.
+cmm.rest_api.constraint_parameter_name_null=Il parametro del vincolo non pu\u00f2 essere nullo.
+cmm.rest_api.constraint_type_null=Il vincolo personalizzato deve avere un attributo ''tipo''.
+cmm.rest_api.constraint_ref_not_defined=Il riferimento vincolo ''{0}'' non \u00e8 definito da questo modello.
+cmm.rest_api.regex_constraint_invalid_expression=L''espressione REGEX ''{0}'' non \u00e8 valida.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' non \u00e8 un valore ''doppio'' valido per il parametro MINMAX ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=Il valore massimo del vincolo MINMAX deve essere maggiore di zero.
+cmm.rest_api.minmax_constraint_invalid_use=Il vincolo MINMAX pu\u00f2 essere utilizzato solo con il tipo di dati numerico.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' non \u00e8 un valore ''intero'' valido per il parametro LENGTH ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=\u00c8 possibile utilizzare solo il vincolo LENGTH con i tipi di dati TEXT, CONTENT e MLTEXT.
+# Property
+cmm.rest_api.property_name_null=Il nome propriet\u00e0 non pu\u00f2 essere nullo.
+cmm.rest_api.property_create_update_failure=Impossibile creare/aggiornare la propriet\u00e0.
+cmm.rest_api.property_delete_failure=Impossibile eliminare la propriet\u00e0.
+cmm.rest_api.property_datatype_invalid=''{0}'' non \u00e8 un formato tipo di dati valido. Un formato valido deve essere costituito da un prefisso dello spazio dei nomi, due punti e un nome, ad esempio: d:testo.
+cmm.rest_api.properties_empty_null=Le propriet\u00e0 non possono essere vuote o nulle.
+cmm.rest_api.property_create_name_already_in_use=Impossibile creare la propriet\u00e0 perch\u00e9 il nome propriet\u00e0 ''{0}'' \u00e8 gi\u00e0 in uso.
+cmm.rest_api.property_update_prop_not_found=Nessuna propriet\u00e0 trovata che corrisponda a ''{0}''.
+cmm.rest_api.property_change_datatype_err=Non \u00e8 possibile modificare il tipo di dati di una propriet\u00e0 in un modello attivo.
+cmm.rest_api.property_change_mandatory_opt_err=Non \u00e8 possibile modificare l'opzione obbligatoria di una propriet\u00e0 in un modello attivo.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=Non \u00e8 possibile modificare l'opzione obbligatoria (imposta) di una propriet\u00e0 in un modello attivo.
+cmm.rest_api.property_change_multi_valued_opt_err=Non \u00e8 possibile modificare l'opzione multivalore di una propriet\u00e0 in un modello attivo.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' non \u00e8 un valore valido. Utilizzare solo numeri, lettere, trattini (-) e caratteri di sottolineatura (_).
+cmm.rest_api.prefix_not_registered=Non esiste un prefisso dello spazio dei nomi registrato per l''URI ''{0}''. Assicurarsi che il modello sia attivo.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' non \u00e8 un valore QName con prefisso valido. {1}
+cmm.rest_api.circular_dependency_err=\u00c8 stata rilevata una dipendenza circolare. Non \u00e8 possibile impostare il genitore''{0}'' perch\u00e9 il modello associato dipende gi\u00e0 da ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' non \u00e8 un formato QName con prefisso valido. Un formato valido deve essere costituito da un prefisso dello spazio dei nomi, due punti e un nome, ad esempio: d:contenuto.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=Il modello personalizzato pu\u00f2 avere solo uno spazio dei nomi. \u00c8 stato trovato''{0}''.
+cmm.rest_api.model.import_namespace_undefined=Il modello personalizzato deve definire uno spazio dei nomi.
+cmm.rest_api.model.import_associations_unsupported=Il modello personalizzato non supporta l'elemento ''associations''.
+cmm.rest_api.model.import_overrides_unsupported=Il modello personalizzato non supporta l'elemento ''overrides''.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=Il modello personalizzato non supporta l'elemento ''mandatory-aspects''.
+cmm.rest_api.model.import_archive_unsupported=Il modello personalizzato non supporta l'elemento ''archive''.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=Il modello personalizzato non supporta l'elemento ''includedInSuperTypeQuery''.
+cmm.rest_api.model.import_not_multi_part_req=La richiesta non rappresenta dati di un modulo a pi\u00f9 parti.
+cmm.rest_api.model.import_not_zip_format=''{0}'' non \u00e8 un file zip valido.
+cmm.rest_api.model.import_process_zip_file_failure=Impossibile elaborare il file zip.
+cmm.rest_api.model.import_no_zip_file_uploaded=Impossibile caricare il file zip.
+cmm.rest_api.model.import_invalid_zip_package=Il file zip non pu\u00f2 contenere pi\u00f9 di due file. Deve contenere un file Modello e un file Estensione.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' non \u00e8 un modello o un file modulo di estensione di Share.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' non \u00e8 un file modello valido.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}''non \u00e8 un file modulo di estensione di Share valido.
+cmm.rest_api.model.import_failure=Impossibile importare il modello.
+cmm.rest_api.model.import_process_ext_module_file_failure=Impossibile elaborare il file modulo di estensione di Share.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_ja.properties b/config/alfresco/messages/custommodel-restapi-messages_ja.properties
new file mode 100644
index 0000000000..e31769adfe
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_ja.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=\u30e2\u30c7\u30eb\u540d\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model_invalid=\u7121\u52b9\u306a\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u3067\u3059\u3002
+cmm.rest_api.model_status_null=\u30e2\u30c7\u30eb\u306e\u30b9\u30c6\u30fc\u30bf\u30b9\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model_name_cannot_update=\u30e2\u30c7\u30eb\u540d\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model_update_failure=\u30e2\u30c7\u30eb\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.model_download_failure=\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u30ce\u30fc\u30c9\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.model_namespace_uri_null=\u30e2\u30c7\u30eb\u306e\u540d\u524d\u7a7a\u9593 URI \u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model_namespace_uri_invalid=\u540d\u524d\u7a7a\u9593 URI \u304c\u7121\u52b9\u3067\u3059\u3002 \u82f1\u5b57\u3001\u6570\u5b57\u3001URI \u6587\u5b57\u3060\u3051\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+cmm.rest_api.model_namespace_prefix_null=\u30e2\u30c7\u30eb\u306e\u540d\u524d\u7a7a\u9593\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+# Type
+cmm.rest_api.type_name_null=\u30bf\u30a4\u30d7\u540d\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.type_create_failure=\u30bf\u30a4\u30d7\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.type_update_failure=\u30bf\u30a4\u30d7\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.type_parent_cannot_update=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u306e\u89aa\u30bf\u30a4\u30d7\u306f\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.type_parent_not_exist=\u30bf\u30a4\u30d7 ''{0}'' \u306f\u5b58\u5728\u3057\u306a\u3044\u305f\u3081\u3001''{0}'' \u306b\u89aa\u30bf\u30a4\u30d7\u306f\u8a2d\u5b9a\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.type_cannot_delete=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u306e\u30bf\u30a4\u30d7\u306f\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.type_delete_failure=\u30bf\u30a4\u30d7\u3092\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+# Aspect
+cmm.rest_api.aspect_name_null=\u30a2\u30b9\u30da\u30af\u30c8\u540d\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.aspect_create_failure=\u30a2\u30b9\u30da\u30af\u30c8\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.aspect_update_failure=\u30a2\u30b9\u30da\u30af\u30c8\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.aspect_parent_cannot_update=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u306e\u89aa\u30a2\u30b9\u30da\u30af\u30c8\u306f\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.aspect_parent_not_exist=\u30a2\u30b9\u30da\u30af\u30c8 ''{0}'' \u306f\u5b58\u5728\u3057\u306a\u3044\u305f\u3081\u3001''{0}'' \u306b\u89aa\u30a2\u30b9\u30da\u30af\u30c8\u306f\u8a2d\u5b9a\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.aspect_cannot_delete=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u306e\u30a2\u30b9\u30da\u30af\u30c8\u306f\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.aspect_delete_failure=\u30a2\u30b9\u30da\u30af\u30c8\u3092\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=''{0}'' \u306f ''{1}'' \u306e\u4f9d\u5b58\u5148\u3067\u3042\u308b\u305f\u3081\u3001\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3002
+# Constraint
+cmm.rest_api.constraint_name_null=\u5236\u7d04\u540d\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.constraint_create_failure=\u5236\u7d04\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.constraint_parameter_name_null=\u5236\u7d04\u30d1\u30e9\u30e1\u30fc\u30bf\u540d\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.constraint_type_null=\u30ab\u30b9\u30bf\u30e0\u306e\u5236\u7d04\u306b\u306f 'type' \u5c5e\u6027\u304c\u5fc5\u8981\u3067\u3059\u3002
+cmm.rest_api.constraint_ref_not_defined=\u5236\u7d04\u306e\u53c2\u7167 ''{0}'' \u306f\u3001\u3053\u306e\u30e2\u30c7\u30eb\u3067\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002
+cmm.rest_api.regex_constraint_invalid_expression=REGEX \u8868\u73fe ''{0}'' \u304c\u7121\u52b9\u3067\u3059\u3002
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' \u306f MINMAX \u30d1\u30e9\u30e1\u30fc\u30bf ''{1}'' \u306e\u6709\u52b9\u306a\u500d\u7cbe\u5ea6\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.minmax_constraint_invalid_max_value=MINMAX \u5236\u7d04\u306e\u6700\u5927\u5024\u306f\u3001\u30bc\u30ed\u3088\u308a\u5927\u304d\u304f\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.minmax_constraint_invalid_use=MINMAX \u5236\u7d04\u306f\u3001\u6570\u5b57\u306e\u30c7\u30fc\u30bf\u578b\u3067\u3057\u304b\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' \u306f\u3001LENGTH \u30d1\u30e9\u30e1\u30fc\u30bf ''{1}'' \u306e\u6709\u52b9\u306a\u6574\u6570\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.length_constraint_invalid_use=LENGTH \u5236\u7d04\u306f\u3001TEXT\u3001CONTENT\u3001MLTEXT \u306e\u30c7\u30fc\u30bf\u578b\u3067\u3057\u304b\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+# Property
+cmm.rest_api.property_name_null=\u30d7\u30ed\u30d1\u30c6\u30a3\u540d\u306f\u30cc\u30eb\u306b\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.property_create_update_failure=\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u4f5c\u6210\u307e\u305f\u306f\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.property_delete_failure=\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.property_datatype_invalid=''{0}'' \u306f\u6709\u52b9\u306a\u30c7\u30fc\u30bf\u578b\u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002 \u6709\u52b9\u306a\u5f62\u5f0f\u306f\u3001\u540d\u524d\u7a7a\u9593\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3001\u30b3\u30ed\u30f3 (:)\u3001\u540d\u524d\u306e\u7d44\u307f\u5408\u308f\u305b\u3067\u3059\u3002\u4f8b: d:text
+cmm.rest_api.properties_empty_null=\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u7a7a\u307e\u305f\u306f\u30cc\u30eb\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.property_create_name_already_in_use=''{0}'' \u3068\u3044\u3046\u30d7\u30ed\u30d1\u30c6\u30a3\u540d\u306f\u3059\u3067\u306b\u4f7f\u308f\u308c\u3066\u3044\u308b\u305f\u3081\u3001\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.property_update_prop_not_found=''{0}'' \u3068\u4e00\u81f4\u3059\u308b\u30d7\u30ed\u30d1\u30c6\u30a3\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.property_change_datatype_err=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u306e\u30d7\u30ed\u30d1\u30c6\u30a3\u306e\u30c7\u30fc\u30bf\u578b\u306f\u5909\u66f4\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.property_change_mandatory_opt_err=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u306e\u30d7\u30ed\u30d1\u30c6\u30a3\u306e\u5fc5\u9808\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u5909\u66f4\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.property_change_mandatory_enforced_opt_err=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u306e\u5fc5\u9808\u81ea\u52d5\u5165\u529b\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u5909\u66f4\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.property_change_multi_valued_opt_err=\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u306e\u30d7\u30ed\u30d1\u30c6\u30a3\u306e\u8907\u6570\u5024\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u5909\u66f4\u3067\u304d\u307e\u305b\u3093\u3002
+# validation
+cmm.rest_api.input_validation_err=''{0}'' \u306f\u6709\u52b9\u306a\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002 \u6570\u5b57\u3001\u82f1\u5b57\u3001\u30cf\u30a4\u30d5\u30f3 (-)\u3001\u30a2\u30f3\u30c0\u30fc\u30b9\u30b3\u30a2 (_) \u3060\u3051\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+cmm.rest_api.prefix_not_registered=URI ''{0}'' \u7528\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u540d\u524d\u7a7a\u9593\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u304c\u3042\u308a\u307e\u305b\u3093\u3002 \u30e2\u30c7\u30eb\u304c\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+cmm.rest_api.prefixed_qname_invalid=''{0}'' \u306f\u6709\u52b9\u306a\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u4ed8\u304d QName \u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002 {1}
+cmm.rest_api.circular_dependency_err=\u5faa\u74b0\u4f9d\u5b58\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002 ''{0}'' \u306f\u3059\u3067\u306b ''{1}'' \u306b\u4f9d\u5b58\u3059\u308b\u30e2\u30c7\u30eb\u3067\u3042\u308b\u305f\u3081\u3001\u89aa\u3092\u8a2d\u5b9a\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' \u306f\u6709\u52b9\u306a\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u4ed8\u304d QName \u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002 \u6709\u52b9\u306a\u5f62\u5f0f\u306f\u3001\u540d\u524d\u7a7a\u9593\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3001\u30b3\u30ed\u30f3 (:)\u3001\u540d\u524d\u306e\u7d44\u307f\u5408\u308f\u305b\u3067\u3059\u3002\u4f8b: cm:content
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u306b\u5b9a\u7fa9\u3067\u304d\u308b\u540d\u524d\u7a7a\u9593\u306f 1 \u3064\u3060\u3051\u3067\u3059\u304c\u3001 ''{0}'' \u500b\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u3002
+cmm.rest_api.model.import_namespace_undefined=\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u306b\u540d\u524d\u7a7a\u9593\u3092 1 \u3064\u5b9a\u7fa9\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002
+cmm.rest_api.model.import_associations_unsupported=\u3053\u306e\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u3067\u306f 'associations' \u8981\u7d20\u3092\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_overrides_unsupported=\u3053\u306e\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u3067\u306f 'overrides' \u8981\u7d20\u3092\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_mandatory_aspects_unsupported=\u3053\u306e\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u3067\u306f 'mandatory-aspects' \u8981\u7d20\u3092\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_archive_unsupported=\u3053\u306e\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u3067\u306f 'archive' \u8981\u7d20\u3092\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=\u3053\u306e\u30ab\u30b9\u30bf\u30e0\u30e2\u30c7\u30eb\u3067\u306f 'includedInSuperTypeQuery' \u8981\u7d20\u3092\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_not_multi_part_req=\u30ea\u30af\u30a8\u30b9\u30c8\u306f\u30de\u30eb\u30c1\u30d1\u30fc\u30c8\u306e\u30d5\u30a9\u30fc\u30e0\u30c7\u30fc\u30bf\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_not_zip_format=''{0}'' \u306f\u6709\u52b9\u306a zip \u30d5\u30a1\u30a4\u30eb\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_process_zip_file_failure=zip \u30d5\u30a1\u30a4\u30eb\u3092\u51e6\u7406\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.model.import_no_zip_file_uploaded=zip \u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.model.import_invalid_zip_package=\u3053\u306e zip \u30d5\u30a1\u30a4\u30eb\u306b 3 \u3064\u4ee5\u4e0a\u306e\u30d5\u30a1\u30a4\u30eb\u3092\u542b\u3081\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002 \u30e2\u30c7\u30eb\u30d5\u30a1\u30a4\u30eb\u3068\u62e1\u5f35\u30e2\u30b8\u30e5\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u3092\u305d\u308c\u305e\u308c 1 \u3064\u305a\u3064\u542b\u3081\u308b\u3088\u3046\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' \u306f\u30e2\u30c7\u30eb\u30d5\u30a1\u30a4\u30eb\u307e\u305f\u306f Share \u62e1\u5f35\u30e2\u30b8\u30e5\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' \u306f\u6709\u52b9\u306a\u30e2\u30c7\u30eb\u30d5\u30a1\u30a4\u30eb\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' \u306f\u6709\u52b9\u306a Share \u62e1\u5f35\u30e2\u30b8\u30e5\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+cmm.rest_api.model.import_failure=\u30e2\u30c7\u30eb\u3092\u30a4\u30f3\u30dd\u30fc\u30c8\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+cmm.rest_api.model.import_process_ext_module_file_failure=Share \u62e1\u5f35\u30e2\u30b8\u30e5\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u3092\u51e6\u7406\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
diff --git a/config/alfresco/messages/custommodel-restapi-messages_nb.properties b/config/alfresco/messages/custommodel-restapi-messages_nb.properties
new file mode 100644
index 0000000000..14807a2d72
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_nb.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=Modellnavn kan ikke v\u00e6re null.
+cmm.rest_api.model_invalid=Ugyldig tilpasset modell.
+cmm.rest_api.model_status_null=Modellstatus kan ikke v\u00e6re null.
+cmm.rest_api.model_name_cannot_update=Modellnavnet kan ikke oppdateres.
+cmm.rest_api.model_update_failure=Vi kan ikke oppdatere modellen.
+cmm.rest_api.model_download_failure=Vi kan ikke opprette en nedlastingsnode.
+cmm.rest_api.model_namespace_uri_null=Modellens URI for navneomr\u00e5det kan ikke v\u00e6re null.
+cmm.rest_api.model_namespace_uri_invalid=URI for navneomr\u00e5det er ugyldig. Bruk kun bokstaver, tall og URI-tegn.
+cmm.rest_api.model_namespace_prefix_null=Modellens prefiks for navneomr\u00e5det kan ikke v\u00e6re null.
+# Type
+cmm.rest_api.type_name_null=Type navn kan ikke v\u00e6re null.
+cmm.rest_api.type_create_failure=Vi kan ikke opprette typen.
+cmm.rest_api.type_update_failure=Vi kan ikke oppdatere typen.
+cmm.rest_api.type_parent_cannot_update=Du kan ikke oppdatere typens overordnede i en aktiv, tilpasset modell.
+cmm.rest_api.type_parent_not_exist=Du kan ikke angi typens overordnede som ''{0}'', fordi typen ''{0}'' finnes ikke.
+cmm.rest_api.type_cannot_delete=En type i en aktiv modell kan ikke slettes.
+cmm.rest_api.type_delete_failure=Vi kan ikke slette typen.
+# Aspect
+cmm.rest_api.aspect_name_null=Aspektnavn kan ikke v\u00e6re null.
+cmm.rest_api.aspect_create_failure=Vi kan ikke opprette aspektet.
+cmm.rest_api.aspect_update_failure=Vi kan ikke oppdatere aspektet.
+cmm.rest_api.aspect_parent_cannot_update=Du kan ikke oppdatere det aspektets overordnede i en aktiv, tilpasset modell.
+cmm.rest_api.aspect_parent_not_exist=Du kan ikke angi aspektets overordnede til ''{0}'', fordi aspektet ''{0}'' finnes ikke.
+cmm.rest_api.aspect_cannot_delete=Et aspekt i en aktiv modell kan ikke slettes.
+cmm.rest_api.aspect_delete_failure=Vi kan ikke slette aspektet.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=''{0}'' kan ikke slettes fordi ''{1}'' er avhengig av den.
+# Constraint
+cmm.rest_api.constraint_name_null=Restriksjonsnavn kan ikke v\u00e6re null.
+cmm.rest_api.constraint_create_failure=Vi kan ikke opprette restriksjon.
+cmm.rest_api.constraint_parameter_name_null=Restriksjonsparameternavn kan ikke v\u00e6re null.
+cmm.rest_api.constraint_type_null=Tilpasset restriksjon m\u00e5 ha en 'type' attributt.
+cmm.rest_api.constraint_ref_not_defined=Restriksjonsreferansen ''{0}'' defineres ikke av denne modellen.
+cmm.rest_api.regex_constraint_invalid_expression=REGEX-uttrykket ''{0}'' er ikke gyldig.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' er ikke et gyldig ''dobbel'' verdi for MINMAX-parameteret ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=Maksimalverdien til MINMAX-restriksjonen m\u00e5 v\u00e6re st\u00f8rre enn null.
+cmm.rest_api.minmax_constraint_invalid_use=MINMAX-restriksjon kan bare brukes med nummerisk datatype.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' er ikke et gyldig ''helt tall'' for LENGTH-parameteret ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=Kun LENGTH-restriksjonen kan brukes sammen med TEXT-, CONTENT- og MLTEXT-datatyper.
+# Property
+cmm.rest_api.property_name_null=Egenskapsnavn kan ikke v\u00e6re null.
+cmm.rest_api.property_create_update_failure=Vi kan ikke opprette/oppdatere egenskap.
+cmm.rest_api.property_delete_failure=Vi kan ikke slette egenskapen..
+cmm.rest_api.property_datatype_invalid=''{0}'' er ikke gyldig datatypeformat. Et gyldig format skal best\u00e5 av prefiks for til navneomr\u00e5de, kolon og et navn, som f.eks: d:tekst.
+cmm.rest_api.properties_empty_null=Egenskapene kan ikke v\u00e6re tomme eller null.
+cmm.rest_api.property_create_name_already_in_use=Vi kan ikke opprette egenskapen fordi egenskapsnavnet ''{0}'' er allerede i bruk.
+cmm.rest_api.property_update_prop_not_found=Vi kan ikke finne en egenskap som stemmer overens med ''{0}''.
+cmm.rest_api.property_change_datatype_err=Du kan ikke endre datatypen til en egenskap i en aktiv modell.
+cmm.rest_api.property_change_mandatory_opt_err=Du kan ikke endre det obligatoriske alternativet til en egenskap i en aktiv modell.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=Du kan ikke endre det obligatoriske, h\u00e5ndhevede alternativet til en egenskap i en aktiv modell.
+cmm.rest_api.property_change_multi_valued_opt_err=Du kan ikke endre alternativet med flere verdier til en egenskap i en aktiv modell.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' er ikke en gyldig verdi. Bruk kun tall, bokstaver, bindestreker (-) og understreking (_).
+cmm.rest_api.prefix_not_registered=Det finnes ikke en prefiks for navneomr\u00e5det som er registrert for URI-en ''{0}''. Kontroller at modellen er aktiv.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' er ikke en gyldig forh\u00e5ndsbestemt QName-verdi. {1}
+cmm.rest_api.circular_dependency_err=En det funnet en sirkul\u00e6r avhengighet. Du kan ikke angi overordnede ''{0}'' fordi dens modell er allerede avhengig av ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' er ikke et gyldig forh\u00e5ndsbestemt QName-format. Et gyldig format skal best\u00e5 av en prefiks for navneomr\u00e5det, et kolon og et navn, som f.eks: cm:innhold.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=Den tilpassede modellen kan bare ha ett navneomr\u00e5de. Vi fant ''{0}''.
+cmm.rest_api.model.import_namespace_undefined=Den tilpassede modellen m\u00e5 definere et navneomr\u00e5de.
+cmm.rest_api.model.import_associations_unsupported=Den tilpassede modellen st\u00f8tter ikke elementet 'associations'.
+cmm.rest_api.model.import_overrides_unsupported=Den tilpassede modellen st\u00f8tter ikke elementet 'overrides'.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=Den tilpassed modellen st\u00f8tter ikke elementet 'mandatory-aspects'.
+cmm.rest_api.model.import_archive_unsupported=Den tilpassede modellen st\u00f8tter ikke elementet 'archive'.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=Den tilpassede modellen st\u00f8tter ikke elementet 'includedInSuperTypeQuery'.
+cmm.rest_api.model.import_not_multi_part_req=Foresp\u00f8rselen har data med flere deler.
+cmm.rest_api.model.import_not_zip_format=''{0}'' er ikke gyldig zip-format.
+cmm.rest_api.model.import_process_zip_file_failure=Vi kan ikke behandle zip-filen.
+cmm.rest_api.model.import_no_zip_file_uploaded=Vi kan ikke laste opp zip-filen.
+cmm.rest_api.model.import_invalid_zip_package=Zip-filen kan ikke ha flere enn to filer. Det skal v\u00e6re \u00e9n modellfil og \u00e9n utvidelsesmodulfil.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' er ikke en modell eller Share-utvidelsesmodulfil.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' er ikke en gyldig modellfil.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' er ikke en gyldig Share-utvidelsesmodulfil.
+cmm.rest_api.model.import_failure=Vi kan ikke importere modellen.
+cmm.rest_api.model.import_process_ext_module_file_failure=Vi kan ikke behandle Share-utvidelsesmodulfilen.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_nl.properties b/config/alfresco/messages/custommodel-restapi-messages_nl.properties
new file mode 100644
index 0000000000..836e39ac51
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_nl.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=Modelnaam kan niet null zijn.
+cmm.rest_api.model_invalid=Ongeldig aangepast model.
+cmm.rest_api.model_status_null=Modelstatus kan niet null zijn.
+cmm.rest_api.model_name_cannot_update=U kunt de modelnaam niet bijwerken.
+cmm.rest_api.model_update_failure=Kan het model niet bijwerken.
+cmm.rest_api.model_download_failure=Kan geen downloadnode maken.
+cmm.rest_api.model_namespace_uri_null=De modelnaamruimte-URI kan niet null zijn.
+cmm.rest_api.model_namespace_uri_invalid=De naamruimte-URI is ongeldig. Gebruik alleen letters, cijfers en URI-tekens.
+cmm.rest_api.model_namespace_prefix_null=Modelnaamruimtevoorvoegsel kan niet null zijn.
+# Type
+cmm.rest_api.type_name_null=Typenaam kan niet null zijn.
+cmm.rest_api.type_create_failure=Kan het type niet maken.
+cmm.rest_api.type_update_failure=Kan het type niet bijwerken.
+cmm.rest_api.type_parent_cannot_update=U kunt het bovenliggende type van een type in een actief aangepast model niet bijwerken.
+cmm.rest_api.type_parent_not_exist=U kunt het bovenliggende type van het type niet instellen op ''{0}'' omdat het type ''{0}'' niet bestaat.
+cmm.rest_api.type_cannot_delete=U kunt een type in een actief model niet verwijderen.
+cmm.rest_api.type_delete_failure=Kan het type niet verwijderen.
+# Aspect
+cmm.rest_api.aspect_name_null=Aspectnaam kan niet null zijn.
+cmm.rest_api.aspect_create_failure=Kan het aspect niet maken.
+cmm.rest_api.aspect_update_failure=Kan het aspect niet bijwerken.
+cmm.rest_api.aspect_parent_cannot_update=U kunt het bovenliggende aspect van het aspect in een actief aangepast model niet bijwerken.
+cmm.rest_api.aspect_parent_not_exist=U kunt het bovenliggende aspect van het aspect niet instellen op ''{0}'' omdat het aspect ''{0}'' niet bestaat.
+cmm.rest_api.aspect_cannot_delete=U kunt een aspect in een actief model niet verwijderen.
+cmm.rest_api.aspect_delete_failure=Kan het aspect niet verwijderen.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=U kunt ''{0}'' niet verwijderen omdat ''{1}'' hiervan afhankelijk is.
+# Constraint
+cmm.rest_api.constraint_name_null=Beperkingsnaam kan niet null zijn.
+cmm.rest_api.constraint_create_failure=Kan beperking niet maken.
+cmm.rest_api.constraint_parameter_name_null=Beperkingsparameter kan niet null zijn.
+cmm.rest_api.constraint_type_null=Aangepaste beperking moet een 'type'-kenmerk hebben.
+cmm.rest_api.constraint_ref_not_defined=Beperkingsverwijzing ''{0}'' wordt niet gedefinieerd door dit model.
+cmm.rest_api.regex_constraint_invalid_expression=REGEX-expressie ''{0}'' is niet geldig.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' is geen geldige ''dubbele'' waarde voor de MINMAX-parameter ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=De maximale waarde van de MINMAX-beperking moet hoger zijn dan nul.
+cmm.rest_api.minmax_constraint_invalid_use=MINMAX-beperking kan alleen worden gebruikt met een numeriek gegevenstype.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' is geen geldige ''integer''-waarde voor de LENGTH-parameter ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=U kunt de LENGTH-beperking alleen gebruiken met TEXT-, CONTENT- en MLTEXT-gegevenstypen.
+# Property
+cmm.rest_api.property_name_null=Eigenschapnaam kan niet null zijn.
+cmm.rest_api.property_create_update_failure=Kan de eigenschap niet maken/bijwerken.
+cmm.rest_api.property_delete_failure=Kan de eigenschap niet verwijderen.
+cmm.rest_api.property_datatype_invalid=''{0}'' is geen geldige gegevenstype-indeling. Een geldige indeling moet bestaan uit een naamruimtevoorvoegsel, een dubbele punt en een naam, bijvoorbeeld: d:tekst.
+cmm.rest_api.properties_empty_null=Eigenschappen kunnen niet leeg of null zijn.
+cmm.rest_api.property_create_name_already_in_use=Kan de eigenschap niet maken omdat de eigenschapsnaam ''{0}'' al in gebruik is.
+cmm.rest_api.property_update_prop_not_found=Kan geen eigenschap vinden dat overeenkomt met ''{0}''.
+cmm.rest_api.property_change_datatype_err=U kunt het gegevenstype van een eigenschap in een actief model niet wijzigen.
+cmm.rest_api.property_change_mandatory_opt_err=U kunt de verplichte optie van een eigenschap in een actief model niet wijzigen.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=U kunt de verplichte-afgedwongen optie van een eigenschap in een actief model niet wijzigen.
+cmm.rest_api.property_change_multi_valued_opt_err=U kunt de meerwaardige optie van een eigenschap in een actief model niet wijzigen.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' is geen geldige waarde. Gebruik alleen cijfers, letters, afbreekstreepjes (-) en onderstrepingstekens (_).
+cmm.rest_api.prefix_not_registered=Er is geen naamruimtevoorvoegsel geregistreerd voor de URI ''{0}''. Controleer of het model actief is.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' is geen geldige waarde voor QName met voorvoegsel. {1}
+cmm.rest_api.circular_dependency_err=Er is een circulaire afhankelijkheid vastgesteld. U kunt het bovenliggende element ''{0}'' niet instellen, omdat het bijbehorende model al afhankelijk is van ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' is geen geldige indeling voor QName met voorvoegsel. Een geldige indeling moet bestaan uit een naamruimtevoorvoegsel, een dubbele punt en een naam, bijvoorbeeld: cm:content.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=Het aangepaste model kan slechts \u00e9\u00e9n naamruimte hebben. ''{0}'' gevonden.
+cmm.rest_api.model.import_namespace_undefined=Het aangepaste model moet een naamruimte defini\u00ebren.
+cmm.rest_api.model.import_associations_unsupported=Het aangepaste model biedt geen ondersteuning voor het element 'associations'.
+cmm.rest_api.model.import_overrides_unsupported=Het aangepaste model biedt geen ondersteuning voor het element 'overrides'.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=Het aangepaste model biedt geen ondersteuning voor het element 'mandatory-aspects'.
+cmm.rest_api.model.import_archive_unsupported=Het aangepaste model biedt geen ondersteuning voor het element 'archive'.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=Het aangepaste model biedt geen ondersteuning voor het element 'includedInSuperTypeQuery'.
+cmm.rest_api.model.import_not_multi_part_req=Aanvraag betreft geen meerdelige-formuliergegevens.
+cmm.rest_api.model.import_not_zip_format=''{0}'' is geen geldig .zip-bestand.
+cmm.rest_api.model.import_process_zip_file_failure=Kan het .zip-bestand niet verwerken.
+cmm.rest_api.model.import_no_zip_file_uploaded=Kan het .zip-bestand niet uploaden.
+cmm.rest_api.model.import_invalid_zip_package=Het .zip-bestand kan niet meer dan twee bestanden bevatten. Er moet \u00e9\u00e9n modelbestand en \u00e9\u00e9n extensiemodulebestand zijn.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' is geen modelbestand of een Share-extensiemodulebestand.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' is geen geldig modelbestand.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' is geen Share-extensiemodulebestand.
+cmm.rest_api.model.import_failure=Kan het model niet importeren.
+cmm.rest_api.model.import_process_ext_module_file_failure=Kan het Share-extensiemodulebestand niet verwerken.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_pt_BR.properties b/config/alfresco/messages/custommodel-restapi-messages_pt_BR.properties
new file mode 100644
index 0000000000..58fca4229a
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_pt_BR.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=O nome do modelo n\u00e3o pode ser nulo.
+cmm.rest_api.model_invalid=Modelo personalizado inv\u00e1lido.
+cmm.rest_api.model_status_null=O status do modelo n\u00e3o pode ser nulo.
+cmm.rest_api.model_name_cannot_update=N\u00e3o \u00e9 poss\u00edvel atualizar o nome do modelo.
+cmm.rest_api.model_update_failure=N\u00e3o foi poss\u00edvel atualizar o modelo.
+cmm.rest_api.model_download_failure=N\u00e3o foi poss\u00edvel criar um n\u00f3 de download.
+cmm.rest_api.model_namespace_uri_null=O URI do espa\u00e7o de nomes do modelo n\u00e3o pode ser nulo.
+cmm.rest_api.model_namespace_uri_invalid=O URI do espa\u00e7o de nomes \u00e9 inv\u00e1lido. Use apenas n\u00fameros, letras e caracteres URI.
+cmm.rest_api.model_namespace_prefix_null=O prefixo do espa\u00e7o de nomes do modelo n\u00e3o pode ser nulo.
+# Type
+cmm.rest_api.type_name_null=O nome do tipo n\u00e3o pode ser nulo.
+cmm.rest_api.type_create_failure=N\u00e3o foi poss\u00edvel criar o tipo.
+cmm.rest_api.type_update_failure=N\u00e3o foi poss\u00edvel atualizar o tipo.
+cmm.rest_api.type_parent_cannot_update=N\u00e3o \u00e9 poss\u00edvel atualizar o tipo principal em um modelo personalizado ativo.
+cmm.rest_api.type_parent_not_exist=N\u00e3o \u00e9 poss\u00edvel configurar o tipo principal como ''{0}'', porque o tipo ''{0}'' n\u00e3o existe.
+cmm.rest_api.type_cannot_delete=N\u00e3o \u00e9 poss\u00edvel excluir um tipo em um modelo ativo.
+cmm.rest_api.type_delete_failure=N\u00e3o foi poss\u00edvel excluir o tipo.
+# Aspect
+cmm.rest_api.aspect_name_null=O nome do aspecto n\u00e3o pode ser nulo.
+cmm.rest_api.aspect_create_failure=N\u00e3o foi poss\u00edvel criar o aspecto.
+cmm.rest_api.aspect_update_failure=N\u00e3o foi poss\u00edvel atualizar o aspecto.
+cmm.rest_api.aspect_parent_cannot_update=N\u00e3o \u00e9 poss\u00edvel atualizar o aspecto principal em um modelo personalizado ativo.
+cmm.rest_api.aspect_parent_not_exist=N\u00e3o \u00e9 poss\u00edvel configurar o aspecto principal como ''{0}'', porque o aspecto ''{0}'' n\u00e3o existe.
+cmm.rest_api.aspect_cannot_delete=N\u00e3o \u00e9 poss\u00edvel excluir um aspecto em um modelo ativo.
+cmm.rest_api.aspect_delete_failure=N\u00e3o foi poss\u00edvel excluir o aspecto.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=N\u00e3o \u00e9 poss\u00edvel excluir ''{0}'', pois ''{1}'' depende dele.
+# Constraint
+cmm.rest_api.constraint_name_null=O nome da restri\u00e7\u00e3o n\u00e3o pode ser nulo.
+cmm.rest_api.constraint_create_failure=N\u00e3o foi poss\u00edvel criar a restri\u00e7\u00e3o.
+cmm.rest_api.constraint_parameter_name_null=O par\u00e2metro da restri\u00e7\u00e3o n\u00e3o pode ser nulo.
+cmm.rest_api.constraint_type_null=Restri\u00e7\u00e3o personalizada deve conter um atributo de 'tipo'.
+cmm.rest_api.constraint_ref_not_defined=A refer\u00eancia de restri\u00e7\u00e3o ''{0}'' n\u00e3o \u00e9 definida por este modelo.
+cmm.rest_api.regex_constraint_invalid_expression=A express\u00e3o REGEX ''{0}'' n\u00e3o \u00e9 v\u00e1lida
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' n\u00e3o \u00e9 um valor ''duplo'' v\u00e1lido para o par\u00e2metro MINMAX ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=O valor m\u00e1ximo da restri\u00e7\u00e3o MINMAX deve ser maior que zero.
+cmm.rest_api.minmax_constraint_invalid_use=A restri\u00e7\u00e3o MINMAX s\u00f3 pode ser usada com tipo de dados num\u00e9ricos.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' n\u00e3o \u00e9 um valor ''inteiro'' v\u00e1lido para o par\u00e2metro LENGTH ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=Voc\u00ea s\u00f3 pode usar a restri\u00e7\u00e3o LENGTH com tipos de dados TEXT, CONTENT e MLTEXT.
+# Property
+cmm.rest_api.property_name_null=O nome da propriedade n\u00e3o pode ser nulo.
+cmm.rest_api.property_create_update_failure=N\u00e3o foi poss\u00edvel criar/atualizar a propriedade.
+cmm.rest_api.property_delete_failure=N\u00e3o foi poss\u00edvel excluir a propriedade.
+cmm.rest_api.property_datatype_invalid=''{0}'' n\u00e3o \u00e9 um formato de tipo de dados v\u00e1lido. Um formato v\u00e1lido deve conter um prefixo do espa\u00e7o de nomes, dois pontos e um nome, por exemplo: d:texto.
+cmm.rest_api.properties_empty_null=As propriedades n\u00e3o podem estar em branco ou ser nulas.
+cmm.rest_api.property_create_name_already_in_use=N\u00e3o foi poss\u00edvel criar a propriedade j\u00e1 que o nome da propriedade ''{0}'' j\u00e1 est\u00e1 em uso.
+cmm.rest_api.property_update_prop_not_found=N\u00e3o foi poss\u00edvel encontrar uma propriedade que corresponda a ''{0}''.
+cmm.rest_api.property_change_datatype_err=N\u00e3o \u00e9 poss\u00edvel alterar o tipo de dados de uma propriedade em um modelo ativo.
+cmm.rest_api.property_change_mandatory_opt_err=N\u00e3o \u00e9 poss\u00edvel alterar a op\u00e7\u00e3o obrigat\u00f3ria de uma propriedade em um modelo ativo.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=N\u00e3o \u00e9 poss\u00edvel alterar a op\u00e7\u00e3o obrigat\u00f3ria e for\u00e7ada de uma propriedade em um modelo ativo.
+cmm.rest_api.property_change_multi_valued_opt_err=N\u00e3o \u00e9 poss\u00edvel alterar a op\u00e7\u00e3o de multivalores de uma propriedade em um modelo ativo.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' n\u00e3o \u00e9 um valor v\u00e1lido. Use apenas n\u00fameros, letras, h\u00edfen (-) e sublinhado (_).
+cmm.rest_api.prefix_not_registered=N\u00e3o h\u00e1 um prefixo do espa\u00e7o de nomes registrado para o URI ''{0}''. Assegure-se de que o modelo esteja ativo.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' n\u00e3o \u00e9 um valor de QName com prefixo v\u00e1lido. {1}
+cmm.rest_api.circular_dependency_err=Foi detectada uma depend\u00eancia circular. N\u00e3o \u00e9 poss\u00edvel configurar o principal ''{0}'', pois seu modelo j\u00e1 depende de ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' n\u00e3o \u00e9 um formato de QName com prefixo v\u00e1lido. Um formato v\u00e1lido deve conter um prefixo do espa\u00e7o de nomes, dois pontos e um nome, por exemplo: cm:conte\u00fado.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=O modelo personalizado s\u00f3 pode ter um espa\u00e7o de nomes. Encontramos ''{0}''.
+cmm.rest_api.model.import_namespace_undefined=O modelo personalizado deve definir um espa\u00e7o de nomes.
+cmm.rest_api.model.import_associations_unsupported=O modelo personalizado n\u00e3o suporta o elemento 'associations'.
+cmm.rest_api.model.import_overrides_unsupported=O modelo personalizado n\u00e3o suporta o elemento 'overrides'.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=O modelo personalizado n\u00e3o suporta o elemento 'mandatory-aspects'.
+cmm.rest_api.model.import_archive_unsupported=O modelo personalizado n\u00e3o suporta o elemento 'archive'.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=O modelo personalizado n\u00e3o suporta o elemento 'includedInSuperTypeQuery'.
+cmm.rest_api.model.import_not_multi_part_req=A solicita\u00e7\u00e3o n\u00e3o \u00e9 um formul\u00e1rio de dados de m\u00faltiplas partes.
+cmm.rest_api.model.import_not_zip_format=''{0}'' n\u00e3o \u00e9 um arquivo zip v\u00e1lido.
+cmm.rest_api.model.import_process_zip_file_failure=N\u00e3o foi poss\u00edvel processar o arquivo zip.
+cmm.rest_api.model.import_no_zip_file_uploaded=N\u00e3o foi poss\u00edvel carregar o arquivo zip.
+cmm.rest_api.model.import_invalid_zip_package=O arquivo zip n\u00e3o pode conter mais do que dois arquivos. Deve existir um arquivo Modelo e um arquivo de m\u00f3dulo Extens\u00e3o.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' n\u00e3o \u00e9 um modelo ou arquivo de m\u00f3dulo de extens\u00e3o Share.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' n\u00e3o \u00e9 um arquivo modelo v\u00e1lido.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' n\u00e3o \u00e9 um arquivo de m\u00f3dulo de extens\u00e3o Share v\u00e1lido.
+cmm.rest_api.model.import_failure=N\u00e3o foi poss\u00edvel importar o modelo.
+cmm.rest_api.model.import_process_ext_module_file_failure=N\u00e3o foi poss\u00edvel processar o arquivo de m\u00f3dulo de extens\u00e3o Share.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_ru.properties b/config/alfresco/messages/custommodel-restapi-messages_ru.properties
new file mode 100644
index 0000000000..85e516916e
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_ru.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=\u0418\u043c\u044f \u043c\u043e\u0434\u0435\u043b\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.model_invalid=\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0430\u044f \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c.
+cmm.rest_api.model_status_null=\u0421\u0442\u0430\u0442\u0443\u0441 \u043c\u043e\u0434\u0435\u043b\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.model_name_cannot_update=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0438\u043c\u044f \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.model_update_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0438\u043c\u044f \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.model_download_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0437\u0435\u043b \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438.
+cmm.rest_api.model_namespace_uri_null=URI \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u0438\u043c\u0435\u043d \u043c\u043e\u0434\u0435\u043b\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.model_namespace_uri_invalid=\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 URI \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u0438\u043c\u0435\u043d. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0443\u043a\u0432\u044b, \u0446\u0438\u0444\u0440\u044b \u0438 \u0441\u0438\u043c\u0432\u043e\u043b\u044b URI.
+cmm.rest_api.model_namespace_prefix_null=\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u0438\u043c\u0435\u043d \u043c\u043e\u0434\u0435\u043b\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+# Type
+cmm.rest_api.type_name_null=\u0418\u043c\u044f \u0442\u0438\u043f\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.type_create_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0442\u0438\u043f.
+cmm.rest_api.type_update_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0442\u0438\u043f.
+cmm.rest_api.type_parent_cannot_update=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0442\u0438\u043f \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.type_parent_not_exist=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c ''{0}'' \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0442\u0438\u043f\u0430: \u0442\u0438\u043f ''{0}'' \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.
+cmm.rest_api.type_cannot_delete=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0442\u0438\u043f \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.type_delete_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0442\u0438\u043f.
+# Aspect
+cmm.rest_api.aspect_name_null=\u0418\u043c\u044f \u0430\u0441\u043f\u0435\u043a\u0442\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.aspect_create_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0430\u0441\u043f\u0435\u043a\u0442.
+cmm.rest_api.aspect_update_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0430\u0441\u043f\u0435\u043a\u0442.
+cmm.rest_api.aspect_parent_cannot_update=\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0430\u0441\u043f\u0435\u043a\u0442 \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.aspect_parent_not_exist=\u041d\u0435\u043b\u044c\u0437\u044f \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c ''{0}'' \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0430\u0441\u043f\u0435\u043a\u0442\u0430: \u0430\u0441\u043f\u0435\u043a\u0442 ''{0}'' \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.
+cmm.rest_api.aspect_cannot_delete=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0430\u0441\u043f\u0435\u043a\u0442 \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.aspect_delete_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0430\u0441\u043f\u0435\u043a\u0442.
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c ''{0}'', \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 ''{1}'' \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430.
+# Constraint
+cmm.rest_api.constraint_name_null=\u0418\u043c\u044f \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.constraint_create_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435.
+cmm.rest_api.constraint_parameter_name_null=\u0418\u043c\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.constraint_type_null=\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0438\u043c\u0435\u0442\u044c \u0430\u0442\u0440\u0438\u0431\u0443\u0442 'type'.
+cmm.rest_api.constraint_ref_not_defined=\u0414\u043b\u044f \u0434\u0430\u043d\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u043d\u0435 \u0437\u0430\u0434\u0430\u043d\u0430 \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 ''{0}''.
+cmm.rest_api.regex_constraint_invalid_expression=\u0420\u0435\u0433\u0443\u043b\u044f\u0440\u043d\u043e\u0435 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u0435 REGEX ''{0}'' \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e.
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u0432\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c \u0434\u0432\u043e\u0439\u043d\u043e\u0439 \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u0438 \u0441 \u043f\u043b\u0430\u0432\u0430\u044e\u0449\u0435\u0439 \u0442\u043e\u0447\u043a\u043e\u0439 \u0434\u043b\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 MINMAX ''{1}''.
+cmm.rest_api.minmax_constraint_invalid_max_value=\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f MINMAX \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0443\u043b\u044f.
+cmm.rest_api.minmax_constraint_invalid_use=\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 MINMAX \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0441 \u0447\u0438\u0441\u043b\u043e\u0432\u044b\u043c \u0442\u0438\u043f\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0445.
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0446\u0435\u043b\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c \u0434\u043b\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 LENGTH ''{1}''.
+cmm.rest_api.length_constraint_invalid_use=\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 LENGTH \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u0442\u0438\u043f\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 TEXT, CONTENT \u0438 MLTEXT.
+# Property
+cmm.rest_api.property_name_null=\u0418\u043c\u044f \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c.
+cmm.rest_api.property_create_update_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0437\u0434\u0430\u0442\u044c/\u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e.
+cmm.rest_api.property_delete_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e.
+cmm.rest_api.property_datatype_invalid=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u043c \u0442\u0438\u043f\u0430 \u0434\u0430\u043d\u043d\u044b\u0445. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u0438\u043c\u0435\u043d, \u0434\u0432\u043e\u0435\u0442\u043e\u0447\u0438\u044f \u0438 \u0438\u043c\u0435\u043d\u0438. \u041f\u0440\u0438\u043c\u0435\u0440: d:text.
+cmm.rest_api.properties_empty_null=\u0414\u043b\u044f \u0441\u0432\u043e\u0439\u0441\u0442\u0432 \u043d\u0435\u043b\u044c\u0437\u044f \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u0435 \u0438\u043b\u0438 \u043d\u0443\u043b\u0435\u0432\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f.
+cmm.rest_api.property_create_name_already_in_use=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e: \u0438\u043c\u044f \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 ''{0}'' \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.
+cmm.rest_api.property_update_prop_not_found=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e, \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0435 ''{0}''.
+cmm.rest_api.property_change_datatype_err=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0442\u0438\u043f \u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.property_change_mandatory_opt_err=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.property_change_mandatory_enforced_opt_err=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 (\u043f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439) \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.property_change_multi_valued_opt_err=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043c\u043d\u043e\u0433\u043e\u0437\u043d\u0430\u0447\u043d\u043e\u0441\u0442\u0438 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438.
+# validation
+cmm.rest_api.input_validation_err=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0438\u0441\u043b\u0430, \u0431\u0443\u043a\u0432\u044b, \u0434\u0435\u0444\u0438\u0441\u044b (-) \u0438 \u0441\u0438\u043c\u0432\u043e\u043b\u044b \u043f\u043e\u0434\u0447\u0435\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u044f (_).
+cmm.rest_api.prefix_not_registered=\u0414\u043b\u044f URI ''{0}'' \u043d\u0435\u0442 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u0438\u043c\u0435\u043d. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043c\u043e\u0434\u0435\u043b\u044c \u0430\u043a\u0442\u0438\u0432\u043d\u0430.
+cmm.rest_api.prefixed_qname_invalid=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c QName \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c. {1}
+cmm.rest_api.circular_dependency_err=\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0446\u0438\u043a\u043b\u0438\u0447\u043d\u0430\u044f \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c. \u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0434\u0430\u0442\u044c \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 ''{0}'', \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0443\u0436\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 ''{1}''.
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u043c QName \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u0438\u043c\u0435\u043d, \u0434\u0432\u043e\u0435\u0442\u043e\u0447\u0438\u044f \u0438 \u0438\u043c\u0435\u043d\u0438. \u041f\u0440\u0438\u043c\u0435\u0440: cm:content.
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=\u0423 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0438\u043c\u0435\u043d. \u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e: ''{0}''.
+cmm.rest_api.model.import_namespace_undefined=\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u043d\u0430 \u0437\u0430\u0434\u0430\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0438\u043c\u0435\u043d.
+cmm.rest_api.model.import_associations_unsupported=\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 'associations'.
+cmm.rest_api.model.import_overrides_unsupported=\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 'overrides'.
+cmm.rest_api.model.import_mandatory_aspects_unsupported=\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 'mandatory-aspects'.
+cmm.rest_api.model.import_archive_unsupported=\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 'archive'.
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 'includedInSuperTypeQuery'.
+cmm.rest_api.model.import_not_multi_part_req=\u0417\u0430\u043f\u0440\u043e\u0441 \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441\u043e\u0441\u0442\u0430\u0432\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u044b.
+cmm.rest_api.model.import_not_zip_format=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c zip-\u0444\u0430\u0439\u043b\u043e\u043c.
+cmm.rest_api.model.import_process_zip_file_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c zip-\u0444\u0430\u0439\u043b.
+cmm.rest_api.model.import_no_zip_file_uploaded=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u0433\u0440\u0443\u0437\u0438\u0442\u044c zip-\u0444\u0430\u0439\u043b.
+cmm.rest_api.model.import_invalid_zip_package=zip-\u0444\u0430\u0439\u043b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u0434\u0432\u0443\u0445 \u0444\u0430\u0439\u043b\u043e\u0432. \u0414\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0434\u0438\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0435\u043b\u0438 \u0438 \u043e\u0434\u0438\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f.
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0444\u0430\u0439\u043b\u043e\u043c \u043c\u043e\u0434\u0435\u043b\u0438 \u0438\u043b\u0438 \u0444\u0430\u0439\u043b\u043e\u043c \u043c\u043e\u0434\u0443\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043e\u0431\u0449\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430.
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0444\u0430\u0439\u043b\u043e\u043c \u043c\u043e\u0434\u0435\u043b\u0438.
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0444\u0430\u0439\u043b\u043e\u043c \u043c\u043e\u0434\u0443\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043e\u0431\u0449\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430.
+cmm.rest_api.model.import_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u044c.
+cmm.rest_api.model.import_process_ext_module_file_failure=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043e\u0431\u0449\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430.
diff --git a/config/alfresco/messages/custommodel-restapi-messages_zh_CN.properties b/config/alfresco/messages/custommodel-restapi-messages_zh_CN.properties
new file mode 100644
index 0000000000..956755be87
--- /dev/null
+++ b/config/alfresco/messages/custommodel-restapi-messages_zh_CN.properties
@@ -0,0 +1,78 @@
+# Messages returned by the CMM Rest API
+
+# Model
+cmm.rest_api.model_name_null=\u6a21\u578b\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.model_invalid=\u81ea\u5b9a\u4e49\u6a21\u578b\u65e0\u6548\u3002
+cmm.rest_api.model_status_null=\u6a21\u578b\u72b6\u6001\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.model_name_cannot_update=\u60a8\u65e0\u6cd5\u66f4\u65b0\u6a21\u578b\u540d\u79f0\u3002
+cmm.rest_api.model_update_failure=\u6211\u4eec\u65e0\u6cd5\u66f4\u65b0\u6a21\u578b\u3002
+cmm.rest_api.model_download_failure=\u6211\u4eec\u65e0\u6cd5\u521b\u5efa\u4e0b\u8f7d\u8282\u70b9\u3002
+cmm.rest_api.model_namespace_uri_null=\u6a21\u578b\u547d\u540d\u7a7a\u95f4 URI \u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.model_namespace_uri_invalid=\u547d\u540d\u7a7a\u95f4 URI \u65e0\u6548\u3002 \u4ec5\u53ef\u4f7f\u7528\u5b57\u6bcd\u3001\u6570\u5b57\u548c URI \u5b57\u7b26\u3002
+cmm.rest_api.model_namespace_prefix_null=\u6a21\u578b\u547d\u540d\u7a7a\u95f4\u524d\u7f00\u4e0d\u80fd\u4e3a\u7a7a\u3002
+# Type
+cmm.rest_api.type_name_null=\u7c7b\u578b\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.type_create_failure=\u6211\u4eec\u65e0\u6cd5\u521b\u5efa\u7c7b\u578b\u3002
+cmm.rest_api.type_update_failure=\u6211\u4eec\u65e0\u6cd5\u66f4\u65b0\u7c7b\u578b\u3002
+cmm.rest_api.type_parent_cannot_update=\u60a8\u65e0\u6cd5\u5728\u6d3b\u8dc3\u7684\u81ea\u5b9a\u4e49\u6a21\u578b\u4e2d\u66f4\u65b0\u7c7b\u578b\u7684\u7236\u7ea7\u7c7b\u578b\u3002
+cmm.rest_api.type_parent_not_exist=\u60a8\u65e0\u6cd5\u5c06\u7c7b\u578b\u7684\u7236\u7ea7\u7c7b\u578b\u8bbe\u7f6e\u4e3a ''{0}''\uff0c\u56e0\u4e3a\u7c7b\u578b ''{0}'' \u4e0d\u5b58\u5728\u3002
+cmm.rest_api.type_cannot_delete=\u60a8\u65e0\u6cd5\u5220\u9664\u6d3b\u8dc3\u6a21\u578b\u4e2d\u7684\u7c7b\u578b\u3002
+cmm.rest_api.type_delete_failure=\u6211\u4eec\u65e0\u6cd5\u5220\u9664\u7c7b\u578b\u3002
+# Aspect
+cmm.rest_api.aspect_name_null=\u5207\u9762\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.aspect_create_failure=\u6211\u4eec\u65e0\u6cd5\u521b\u5efa\u5207\u9762\u3002
+cmm.rest_api.aspect_update_failure=\u6211\u4eec\u65e0\u6cd5\u66f4\u65b0\u5207\u9762\u3002
+cmm.rest_api.aspect_parent_cannot_update=\u60a8\u65e0\u6cd5\u5728\u6d3b\u8dc3\u7684\u81ea\u5b9a\u4e49\u6a21\u578b\u4e2d\u66f4\u65b0\u5207\u9762\u7684\u7236\u7ea7\u5207\u9762\u3002
+cmm.rest_api.aspect_parent_not_exist=\u60a8\u65e0\u6cd5\u5c06\u5207\u9762\u7684\u7236\u7ea7\u5207\u9762\u8bbe\u7f6e\u4e3a ''{0}''\uff0c\u56e0\u4e3a\u5207\u9762 ''{0}'' \u4e0d\u5b58\u5728\u3002
+cmm.rest_api.aspect_cannot_delete=\u60a8\u65e0\u6cd5\u5728\u6d3b\u8dc3\u6a21\u578b\u4e2d\u5220\u9664\u5207\u9762\u3002
+cmm.rest_api.aspect_delete_failure=\u6211\u4eec\u65e0\u6cd5\u5220\u9664\u5207\u9762\u3002
+# TypeAspect
+cmm.rest_api.aspect_type_cannot_delete=\u60a8\u65e0\u6cd5\u5220\u9664 ''{0}''\uff0c\u56e0\u4e3a ''{1}'' \u4f9d\u8d56\u4e8e\u5b83\u3002
+# Constraint
+cmm.rest_api.constraint_name_null=\u7ea6\u675f\u6761\u4ef6\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.constraint_create_failure=\u6211\u4eec\u65e0\u6cd5\u521b\u5efa\u7ea6\u675f\u6761\u4ef6\u3002
+cmm.rest_api.constraint_parameter_name_null=\u7ea6\u675f\u6761\u4ef6\u53c2\u6570\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.constraint_type_null=\u81ea\u5b9a\u4e49\u7ea6\u675f\u6761\u4ef6\u5fc5\u987b\u6709 '\u7c7b\u578b' \u5c5e\u6027\u3002
+cmm.rest_api.constraint_ref_not_defined=\u6b64\u6a21\u578b\u672a\u5b9a\u4e49\u7ea6\u675f\u6761\u4ef6\u5f15\u7528 ''{0}''\u3002
+cmm.rest_api.regex_constraint_invalid_expression=REGEX \u8868\u8fbe\u5f0f ''{0}'' \u65e0\u6548\u3002
+cmm.rest_api.minmax_constraint_invalid_parameter=''{0}'' \u4e0d\u662f MINMAX \u53c2\u6570 ''{1}'' \u7684\u6709\u6548 ''\u53cc\u7cbe\u5ea6'' \u503c\u3002
+cmm.rest_api.minmax_constraint_invalid_max_value=MINMAX \u7ea6\u675f\u6761\u4ef6\u7684\u6700\u5927\u503c\u5fc5\u987b\u5927\u4e8e\u96f6\u3002
+cmm.rest_api.minmax_constraint_invalid_use=MINMAX \u7ea6\u675f\u6761\u4ef6\u53ea\u80fd\u4e0e\u6570\u5b57\u578b\u6570\u636e\u7ed3\u5408\u4f7f\u7528\u3002
+cmm.rest_api.length_constraint_invalid_parameter=''{0}'' \u4e0d\u662f LENGTH \u53c2\u6570 ''{1}'' \u7684\u6709\u6548 ''\u6574\u6570'' \u503c\u3002
+cmm.rest_api.length_constraint_invalid_use=\u60a8\u53ea\u80fd\u5c06 LENGTH \u7ea6\u675f\u6761\u4ef6\u4e0e TEXT\u3001CONTENT \u548c MLTEXT \u7c7b\u578b\u6570\u636e\u7ed3\u5408\u4f7f\u7528\u3002
+# Property
+cmm.rest_api.property_name_null=\u5c5e\u6027\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.property_create_update_failure=\u6211\u4eec\u65e0\u6cd5\u521b\u5efa/\u66f4\u65b0\u5c5e\u6027\u3002
+cmm.rest_api.property_delete_failure=\u6211\u4eec\u65e0\u6cd5\u5220\u9664\u5c5e\u6027\u3002
+cmm.rest_api.property_datatype_invalid=''{0}'' \u4e0d\u662f\u6709\u6548\u7684\u6570\u636e\u7c7b\u578b\u683c\u5f0f\u3002 \u6709\u6548\u7684\u683c\u5f0f\u5e94\u5305\u542b\u547d\u540d\u7a7a\u95f4\u524d\u7f00\u3001\u5192\u53f7\u548c\u540d\u79f0\uff0c\u4f8b\u5982\uff1ad:text\u3002
+cmm.rest_api.properties_empty_null=\u5c5e\u6027\u4e0d\u80fd\u4e3a\u7a7a\u3002
+cmm.rest_api.property_create_name_already_in_use=\u6211\u4eec\u65e0\u6cd5\u521b\u5efa\u5c5e\u6027\uff0c\u56e0\u4e3a\u5c5e\u6027\u540d\u79f0 ''{0}''\u5df2\u5b58\u5728\u3002
+cmm.rest_api.property_update_prop_not_found=\u6211\u4eec\u65e0\u6cd5\u627e\u5230\u4e0e ''{0}'' \u5339\u914d\u7684\u5c5e\u6027\u3002
+cmm.rest_api.property_change_datatype_err=\u60a8\u65e0\u6cd5\u66f4\u6539\u6d3b\u8dc3\u6a21\u578b\u4e2d\u5c5e\u6027\u7684\u6570\u636e\u7c7b\u578b\u3002
+cmm.rest_api.property_change_mandatory_opt_err=\u60a8\u65e0\u6cd5\u66f4\u6539\u6d3b\u8dc3\u6a21\u578b\u4e2d\u5c5e\u6027\u7684\u5f3a\u5236\u9009\u9879\u3002
+cmm.rest_api.property_change_mandatory_enforced_opt_err=\u60a8\u65e0\u6cd5\u66f4\u6539\u6d3b\u8dc3\u6a21\u578b\u4e2d\u5c5e\u6027\u7684\u5f3a\u5236\u6267\u884c\u9009\u9879\u3002
+cmm.rest_api.property_change_multi_valued_opt_err=\u60a8\u65e0\u6cd5\u66f4\u6539\u6d3b\u8dc3\u6a21\u578b\u4e2d\u5c5e\u6027\u7684\u591a\u503c\u9009\u9879\u3002
+# validation
+cmm.rest_api.input_validation_err=''{0}'' \u4e0d\u662f\u6709\u6548\u503c\u3002 \u4ec5\u53ef\u4f7f\u7528\u6570\u5b57\u3001\u5b57\u6bcd\u3001\u8fde\u5b57\u7b26 (-) \u548c\u4e0b\u5212\u7ebf (_)\u3002
+cmm.rest_api.prefix_not_registered=\u6ca1\u6709\u4e3a URI ''{0}'' \u6ce8\u518c\u547d\u540d\u7a7a\u95f4\u524d\u7f00\u3002 \u786e\u4fdd\u6a21\u578b\u4e3a\u6d3b\u8dc3\u3002
+cmm.rest_api.prefixed_qname_invalid=''{0}'' \u4e0d\u662f\u52a0\u524d\u7f00\u7684\u6709\u6548 QName \u503c\u3002 {1}
+cmm.rest_api.circular_dependency_err=\u68c0\u6d4b\u5230\u5faa\u73af\u4f9d\u5b58\u5173\u7cfb\u3002 \u60a8\u65e0\u6cd5\u8bbe\u7f6e\u7236\u7ea7 ''{0}''\uff0c\u56e0\u4e3a\u5b83\u7684\u6a21\u578b\u5df2\u7ecf\u4f9d\u8d56\u4e8e ''{1}''\u3002
+cmm.rest_api.prefixed_qname_invalid_format=''{0}'' \u4e0d\u662f\u52a0\u524d\u7f00\u7684\u6709\u6548 QName \u683c\u5f0f\u3002 \u6709\u6548\u683c\u5f0f\u5e94\u5305\u542b\u547d\u540d\u7a7a\u95f4\u524d\u7f00\u3001\u5192\u53f7\u548c\u540d\u79f0\uff0c\u4f8b\u5982\uff1acm:content\u3002
+# model import
+cmm.rest_api.model.import_namespace_multiple_found=\u81ea\u5b9a\u4e49\u6a21\u578b\u53ea\u80fd\u6709\u4e00\u4e2a\u547d\u540d\u7a7a\u95f4\u3002 \u6211\u4eec\u627e\u5230 ''{0}''\u3002
+cmm.rest_api.model.import_namespace_undefined=\u81ea\u5b9a\u4e49\u6a21\u578b\u5fc5\u987b\u5b9a\u4e49\u4e00\u4e2a\u547d\u540d\u7a7a\u95f4\u3002
+cmm.rest_api.model.import_associations_unsupported=\u81ea\u5b9a\u4e49\u6a21\u578b\u4e0d\u652f\u6301 'associations' \u5143\u7d20\u3002
+cmm.rest_api.model.import_overrides_unsupported=\u81ea\u5b9a\u4e49\u6a21\u578b\u4e0d\u652f\u6301 'overrides' \u5143\u7d20\u3002
+cmm.rest_api.model.import_mandatory_aspects_unsupported=\u81ea\u5b9a\u4e49\u6a21\u578b\u4e0d\u652f\u6301 'mandatory-aspects' \u5143\u7d20\u3002
+cmm.rest_api.model.import_archive_unsupported=\u81ea\u5b9a\u4e49\u6a21\u578b\u4e0d\u652f\u6301 'archive' \u5143\u7d20\u3002
+cmm.rest_api.model.import_includedInSuperTQ_unsupported=\u81ea\u5b9a\u4e49\u6a21\u578b\u4e0d\u652f\u6301 'includedInSuperTypeQuery' \u5143\u7d20\u3002
+cmm.rest_api.model.import_not_multi_part_req=\u8bf7\u6c42\u4e0d\u662f\u591a\u90e8\u5206\u7ec4\u6210\u6570\u636e\u3002
+cmm.rest_api.model.import_not_zip_format=''{0}'' \u4e0d\u662f\u6709\u6548\u7684 zip \u6587\u4ef6\u3002
+cmm.rest_api.model.import_process_zip_file_failure=\u6211\u4eec\u65e0\u6cd5\u5904\u7406 zip \u6587\u4ef6\u3002
+cmm.rest_api.model.import_no_zip_file_uploaded=\u6211\u4eec\u65e0\u6cd5\u4e0a\u4f20 zip \u6587\u4ef6\u3002
+cmm.rest_api.model.import_invalid_zip_package=zip \u6587\u4ef6\u5305\u542b\u7684\u6587\u4ef6\u4e0d\u80fd\u8d85\u8fc7\u4e24\u4e2a\u3002 \u5b83\u4eec\u5e94\u8be5\u4e3a\u4e00\u4e2a\u6a21\u578b\u6587\u4ef6\u548c\u4e00\u4e2a\u6269\u5c55\u6a21\u5757\u6587\u4ef6\u3002
+cmm.rest_api.model.import_invalid_zip_entry_format=''{0}'' \u4e0d\u662f\u6a21\u578b\u6216\u5171\u4eab\u6269\u5c55\u6a21\u5757\u6587\u4ef6\u3002
+cmm.rest_api.model.import_invalid_model_entry=''{0}'' \u4e0d\u662f\u6709\u6548\u7684\u6a21\u578b\u6587\u4ef6\u3002
+cmm.rest_api.model.import_invalid_ext_module_entry=''{0}'' \u4e0d\u662f\u6709\u6548\u7684\u5171\u4eab\u6269\u5c55\u6a21\u5757\u6587\u4ef6\u3002
+cmm.rest_api.model.import_failure=\u6211\u4eec\u65e0\u6cd5\u5bfc\u5165\u6a21\u578b\u3002
+cmm.rest_api.model.import_process_ext_module_file_failure=\u6211\u4eec\u65e0\u6cd5\u5904\u7406\u5171\u4eab\u6269\u5c55\u6a21\u5757\u6587\u4ef6\u3002
diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml
index 24b9753946..4ffbb8cd97 100644
--- a/config/alfresco/public-rest-context.xml
+++ b/config/alfresco/public-rest-context.xml
@@ -102,6 +102,7 @@
alfresco.messages.rest-framework-messages
+ alfresco.messages.custommodel-restapi-messages
@@ -937,4 +938,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.alfresco.rest.api.CustomModels
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.desc.xml
new file mode 100644
index 0000000000..41e1990f27
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.desc.xml
@@ -0,0 +1,20 @@
+
+ Custom Model Upload
+
+ :/alfresco/service/api/cmm/upload
+
+
+ Notes:
+ - user must be an Admin or a member of the ALFRESCO_MODEL_ADMINISTRATORS group
+
+ ]]>
+
+ /api/cmm/upload
+
+ user
+ required
+ internal
+
\ No newline at end of file
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.html.status.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.html.status.ftl
new file mode 100644
index 0000000000..5cb1b2c125
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.html.status.ftl
@@ -0,0 +1,19 @@
+
+
+ Upload Custom Model Failure
+
+
+<#if (args.failure!"")?matches("^[\\w\\d\\._]+$")>
+
+#if>
+
+
\ No newline at end of file
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.json.ftl
new file mode 100644
index 0000000000..b505cc606d
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/custommodel/cmm-upload.post.json.ftl
@@ -0,0 +1,16 @@
+<#escape x as jsonUtils.encodeJSONString(x)>
+{
+ <#if importedModelName??>
+ "modelName": "${importedModelName}",
+ #if>
+ <#if shareExtXMLFragment??>
+ "shareExtModule": "${shareExtXMLFragment}",
+ #if>
+ "status":
+ {
+ "code": 200,
+ "name": "OK",
+ "description": "Model uploaded successfully"
+ }
+}
+#escape>
\ No newline at end of file
diff --git a/config/alfresco/web-scripts-application-context.xml b/config/alfresco/web-scripts-application-context.xml
index f11410bf84..5d8807345a 100644
--- a/config/alfresco/web-scripts-application-context.xml
+++ b/config/alfresco/web-scripts-application-context.xml
@@ -1856,5 +1856,12 @@
-
+
+
+
+
+
+
diff --git a/source/java/org/alfresco/repo/web/scripts/custommodel/CustomModelUploadPost.java b/source/java/org/alfresco/repo/web/scripts/custommodel/CustomModelUploadPost.java
new file mode 100644
index 0000000000..97bc90a5c6
--- /dev/null
+++ b/source/java/org/alfresco/repo/web/scripts/custommodel/CustomModelUploadPost.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.repo.web.scripts.custommodel;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+import javax.xml.parsers.DocumentBuilder;
+
+import org.alfresco.repo.dictionary.CustomModelServiceImpl;
+import org.alfresco.repo.dictionary.M2Model;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.rest.api.CustomModels;
+import org.alfresco.rest.api.model.CustomModel;
+import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
+import org.alfresco.service.cmr.dictionary.CustomModelService;
+import org.alfresco.service.cmr.dictionary.DictionaryException;
+import org.alfresco.util.TempFileProvider;
+import org.alfresco.util.XMLUtil;
+import org.springframework.extensions.webscripts.Cache;
+import org.springframework.extensions.webscripts.DeclarativeWebScript;
+import org.springframework.extensions.webscripts.Status;
+import org.springframework.extensions.webscripts.WebScriptException;
+import org.springframework.extensions.webscripts.WebScriptRequest;
+import org.springframework.extensions.webscripts.servlet.FormData;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+/**
+ * Custom model upload POST. This class is the controller for the
+ * "cmm-upload.post" web scripts.
+ *
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModelUploadPost extends DeclarativeWebScript
+{
+ private static final String SHARE_EXT_MODULE_ROOT_ELEMENT = "module";
+ private static final String TEMP_FILE_PREFIX = "cmmExport";
+ private static final String TEMP_FILE_SUFFIX = ".zip";
+ private static final int BUFFER_SIZE = 10 * 1024;
+
+ private CustomModels customModels;
+ private CustomModelService customModelService;
+
+ public void setCustomModels(CustomModels customModels)
+ {
+ this.customModels = customModels;
+ }
+
+ public void setCustomModelService(CustomModelService customModelService)
+ {
+ this.customModelService = customModelService;
+ }
+
+ @Override
+ protected Map executeImpl(WebScriptRequest req, Status status, Cache cache)
+ {
+ if (!customModelService.isModelAdmin(AuthenticationUtil.getFullyAuthenticatedUser()))
+ {
+ throw new WebScriptException(Status.STATUS_FORBIDDEN, PermissionDeniedException.DEFAULT_MESSAGE_ID);
+ }
+
+ FormData formData = (FormData) req.parseContent();
+ if (formData == null || !formData.getIsMultiPart())
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_not_multi_part_req");
+ }
+
+ ImportResult resultData = null;
+ boolean processed = false;
+ for (FormData.FormField field : formData.getFields())
+ {
+ if (field.getIsFile())
+ {
+ final String fileName = field.getFilename();
+ File tempFile = createTempFile(field.getInputStream());
+ try (ZipFile zipFile = new ZipFile(tempFile, StandardCharsets.UTF_8))
+ {
+ resultData = processUpload(zipFile, field.getFilename());
+ }
+ catch (ZipException ze)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_not_zip_format", new Object[] { fileName });
+ }
+ catch (IOException io)
+ {
+ throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "cmm.rest_api.model.import_process_zip_file_failure", io);
+ }
+ finally
+ {
+ // now the import is done, delete the temp file
+ tempFile.delete();
+ }
+ processed = true;
+ break;
+ }
+
+ }
+
+ if (!processed)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_no_zip_file_uploaded");
+ }
+
+ // If we get here, then importing the custom model didn't throw any exceptions.
+ Map model = new HashMap<>(2);
+ model.put("importedModelName", resultData.getImportedModelName());
+ model.put("shareExtXMLFragment", resultData.getShareExtXMLFragment());
+
+ return model;
+ }
+
+ protected File createTempFile(InputStream inputStream)
+ {
+ try
+ {
+ File tempFile = TempFileProvider.createTempFile(inputStream, TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
+ return tempFile;
+ }
+ catch (Exception ex)
+ {
+ throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "cmm.rest_api.model.import_process_zip_file_failure", ex);
+ }
+ }
+
+ protected ImportResult processUpload(ZipFile zipFile, String filename) throws IOException
+ {
+ if (zipFile.size() > 2)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_zip_package");
+ }
+
+ CustomModel customModel = null;
+ String shareExtModule = null;
+ Enumeration extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements())
+ {
+ ZipEntry entry = entries.nextElement();
+
+ if (!entry.isDirectory())
+ {
+ final String entryName = entry.getName();
+ try (InputStream input = new BufferedInputStream(zipFile.getInputStream(entry), BUFFER_SIZE))
+ {
+ if (!(entryName.endsWith(CustomModelServiceImpl.SHARE_EXT_MODULE_SUFFIX)) && customModel == null)
+ {
+ try
+ {
+ M2Model m2Model = M2Model.createModel(input);
+ customModel = importModel(m2Model);
+ }
+ catch (DictionaryException ex)
+ {
+ if (shareExtModule == null)
+ {
+ // Get the input stream again, as the zip file doesn't support reset.
+ try (InputStream moduleInputStream = new BufferedInputStream(zipFile.getInputStream(entry), BUFFER_SIZE))
+ {
+ shareExtModule = getExtensionModule(moduleInputStream, entryName);
+ }
+
+ if (shareExtModule == null)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_zip_entry_format", new Object[] { entryName });
+ }
+ }
+ else
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_model_entry", new Object[] { entryName });
+ }
+ }
+ }
+ else
+ {
+ shareExtModule = getExtensionModule(input, entryName);
+ if (shareExtModule == null)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_ext_module_entry", new Object[] { entryName });
+ }
+ }
+ }
+ }
+ }
+
+ return new ImportResult(customModel, shareExtModule);
+ }
+
+ protected CustomModel importModel(M2Model m2Model)
+ {
+ CustomModel model = null;
+ try
+ {
+ model = customModels.createCustomModel(m2Model);
+ }
+ catch (Exception ex)
+ {
+ int statusCode;
+ if (ex instanceof ConstraintViolatedException)
+ {
+ statusCode = Status.STATUS_CONFLICT;
+ }
+ else if (ex instanceof InvalidArgumentException)
+ {
+ statusCode = Status.STATUS_BAD_REQUEST;
+ }
+ else
+ {
+ statusCode = Status.STATUS_INTERNAL_SERVER_ERROR;
+ }
+ String msg = ex.getMessage();
+ // remove log numbers. regEx => match 8 or more integers
+ msg = (msg != null) ? msg.replaceAll("\\d{8,}", "").trim() : "cmm.rest_api.model.import_failure";
+
+ throw new WebScriptException(statusCode, msg);
+ }
+
+ return model;
+ }
+
+ protected String getExtensionModule(InputStream inputStream, String fileName)
+ {
+ Element rootElement = null;
+ try
+ {
+ final DocumentBuilder db = XMLUtil.getDocumentBuilder();
+ rootElement = db.parse(inputStream).getDocumentElement();
+ }
+ catch (IOException io)
+ {
+ throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "cmm.rest_api.model.import_process_ext_module_file_failure", io);
+ }
+ catch (SAXException ex)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_ext_module_entry", new Object[] { fileName }, ex);
+ }
+
+ if (rootElement != null && SHARE_EXT_MODULE_ROOT_ELEMENT.equals(rootElement.getNodeName()))
+ {
+ StringWriter sw = new StringWriter();
+ XMLUtil.print(rootElement, sw, false);
+
+ return sw.toString();
+ }
+
+ return null;
+ }
+
+ /**
+ * Simple POJO for model import result.
+ *
+ * @author Jamal Kaabi-Mofrad
+ */
+ public static class ImportResult
+ {
+ private String importedModelName;
+ private String shareExtXMLFragment;
+
+ public ImportResult(CustomModel customModel, String shareExtXMLFragment)
+ {
+ this.shareExtXMLFragment = shareExtXMLFragment;
+ if (customModel != null)
+ {
+ this.importedModelName = customModel.getName();
+ }
+ }
+
+ public String getImportedModelName()
+ {
+ return this.importedModelName;
+ }
+
+ public String getShareExtXMLFragment()
+ {
+ return this.shareExtXMLFragment;
+ }
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/CustomModels.java b/source/java/org/alfresco/rest/api/CustomModels.java
new file mode 100644
index 0000000000..84c641bf01
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/CustomModels.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api;
+
+import org.alfresco.repo.dictionary.M2Model;
+import org.alfresco.rest.api.model.CustomAspect;
+import org.alfresco.rest.api.model.CustomModel;
+import org.alfresco.rest.api.model.CustomModelConstraint;
+import org.alfresco.rest.api.model.CustomModelDownload;
+import org.alfresco.rest.api.model.CustomType;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public interface CustomModels
+{
+ /**
+ * Gets the {@code org.alfresco.rest.api.model.CustomModel} representation for the given model
+ *
+ * @param modelName the model name
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomModel} object
+ */
+ public CustomModel getCustomModel(String modelName, Parameters parameters);
+
+ /**
+ * Gets a paged list of all custom models
+ *
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return a paged list of {@code org.alfresco.rest.api.model.CustomModel} objects
+ */
+ public CollectionWithPagingInfo getCustomModels(Parameters parameters);
+
+ /**
+ * Creates custom model
+ *
+ * @param model the custom model to create
+ * @return {@code org.alfresco.rest.api.model.CustomModel} object
+ */
+ public CustomModel createCustomModel(CustomModel model);
+
+ /**
+ * Creates custom model from the imported {@link M2Model}.
+ *
+ * @param m2Model the model
+ * @return {@code org.alfresco.rest.api.model.CustomModel} object
+ */
+ public CustomModel createCustomModel(M2Model m2Model);
+
+ /**
+ * Updates or activates/deactivates the custom model
+ *
+ * @param modelName the model name
+ * @param model the custom model to update (JSON payload)
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomModel} object
+ */
+ public CustomModel updateCustomModel(String modelName, CustomModel model, Parameters parameters);
+
+ /**
+ * Deletes the custom model
+ *
+ * @param modelName the model name
+ */
+ public void deleteCustomModel(String modelName);
+
+ /**
+ * Gets the {@code org.alfresco.rest.api.model.CustomType} representation of
+ * the given model's type
+ *
+ * @param modelName the model name
+ * @param typeName the model's type name
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomType} object
+ */
+ public CustomType getCustomType(String modelName, String typeName, Parameters parameters);
+
+ /**
+ * Gets a paged list of all the given custom model's types
+ *
+ * @param modelName the model name
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return a paged list of {@code org.alfresco.rest.api.model.CustomType} objects
+ */
+ public CollectionWithPagingInfo getCustomTypes(String modelName, Parameters parameters);
+
+ /**
+ * Creates custom model's type
+ *
+ * @param modelName the model name
+ * @param type the custom type to create within the given model
+ * @return {@code org.alfresco.rest.api.model.CustomType} object
+ */
+ public CustomType createCustomType(String modelName, CustomType type);
+
+ /**
+ * Updates the custom model's type
+ *
+ * @param modelName the model name
+ * @param type the custom model's type to update (JSON payload)
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomType} object
+ */
+ public CustomType updateCustomType(String modelName, CustomType type, Parameters parameters);
+
+ /**
+ * Deletes the custom model's type
+ *
+ * @param modelName the model name
+ * @param typeName the model's type name
+ */
+ public void deleteCustomType(String modelName, String typeName);
+
+ /**
+ * Gets the {@code org.alfresco.rest.api.model.CustomAspect} representation of
+ * the given model's aspect
+ *
+ * @param modelName the model name
+ * @param aspectName the model's aspect name
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomAspect} object
+ */
+ public CustomAspect getCustomAspect(String modelName, String aspectName, Parameters parameters);
+
+ /**
+ * Gets a paged list of all the given custom model's aspects
+ *
+ * @param modelName the model name
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return a paged list of {@code org.alfresco.rest.api.model.CustomAspect} objects
+ */
+ public CollectionWithPagingInfo getCustomAspects(String modelName, Parameters parameters);
+
+ /**
+ * Creates custom model's aspect
+ *
+ * @param modelName the model name
+ * @param aspect the custom aspect to create within the given model
+ * @return {@code org.alfresco.rest.api.model.CustomAspect} object
+ */
+ public CustomAspect createCustomAspect(String modelName, CustomAspect aspect);
+
+ /**
+ * Updates the custom model's aspect
+ *
+ * @param modelName the model name
+ * @param aspect the custom model's aspect to update (JSON payload)
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomAspect} object
+ */
+ public CustomAspect updateCustomAspect(String modelName, CustomAspect aspect, Parameters parameters);
+
+ /**
+ * Deletes the custom model's aspect
+ *
+ * @param modelName the model name
+ * @param aspectName the model's aspect name
+ */
+ public void deleteCustomAspect(String modelName, String aspectName);
+
+ /**
+ * Gets the {@code org.alfresco.rest.api.model.CustomModelConstraint}
+ * representation of the given model's constraint
+ *
+ * @param modelName the model name
+ * @param constraintName the model's constraint name
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomModelConstraint} object
+ */
+ public CustomModelConstraint getCustomModelConstraint(String modelName, String constraintName, Parameters parameters);
+
+ /**
+ * Gets a paged list of all of the given custom model's constraints
+ *
+ * @param modelName the model name
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return a paged list of {@code org.alfresco.rest.api.model.CustomModelConstraint} objects
+ */
+ public CollectionWithPagingInfo getCustomModelConstraints(String modelName, Parameters parameters);
+
+ /**
+ * Creates custom model's constraint
+ *
+ * @param modelName the model name
+ * @param constraint the custom constraint to create within the given model
+ * @return {@code org.alfresco.rest.api.model.CustomModelConstraint} object
+ */
+ public CustomModelConstraint createCustomModelConstraint(String modelName, CustomModelConstraint constraint);
+
+ /**
+ * Starts the creation of a downloadable archive file containing the
+ * custom model file and its associated Share extension module file (if requested).
+ *
+ * @param modelName the model name
+ * @param parameters the {@link Parameters} object to get the parameters
+ * passed into the request
+ * @return {@code org.alfresco.rest.api.model.CustomModelDownload} object
+ * containing the archive node reference
+ */
+ public CustomModelDownload createDownload(String modelName, Parameters parameters);
+}
diff --git a/source/java/org/alfresco/rest/api/cmm/CustomModelAspectsRelation.java b/source/java/org/alfresco/rest/api/cmm/CustomModelAspectsRelation.java
new file mode 100644
index 0000000000..0ee1a8094e
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/cmm/CustomModelAspectsRelation.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.cmm;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.rest.api.CustomModels;
+import org.alfresco.rest.api.model.CustomAspect;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.PropertyCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+@RelationshipResource(name = "aspects", entityResource = CustomModelEntityResource.class, title = "Custom Model Aspects")
+public class CustomModelAspectsRelation implements RelationshipResourceAction.Read,
+ RelationshipResourceAction.ReadById,
+ RelationshipResourceAction.Create,
+ RelationshipResourceAction.Update,
+ RelationshipResourceAction.Delete,
+ InitializingBean
+{
+
+ private CustomModels customModels;
+
+ public void setCustomModels(CustomModels customModels)
+ {
+ this.customModels = customModels;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ PropertyCheck.mandatory(this, "customModels", customModels);
+ }
+
+ @Override
+ @WebApiDescription(title = "Returns a paged list of all the custom model's aspects.")
+ public CollectionWithPagingInfo readAll(String modelName, Parameters parameters)
+ {
+ return customModels.getCustomAspects(modelName, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Returns custom aspect information for the given 'aspectName' in 'modelName'.")
+ public CustomAspect readById(String modelName, String aspectName, Parameters parameters)
+ {
+ return customModels.getCustomAspect(modelName, aspectName, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Removes the custom aspect for the given 'aspectName' in 'modelName'.")
+ public void delete(String modelName, String aspectName, Parameters parameters)
+ {
+ customModels.deleteCustomAspect(modelName, aspectName);
+ }
+
+ @Override
+ @WebApiDescription(title = "Updates the custom aspect in the given 'modelName'.")
+ public CustomAspect update(String modelName, CustomAspect aspect, Parameters parameters)
+ {
+ return customModels.updateCustomAspect(modelName, aspect, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Creates custom aspects for the model 'modelName'.")
+ public List create(String modelName, List aspects, Parameters parameters)
+ {
+ List result = new ArrayList<>(aspects.size());
+ for (CustomAspect aspect : aspects)
+ {
+ result.add(customModels.createCustomAspect(modelName, aspect));
+ }
+ return result;
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/cmm/CustomModelConstraintRelation.java b/source/java/org/alfresco/rest/api/cmm/CustomModelConstraintRelation.java
new file mode 100644
index 0000000000..5b6b3da2af
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/cmm/CustomModelConstraintRelation.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.cmm;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.rest.api.CustomModels;
+import org.alfresco.rest.api.model.CustomModelConstraint;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.PropertyCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+@RelationshipResource(name = "constraints", entityResource = CustomModelEntityResource.class, title = "Custom Model Constraints")
+public class CustomModelConstraintRelation implements RelationshipResourceAction.Read,
+ RelationshipResourceAction.ReadById,
+ RelationshipResourceAction.Create,
+ InitializingBean
+{
+
+ private CustomModels customModels;
+
+ public void setCustomModels(CustomModels customModels)
+ {
+ this.customModels = customModels;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ PropertyCheck.mandatory(this, "customModels", customModels);
+ }
+
+ @Override
+ @WebApiDescription(title = "Returns a paged list of all the custom model's constraints.")
+ public CollectionWithPagingInfo readAll(String modelName, Parameters parameters)
+ {
+ return customModels.getCustomModelConstraints(modelName, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Returns custom constraint information for the given 'constraintName' in 'modelName'.")
+ public CustomModelConstraint readById(String modelName, String constraintName, Parameters parameters)
+ {
+ return customModels.getCustomModelConstraint(modelName, constraintName, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Creates custom constraints for the model 'modelName'.")
+ public List create(String modelName, List constraints, Parameters parameters)
+ {
+ List result = new ArrayList<>(constraints.size());
+ for (CustomModelConstraint constraint : constraints)
+ {
+ result.add(customModels.createCustomModelConstraint(modelName, constraint));
+ }
+ return result;
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/cmm/CustomModelDownloadRelation.java b/source/java/org/alfresco/rest/api/cmm/CustomModelDownloadRelation.java
new file mode 100644
index 0000000000..b546250f41
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/cmm/CustomModelDownloadRelation.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.cmm;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.rest.api.CustomModels;
+import org.alfresco.rest.api.model.CustomModelDownload;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.PropertyCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+@RelationshipResource(name = "download", entityResource = CustomModelEntityResource.class, title = "Custom Model Download")
+public class CustomModelDownloadRelation implements RelationshipResourceAction.Create, InitializingBean
+{
+
+ private CustomModels customModels;
+
+ public void setCustomModels(CustomModels customModels)
+ {
+ this.customModels = customModels;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ PropertyCheck.mandatory(this, "customModels", customModels);
+ }
+
+ @Override
+ @WebApiDescription(title = "Creates download node containing the custom model file and if specified, its associated Share extension module file.")
+ public List create(String modelName, List download, Parameters parameters)
+ {
+ CustomModelDownload result = customModels.createDownload(modelName, parameters);
+ return Collections.singletonList(result);
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/cmm/CustomModelEntityResource.java b/source/java/org/alfresco/rest/api/cmm/CustomModelEntityResource.java
new file mode 100644
index 0000000000..0bd0da59e9
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/cmm/CustomModelEntityResource.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.cmm;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.rest.api.CustomModels;
+import org.alfresco.rest.api.model.CustomModel;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
+import org.alfresco.rest.framework.resource.EntityResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.PropertyCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+@EntityResource(name = "cmm", title = "Custom Model Management")
+public class CustomModelEntityResource implements EntityResourceAction.Read,
+ EntityResourceAction.ReadById,
+ EntityResourceAction.Create,
+ EntityResourceAction.Update,
+ EntityResourceAction.Delete,
+ InitializingBean
+{
+
+ private CustomModels customModels;
+
+ public void setCustomModels(CustomModels customModels)
+ {
+ this.customModels = customModels;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ PropertyCheck.mandatory(this, "customModels", customModels);
+ }
+
+ @Override
+ @WebApiDescription(title="Returns custom model information for the given model name.")
+ public CustomModel readById(String modelName, Parameters parameters) throws EntityNotFoundException
+ {
+ return customModels.getCustomModel(modelName, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title="Returns a paged list of all custom models.")
+ public CollectionWithPagingInfo readAll(Parameters parameters)
+ {
+ return customModels.getCustomModels(parameters);
+ }
+
+ @Override
+ @WebApiDescription(title="Creates custom model(s).")
+ public List create(List entity, Parameters parameters)
+ {
+ List result = new ArrayList<>(entity.size());
+ for (CustomModel cm : entity)
+ {
+ result.add(customModels.createCustomModel(cm));
+ }
+ return result;
+ }
+
+ @Override
+ @WebApiDescription(title = "Updates or activates/deactivates the custom model.")
+ public CustomModel update(String modelName, CustomModel entity, Parameters parameters)
+ {
+ return customModels.updateCustomModel(modelName, entity, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Deletes the custom model.")
+ public void delete(String modelName, Parameters parameters)
+ {
+ customModels.deleteCustomModel(modelName);
+ }
+}
\ No newline at end of file
diff --git a/source/java/org/alfresco/rest/api/cmm/CustomModelTypesRelation.java b/source/java/org/alfresco/rest/api/cmm/CustomModelTypesRelation.java
new file mode 100644
index 0000000000..dab3bed39a
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/cmm/CustomModelTypesRelation.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.cmm;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.rest.api.CustomModels;
+import org.alfresco.rest.api.model.CustomType;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.PropertyCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+@RelationshipResource(name = "types", entityResource = CustomModelEntityResource.class, title = "Custom Model Types")
+public class CustomModelTypesRelation implements RelationshipResourceAction.Read,
+ RelationshipResourceAction.ReadById,
+ RelationshipResourceAction.Create,
+ RelationshipResourceAction.Update,
+ RelationshipResourceAction.Delete,
+ InitializingBean
+{
+
+ private CustomModels customModels;
+
+ public void setCustomModels(CustomModels customModels)
+ {
+ this.customModels = customModels;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ PropertyCheck.mandatory(this, "customModels", customModels);
+ }
+
+ @Override
+ @WebApiDescription(title = "Returns a paged list of all the custom model's types.")
+ public CollectionWithPagingInfo readAll(String modelName, Parameters parameters)
+ {
+ return customModels.getCustomTypes(modelName, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Returns custom type information for the given 'typeName' in 'modelName'.")
+ public CustomType readById(String modelName, String typeName, Parameters parameters)
+ {
+ return customModels.getCustomType(modelName, typeName, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Removes the custom type for the given 'typeName' in 'modelName'.")
+ public void delete(String modelName, String typeName, Parameters parameters)
+ {
+ customModels.deleteCustomType(modelName, typeName);
+ }
+
+ @Override
+ @WebApiDescription(title = "Updates the custom type in the given 'modelName'.")
+ public CustomType update(String modelName, CustomType type, Parameters parameters)
+ {
+ return customModels.updateCustomType(modelName, type, parameters);
+ }
+
+ @Override
+ @WebApiDescription(title = "Creates custom types for the model 'modelName'.")
+ public List create(String modelName, List types, Parameters parameters)
+ {
+ List result = new ArrayList<>(types.size());
+ for (CustomType type : types)
+ {
+ result.add(customModels.createCustomType(modelName, type));
+ }
+ return result;
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/cmm/package-info.java b/source/java/org/alfresco/rest/api/cmm/package-info.java
new file mode 100644
index 0000000000..728a8bc8cb
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/cmm/package-info.java
@@ -0,0 +1,4 @@
+@WebApi(name="alfresco", scope=Api.SCOPE.PRIVATE, version=1)
+package org.alfresco.rest.api.cmm;
+import org.alfresco.rest.framework.Api;
+import org.alfresco.rest.framework.WebApi;
diff --git a/source/java/org/alfresco/rest/api/impl/CustomModelsImpl.java b/source/java/org/alfresco/rest/api/impl/CustomModelsImpl.java
new file mode 100644
index 0000000000..4579cf82b9
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/impl/CustomModelsImpl.java
@@ -0,0 +1,1718 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.repo.dictionary.CompiledModel;
+import org.alfresco.repo.dictionary.CustomModelDefinitionImpl;
+import org.alfresco.repo.dictionary.Facetable;
+import org.alfresco.repo.dictionary.M2Aspect;
+import org.alfresco.repo.dictionary.M2Class;
+import org.alfresco.repo.dictionary.M2Constraint;
+import org.alfresco.repo.dictionary.M2Model;
+import org.alfresco.repo.dictionary.M2Namespace;
+import org.alfresco.repo.dictionary.M2Property;
+import org.alfresco.repo.dictionary.M2Type;
+import org.alfresco.repo.dictionary.ValueDataTypeValidator;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.rest.api.CustomModels;
+import org.alfresco.rest.api.model.AbstractClassModel;
+import org.alfresco.rest.api.model.AbstractCommonDetails;
+import org.alfresco.rest.api.model.CustomAspect;
+import org.alfresco.rest.api.model.CustomModel;
+import org.alfresco.rest.api.model.CustomModel.ModelStatus;
+import org.alfresco.rest.api.model.CustomModelConstraint;
+import org.alfresco.rest.api.model.CustomModelDownload;
+import org.alfresco.rest.api.model.CustomModelNamedValue;
+import org.alfresco.rest.api.model.CustomModelProperty;
+import org.alfresco.rest.api.model.CustomType;
+import org.alfresco.rest.framework.core.exceptions.ApiException;
+import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
+import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Paging;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.service.cmr.dictionary.AspectDefinition;
+import org.alfresco.service.cmr.dictionary.ClassDefinition;
+import org.alfresco.service.cmr.dictionary.ConstraintDefinition;
+import org.alfresco.service.cmr.dictionary.CustomModelDefinition;
+import org.alfresco.service.cmr.dictionary.CustomModelException;
+import org.alfresco.service.cmr.dictionary.CustomModelService;
+import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.dictionary.ModelDefinition;
+import org.alfresco.service.cmr.dictionary.NamespaceDefinition;
+import org.alfresco.service.cmr.dictionary.PropertyDefinition;
+import org.alfresco.service.cmr.dictionary.TypeDefinition;
+import org.alfresco.service.cmr.dictionary.CustomModelException.ActiveModelConstraintException;
+import org.alfresco.service.cmr.dictionary.CustomModelException.CustomModelConstraintException;
+import org.alfresco.service.cmr.dictionary.CustomModelException.InvalidCustomModelException;
+import org.alfresco.service.cmr.dictionary.CustomModelException.ModelDoesNotExistException;
+import org.alfresco.service.cmr.dictionary.CustomModelException.ModelExistsException;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.Pair;
+import org.alfresco.util.collections.CollectionUtils;
+import org.alfresco.util.collections.Function;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.extensions.surf.util.I18NUtil;
+
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModelsImpl implements CustomModels
+{
+ // for consistency the patterns are equivalent to the patterns defined in the cmm-misc.lib.js
+ public static final Pattern NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_\\-]+$");
+ public static final Pattern URI_PATTERN = Pattern.compile("^[A-Za-z0-9:/_\\.\\-]+$");
+
+ public static final String MODEL_NAME_NULL_ERR = "cmm.rest_api.model_name_null";
+ public static final String TYPE_NAME_NULL_ERR = "cmm.rest_api.type_name_null";
+ public static final String ASPECT_NAME_NULL_ERR = "cmm.rest_api.aspect_name_null";
+ public static final String CONSTRAINT_NAME_NULL_ERR = "cmm.rest_api.constraint_name_null";
+
+ // Services
+ protected CustomModelService customModelService;
+ protected DictionaryService dictionaryService;
+ protected PersonService personService;
+ protected NodeService nodeService;
+ protected NamespaceService namespaceService;
+ protected ValueDataTypeValidator valueDataTypeValidator;
+
+ private static final String DEFAULT_DATA_TYPE = "d:text";
+ private static final String SELECT_ALL = "all";
+ private static final String SELECT_STATUS = "status";
+ private static final String SELECT_PROPS = "props";
+ private static final String SELECT_ALL_PROPS = "allProps";
+ private static final String PARAM_UPDATE_PROP = "update";
+ private static final String PARAM_DELETE_PROP = "delete";
+ private static final String PARAM_WITH_EXT_MODULE = "extModule";
+
+ public void setCustomModelService(CustomModelService customModelService)
+ {
+ this.customModelService = customModelService;
+ }
+
+ public void setDictionaryService(DictionaryService dictionaryService)
+ {
+ this.dictionaryService = dictionaryService;
+ }
+
+ public void setPersonService(PersonService personService)
+ {
+ this.personService = personService;
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ public void setNamespaceService(NamespaceService namespaceService)
+ {
+ this.namespaceService = namespaceService;
+ }
+
+ public void setValueDataTypeValidator(ValueDataTypeValidator valueDataTypeValidator)
+ {
+ this.valueDataTypeValidator = valueDataTypeValidator;
+ }
+
+ @Override
+ public CustomModel getCustomModel(String modelName, Parameters parameters)
+ {
+ CustomModelDefinition modelDef = getCustomModelImpl(modelName);
+
+ if (hasSelectProperty(parameters, SELECT_ALL))
+ {
+ return new CustomModel(modelDef,
+ convertToCustomTypes(modelDef.getTypeDefinitions(), false),
+ convertToCustomAspects(modelDef.getAspectDefinitions(), false),
+ convertToCustomModelConstraints(modelDef.getModelDefinedConstraints()));
+ }
+
+ return new CustomModel(modelDef);
+ }
+
+ private CustomModelDefinition getCustomModelImpl(String modelName)
+ {
+ if(modelName == null)
+ {
+ throw new InvalidArgumentException(MODEL_NAME_NULL_ERR);
+ }
+
+ CustomModelDefinition model = null;
+ try
+ {
+ model = customModelService.getCustomModel(modelName);
+ }
+ catch (CustomModelException ex)
+ {
+ throw new EntityNotFoundException(modelName);
+ }
+
+ if (model == null)
+ {
+ throw new EntityNotFoundException(modelName);
+ }
+
+ return model;
+ }
+
+ @Override
+ public CollectionWithPagingInfo getCustomModels(Parameters parameters)
+ {
+ Paging paging = parameters.getPaging();
+ PagingRequest pagingRequest = Util.getPagingRequest(paging);
+ PagingResults results = customModelService.getCustomModels(pagingRequest);
+
+ Integer totalItems = results.getTotalResultCount().getFirst();
+ List page = results.getPage();
+
+ List models = new ArrayList<>(page.size());
+ for (CustomModelDefinition modelDefinition : page)
+ {
+ models.add(new CustomModel(modelDefinition));
+ }
+
+ return CollectionWithPagingInfo.asPaged(paging, models, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
+ }
+
+ @Override
+ public CustomModel createCustomModel(CustomModel model)
+ {
+ // Check the current user is authorised to create a custom model
+ validateCurrentUser();
+ return createCustomModelImpl(model, true);
+ }
+
+ private CustomModel createCustomModelImpl(CustomModel model, boolean basicModelOnly)
+ {
+ M2Model m2Model = null;
+ if (basicModelOnly)
+ {
+ m2Model = convertToM2Model(model, null, null, null);
+ }
+ else
+ {
+ m2Model = convertToM2Model(model, model.getTypes(), model.getAspects(), model.getConstraints());
+ }
+
+ boolean activate = ModelStatus.ACTIVE.equals(model.getStatus());
+ try
+ {
+ CustomModelDefinition modelDefinition = customModelService.createCustomModel(m2Model, activate);
+ return new CustomModel(modelDefinition);
+ }
+ catch (ModelExistsException me)
+ {
+ throw new ConstraintViolatedException(me.getMessage());
+ }
+ catch (CustomModelConstraintException ncx)
+ {
+ throw new ConstraintViolatedException(ncx.getMessage());
+ }
+ catch (InvalidCustomModelException iex)
+ {
+ throw new InvalidArgumentException(iex.getMessage());
+ }
+ catch (Exception e)
+ {
+ throw new ApiException("cmm.rest_api.model_invalid", e);
+ }
+ }
+
+ @Override
+ public CustomModel updateCustomModel(String modelName, CustomModel model, Parameters parameters)
+ {
+ // Check the current user is authorised to update the custom model
+ validateCurrentUser();
+
+ // Check to see if the model exists
+ ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName));
+ CustomModel existingModel = existingModelDetails.getModel();
+
+ // The model just needs to be activated/deactivated (in other words,
+ // the other properties should be untouched)
+ if (hasSelectProperty(parameters, SELECT_STATUS))
+ {
+ ModelStatus status = model.getStatus();
+ if (status == null)
+ {
+ throw new InvalidArgumentException("cmm.rest_api.model_status_null");
+ }
+ try
+ {
+ if (ModelStatus.ACTIVE.equals(status))
+ {
+ customModelService.activateCustomModel(modelName);
+ }
+ else
+ {
+ customModelService.deactivateCustomModel(modelName);
+ }
+ // update the model's status
+ existingModel.setStatus(status);
+ return existingModel;
+ }
+ catch (CustomModelConstraintException mce)
+ {
+ throw new ConstraintViolatedException(mce.getMessage());
+ }
+ catch (Exception ex)
+ {
+ throw new ApiException(ex.getMessage(), ex);
+ }
+ }
+ else
+ {
+ if (model.getName() != null && !(existingModel.getName().equals(model.getName())))
+ {
+ throw new InvalidArgumentException("cmm.rest_api.model_name_cannot_update");
+ }
+
+ existingModel.setNamespaceUri(model.getNamespaceUri());
+ final boolean isNamespacePrefixChanged = !(existingModel.getNamespacePrefix().equals(model.getNamespacePrefix()));
+ if(isNamespacePrefixChanged)
+ {
+ // Change types' and aspects' parents as well as the property constraint's Ref namespace prefix
+ replacePrefix(existingModelDetails.getTypes(), existingModel.getNamespacePrefix(), model.getNamespacePrefix());
+ replacePrefix(existingModelDetails.getAspects(), existingModel.getNamespacePrefix(), model.getNamespacePrefix());
+ }
+ existingModel.setNamespacePrefix(model.getNamespacePrefix());
+ existingModel.setAuthor(model.getAuthor());
+ existingModel.setDescription(model.getDescription());
+
+ CustomModelDefinition modelDef = updateModel(existingModelDetails, "cmm.rest_api.model_update_failure");
+ return new CustomModel(modelDef);
+ }
+ }
+
+ private void replacePrefix(List extends AbstractClassModel> existingTypesOrAspects, String modelOldNamespacePrefix, String modelNewNamespacePrefix)
+ {
+ for(AbstractClassModel classModel : existingTypesOrAspects)
+ {
+ // Type/Aspect's parent name
+ String parentName = classModel.getParentName();
+ if(parentName != null)
+ {
+ Pair prefixLocalNamePair = splitPrefixedQName(parentName);
+ // Check to see if the parent name prefix, is the namespace prefix of the model being edited.
+ // As we don't want to modify the parent name of the imported models.
+ if(modelOldNamespacePrefix.equals(prefixLocalNamePair.getFirst()))
+ {
+ // Change the parent name prefix, to a new model namespace prefix.
+ String newParentName = constructName(prefixLocalNamePair.getSecond(), modelNewNamespacePrefix);
+ classModel.setParentName(newParentName);
+ }
+ }
+
+ // Change the property constraint ref
+ List properties = classModel.getProperties();
+ for(CustomModelProperty prop : properties)
+ {
+ List constraintRefs = prop.getConstraintRefs();
+ if(constraintRefs.size() > 0)
+ {
+ List modifiedRefs = new ArrayList<>(constraintRefs.size());
+ for(String ref : constraintRefs)
+ {
+ // We don't need to check if the prefix is equal to the model prefix here, as it was
+ // done upon adding the constraint refs in the setM2Properties method.
+ Pair prefixLocalNamePair = splitPrefixedQName(ref);
+ // Change the constraint ref prefix, to a new model namespace prefix.
+ String newRef = constructName(prefixLocalNamePair.getSecond(), modelNewNamespacePrefix);
+ modifiedRefs.add(newRef);
+ }
+ prop.setConstraintRefs(modifiedRefs);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void deleteCustomModel(String modelName)
+ {
+ // Check the current user is authorised to delete the custom model
+ validateCurrentUser();
+
+ if(modelName == null)
+ {
+ throw new InvalidArgumentException(MODEL_NAME_NULL_ERR);
+ }
+
+ try
+ {
+ customModelService.deleteCustomModel(modelName);
+ }
+ catch (ModelDoesNotExistException ee)
+ {
+ throw new EntityNotFoundException(modelName);
+ }
+ catch (ActiveModelConstraintException ae)
+ {
+ throw new ConstraintViolatedException(ae.getMessage());
+ }
+ catch (Exception ex)
+ {
+ throw new ApiException(ex.getMessage(), ex);
+ }
+ }
+
+ @Override
+ public CustomType getCustomType(String modelName, String typeName, Parameters parameters)
+ {
+ if(typeName == null)
+ {
+ throw new InvalidArgumentException(TYPE_NAME_NULL_ERR);
+ }
+
+ final CustomModelDefinition modelDef = getCustomModelImpl(modelName);
+ QName typeQname = QName.createQName(modelDef.getName().getNamespaceURI(), typeName);
+
+ TypeDefinition customTypeDef = customModelService.getCustomType(typeQname);
+ if (customTypeDef == null)
+ {
+ throw new EntityNotFoundException(typeName);
+ }
+
+ // Check if inherited properties have been requested
+ boolean includeInheritedProps = hasSelectProperty(parameters, SELECT_ALL_PROPS);
+ return convertToCustomType(customTypeDef, includeInheritedProps);
+ }
+
+ @Override
+ public CollectionWithPagingInfo getCustomTypes(String modelName, Parameters parameters)
+ {
+ CustomModelDefinition modelDef = getCustomModelImpl(modelName);
+ Collection typeDefinitions = modelDef.getTypeDefinitions();
+ // TODO Should we support paging?
+ Paging paging = Paging.DEFAULT;
+
+ List customTypes = convertToCustomTypes(typeDefinitions, false);
+
+ return CollectionWithPagingInfo.asPaged(paging, customTypes, false, typeDefinitions.size());
+
+ }
+
+ @Override
+ public CustomType createCustomType(String modelName, CustomType type)
+ {
+ // Check the current user is authorised to update the custom model
+ validateCurrentUser();
+
+ ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName));
+
+ // Validate type's parent
+ validateTypeAspectParent(type, existingModelDetails.getModel());
+ existingModelDetails.getTypes().add(type);
+
+ updateModel(existingModelDetails, "cmm.rest_api.type_create_failure");
+ return type;
+ }
+
+ @Override
+ public CustomType updateCustomType(String modelName, CustomType type, Parameters parameters)
+ {
+ return updateTypeAspect(modelName, type, parameters);
+ }
+
+ private T updateTypeAspect(String modelName, T classDef, Parameters parameters)
+ {
+ // Check the current user is authorised to update the custom model
+ validateCurrentUser();
+
+ final boolean isAspect = classDef instanceof CustomAspect;
+
+ String name = classDef.getName();
+ if(name == null)
+ {
+ String msgId = isAspect ? ASPECT_NAME_NULL_ERR : TYPE_NAME_NULL_ERR;
+ throw new InvalidArgumentException(msgId);
+ }
+
+ ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName));
+
+ List extends AbstractClassModel> allClassDefs = isAspect ? existingModelDetails.getAspects() : existingModelDetails.getTypes();
+
+ @SuppressWarnings("unchecked")
+ T existingClassDef = (T) getObjectByName(allClassDefs, name);
+ if (existingClassDef == null)
+ {
+ throw new EntityNotFoundException(name);
+ }
+
+ if (hasSelectProperty(parameters, SELECT_PROPS))
+ {
+ String errorMsg = null;
+ String propName = parameters.getParameter(PARAM_DELETE_PROP);
+ if (propName == null)
+ {
+ errorMsg = "cmm.rest_api.property_create_update_failure";
+ // Add/Update properties
+ mergeProperties(existingClassDef, classDef, parameters, existingModelDetails.isActive());
+ }
+ else //Delete property request
+ {
+ errorMsg = "cmm.rest_api.property_delete_failure";
+ deleteProperty(existingClassDef, propName);
+ }
+
+ updateModel(existingModelDetails, errorMsg);
+ }
+ else
+ {
+ existingClassDef.setTitle(classDef.getTitle());
+ existingClassDef.setDescription(classDef.getDescription());
+ final boolean isParentChanged = !(StringUtils.equals(existingClassDef.getParentName(), classDef.getParentName()));
+ if (isParentChanged && existingModelDetails.isActive())
+ {
+ String errMsgId = isAspect ? "cmm.rest_api.aspect_parent_cannot_update" : "cmm.rest_api.type_parent_cannot_update";
+ throw new ConstraintViolatedException(errMsgId);
+ }
+ // Validate type/aspect parent
+ validateTypeAspectParent(classDef, existingModelDetails.getModel());
+ existingClassDef.setParentName(classDef.getParentName());
+
+ String errMsgId = isAspect ? "cmm.rest_api.aspect_update_failure" : "cmm.rest_api.type_update_failure";
+ updateModel(existingModelDetails, errMsgId);
+ }
+ return existingClassDef;
+ }
+
+ @Override
+ public void deleteCustomType(String modelName, String typeName)
+ {
+ // Check the current user is authorised to delete the custom model's type
+ validateCurrentUser();
+
+ if(typeName == null)
+ {
+ throw new InvalidArgumentException(TYPE_NAME_NULL_ERR);
+ }
+
+ ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName));
+ if(existingModelDetails.isActive())
+ {
+ throw new ConstraintViolatedException("cmm.rest_api.type_cannot_delete");
+ }
+
+ Map allTypes = transformToMap(existingModelDetails.getTypes(), toNameFunction());
+ CustomType typeToBeDeleted = allTypes.get(typeName);
+
+ if(typeToBeDeleted == null)
+ {
+ throw new EntityNotFoundException(typeName);
+ }
+
+ // Validate type's dependency
+ validateTypeAspectDelete(allTypes.values(), typeToBeDeleted.getPrefixedName());
+
+ // Remove the validated type
+ allTypes.remove(typeName);
+ existingModelDetails.setTypes(new ArrayList<>(allTypes.values()));
+
+ updateModel(existingModelDetails, "cmm.rest_api.type_delete_failure");
+ }
+
+ @Override
+ public CustomAspect getCustomAspect(String modelName, String aspectName, Parameters parameters)
+ {
+ if(aspectName == null)
+ {
+ throw new InvalidArgumentException(ASPECT_NAME_NULL_ERR);
+ }
+
+ final CustomModelDefinition modelDef = getCustomModelImpl(modelName);
+ QName aspectQname = QName.createQName(modelDef.getName().getNamespaceURI(), aspectName);
+
+ AspectDefinition customAspectDef = customModelService.getCustomAspect(aspectQname);
+ if (customAspectDef == null)
+ {
+ throw new EntityNotFoundException(aspectName);
+ }
+
+ // Check if inherited properties have been requested
+ boolean includeInheritedProps = hasSelectProperty(parameters, SELECT_ALL_PROPS);
+ return convertToCustomAspect(customAspectDef, includeInheritedProps);
+ }
+
+ @Override
+ public CollectionWithPagingInfo getCustomAspects(String modelName, Parameters parameters)
+ {
+ CustomModelDefinition modelDef = getCustomModelImpl(modelName);
+ Collection aspectDefinitions = modelDef.getAspectDefinitions();
+ // TODO Should we support paging?
+ Paging paging = Paging.DEFAULT;
+
+ List customAspects = convertToCustomAspects(aspectDefinitions, false);
+
+ return CollectionWithPagingInfo.asPaged(paging, customAspects, false, aspectDefinitions.size());
+ }
+
+ @Override
+ public CustomAspect createCustomAspect(String modelName, CustomAspect aspect)
+ {
+ // Check the current user is authorised to update the custom model
+ validateCurrentUser();
+
+ ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName));
+
+ // Validate aspect's parent
+ validateTypeAspectParent(aspect, existingModelDetails.getModel());
+ existingModelDetails.getAspects().add(aspect);
+
+ updateModel(existingModelDetails, "cmm.rest_api.aspect_create_failure");
+ return aspect;
+ }
+
+ @Override
+ public CustomAspect updateCustomAspect(String modelName, CustomAspect aspect, Parameters parameters)
+ {
+ return updateTypeAspect(modelName, aspect, parameters);
+ }
+
+ @Override
+ public void deleteCustomAspect(String modelName, String aspectName)
+ {
+ // Check the current user is authorised to delete the custom model's aspect
+ validateCurrentUser();
+
+ if(aspectName == null)
+ {
+ throw new InvalidArgumentException(ASPECT_NAME_NULL_ERR);
+ }
+
+ ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName));
+ if(existingModelDetails.isActive())
+ {
+ throw new ConstraintViolatedException("cmm.rest_api.aspect_cannot_delete");
+ }
+
+ Map allAspects = transformToMap(existingModelDetails.getAspects(), toNameFunction());
+ CustomAspect aspectToBeDeleted = allAspects.get(aspectName);
+
+ if(aspectToBeDeleted == null)
+ {
+ throw new EntityNotFoundException(aspectName);
+ }
+
+ // Validate aspect's dependency
+ validateTypeAspectDelete(allAspects.values(), aspectToBeDeleted.getPrefixedName());
+
+ // Remove the validated aspect
+ allAspects.remove(aspectName);
+ existingModelDetails.setAspects(new ArrayList<>(allAspects.values()));
+
+ updateModel(existingModelDetails, "cmm.rest_api.aspect_delete_failure");
+ }
+
+ @Override
+ public CollectionWithPagingInfo getCustomModelConstraints(String modelName, Parameters parameters)
+ {
+ CustomModelDefinition modelDef = getCustomModelImpl(modelName);
+ Collection constraintDefinitions = modelDef.getModelDefinedConstraints();
+ // TODO Should we support paging?
+ Paging paging = Paging.DEFAULT;
+
+ List customModelConstraints = convertToCustomModelConstraints(constraintDefinitions);
+
+ return CollectionWithPagingInfo.asPaged(paging, customModelConstraints, false, constraintDefinitions.size());
+ }
+
+ @Override
+ public CustomModelConstraint getCustomModelConstraint(String modelName, String constraintName, Parameters parameters)
+ {
+ if (constraintName == null)
+ {
+ throw new InvalidArgumentException(CONSTRAINT_NAME_NULL_ERR);
+ }
+
+ final CustomModelDefinition modelDef = getCustomModelImpl(modelName);
+ QName constraintQname = QName.createQName(modelDef.getName().getNamespaceURI(), constraintName);
+
+ ConstraintDefinition constraintDef = customModelService.getCustomConstraint(constraintQname);
+ if (constraintDef == null)
+ {
+ throw new EntityNotFoundException(constraintName);
+ }
+
+ return new CustomModelConstraint(constraintDef, dictionaryService);
+ }
+
+ @Override
+ public CustomModelConstraint createCustomModelConstraint(String modelName, CustomModelConstraint constraint)
+ {
+ // Check the current user is authorised to create constraints
+ validateCurrentUser();
+
+ ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName));
+
+ existingModelDetails.getModelDefinedConstraints().add(constraint);
+
+ updateModel(existingModelDetails, "cmm.rest_api.constraint_create_failure");
+ return constraint;
+ }
+
+ @Override
+ public CustomModelDownload createDownload(String modelName, Parameters parameters)
+ {
+ // Check the current user is authorised to export the model
+ validateCurrentUser();
+
+ if (modelName == null)
+ {
+ throw new InvalidArgumentException(MODEL_NAME_NULL_ERR);
+ }
+
+ String propName = parameters.getParameter(PARAM_WITH_EXT_MODULE);
+ boolean withForm = Boolean.valueOf(propName);
+ try
+ {
+ NodeRef nodeRef = customModelService.createDownloadNode(modelName, withForm);
+ return new CustomModelDownload(nodeRef);
+ }
+ catch (Exception ex)
+ {
+ String errorMsg = "cmm.rest_api.model_download_failure";
+ if (ex.getMessage() != null)
+ {
+ errorMsg = ex.getMessage();
+ }
+ throw new ApiException(errorMsg, ex);
+ }
+ }
+
+ private CustomType convertToCustomType(TypeDefinition typeDefinition, boolean includeInheritedProps)
+ {
+ List properties = convertToCustomModelProperty(typeDefinition, includeInheritedProps);
+ return new CustomType(typeDefinition, dictionaryService, properties);
+ }
+
+ private List convertToCustomTypes(Collection typeDefinitions, boolean includeInheritedProps)
+ {
+ // Convert a collection of TypeDefinitions into a list of CustomTypes
+ List customTypes = new ArrayList<>(typeDefinitions.size());
+ for (TypeDefinition td : typeDefinitions)
+ {
+ customTypes.add(convertToCustomType(td, includeInheritedProps));
+ }
+
+ return customTypes;
+ }
+
+ private CustomAspect convertToCustomAspect(AspectDefinition aspectDefinition, boolean includeInheritedProps)
+ {
+ List properties = convertToCustomModelProperty(aspectDefinition, includeInheritedProps);
+ return new CustomAspect(aspectDefinition, dictionaryService, properties);
+ }
+
+ private List convertToCustomAspects(Collection aspectDefinitions, boolean includeInheritedProps)
+ {
+ // Convert a collection of AspectDefinitions into a list of CustomAspect
+ List customAspects = new ArrayList<>(aspectDefinitions.size());
+ for (AspectDefinition ad : aspectDefinitions)
+ {
+ customAspects.add(convertToCustomAspect(ad, includeInheritedProps));
+ }
+
+ return customAspects;
+ }
+
+ private List convertToCustomModelProperty(ClassDefinition classDefinition, boolean includeInherited)
+ {
+ Collection ownProperties = null;
+ ClassDefinition parentDef = classDefinition.getParentClassDefinition();
+ if (!includeInherited && parentDef != null)
+ {
+ // Remove inherited properties
+ ownProperties = removeRightEntries(classDefinition.getProperties(), parentDef.getProperties()).values();
+ }
+ else
+ {
+ ownProperties = classDefinition.getProperties().values();
+ }
+
+ List customProperties = new ArrayList<>(ownProperties.size());
+ for (PropertyDefinition propDef : ownProperties)
+ {
+ customProperties.add(new CustomModelProperty(propDef, dictionaryService));
+ }
+
+ return customProperties;
+ }
+
+ private List convertToCustomModelConstraints(Collection constraintDefinitions)
+ {
+ List constraints = new ArrayList<>(constraintDefinitions.size());
+ for (ConstraintDefinition definition : constraintDefinitions)
+ {
+ constraints.add(new CustomModelConstraint(definition, dictionaryService));
+ }
+ return constraints;
+ }
+
+ /**
+ * Converts the given {@code ModelDetails} object into a {@link M2Model} object
+ *
+ * @param modelDetails the custom model details
+ * @return {@link M2Model} object
+ */
+ private M2Model convertToM2Model(ModelDetails modelDetails)
+ {
+ return convertToM2Model(modelDetails.getModel(), modelDetails.getTypes(), modelDetails.getAspects(), modelDetails.getModelDefinedConstraints());
+ }
+
+ /**
+ * Converts the given {@code org.alfresco.rest.api.model.CustomModel}
+ * object, a collection of {@code org.alfresco.rest.api.model.CustomType}
+ * objects, a collection of
+ * {@code org.alfresco.rest.api.model.CustomAspect} objects, and a collection of
+ * {@code org.alfresco.rest.api.model.CustomModelConstraint} objects into a {@link M2Model} object
+ *
+ * @param customModel the custom model
+ * @param types the custom types
+ * @param aspects the custom aspects
+ * @param constraints the custom constraints
+ * @return {@link M2Model} object
+ */
+ private M2Model convertToM2Model(CustomModel customModel, Collection types, Collection aspects, Collection constraints)
+ {
+ validateBasicModelInput(customModel);
+
+ Set> namespacesToImport = new LinkedHashSet<>();
+
+ final String namespacePrefix = customModel.getNamespacePrefix();
+ final String namespaceURI = customModel.getNamespaceUri();
+ // Construct the model name
+ final String name = constructName(customModel.getName(), namespacePrefix);
+
+ M2Model model = M2Model.createModel(name);
+ model.createNamespace(namespaceURI, namespacePrefix);
+ model.setDescription(customModel.getDescription());
+ String author = customModel.getAuthor();
+ if (author == null)
+ {
+ author = getCurrentUserFullName();
+ }
+ model.setAuthor(author);
+
+ // Types
+ if(types != null)
+ {
+ for(CustomType type : types)
+ {
+ validateName(type.getName(), TYPE_NAME_NULL_ERR);
+ M2Type m2Type = model.createType(constructName(type.getName(), namespacePrefix));
+ m2Type.setDescription(type.getDescription());
+ m2Type.setTitle(type.getTitle());
+ setParentName(m2Type, type.getParentName(), namespacesToImport, namespacePrefix);
+ setM2Properties(m2Type, type.getProperties(), namespacePrefix, namespacesToImport);
+ }
+ }
+
+ // Aspects
+ if(aspects != null)
+ {
+ for(CustomAspect aspect : aspects)
+ {
+ validateName(aspect.getName(), ASPECT_NAME_NULL_ERR);
+ M2Aspect m2Aspect = model.createAspect(constructName(aspect.getName(), namespacePrefix));
+ m2Aspect.setDescription(aspect.getDescription());
+ m2Aspect.setTitle(aspect.getTitle());
+ setParentName(m2Aspect, aspect.getParentName(), namespacesToImport, namespacePrefix);
+ setM2Properties(m2Aspect, aspect.getProperties(), namespacePrefix, namespacesToImport);
+ }
+ }
+
+ // Constraints
+ if(constraints != null)
+ {
+ for (CustomModelConstraint constraint : constraints)
+ {
+ validateName(constraint.getName(), CONSTRAINT_NAME_NULL_ERR);
+ final String constraintName = constructName(constraint.getName(), namespacePrefix);
+ M2Constraint m2Constraint = model.createConstraint(constraintName, constraint.getType());
+ // Set title, desc and parameters
+ setConstraintOtherData(constraint, m2Constraint, null);
+ }
+ }
+
+ // Add imports
+ for (Pair uriPrefix : namespacesToImport)
+ {
+ // Don't import the already defined namespace
+ if (!namespaceURI.equals(uriPrefix.getFirst()))
+ {
+ model.createImport(uriPrefix.getFirst(), uriPrefix.getSecond());
+ }
+ }
+
+ return model;
+ }
+
+ private void setConstraintOtherData(CustomModelConstraint constraint, M2Constraint m2Constraint, String propDataType)
+ {
+ if (m2Constraint.getType() == null)
+ {
+ throw new InvalidArgumentException("cmm.rest_api.constraint_type_null");
+ }
+
+ ConstraintValidator constraintValidator = ConstraintValidator.findByType(m2Constraint.getType());
+ if (propDataType != null)
+ {
+ // Check if the constraint can be used with given data type
+ constraintValidator.validateUsage(prefixedStringToQname(propDataType));
+ }
+
+ m2Constraint.setTitle(constraint.getTitle());
+ m2Constraint.setDescription(constraint.getDescription());
+ for (CustomModelNamedValue parameter : constraint.getParameters())
+ {
+ validateName(parameter.getName(), "cmm.rest_api.constraint_parameter_name_null");
+ if (parameter.getListValue() != null)
+ {
+ if (propDataType != null && "allowedValues".equals(parameter.getName()))
+ {
+ validateListConstraint(parameter.getListValue(), propDataType);
+ }
+ m2Constraint.createParameter(parameter.getName(), parameter.getListValue());
+ }
+ else
+ {
+ constraintValidator.validate(parameter.getName(), parameter.getSimpleValue());
+ m2Constraint.createParameter(parameter.getName(), parameter.getSimpleValue());
+ }
+ }
+ }
+
+ /*
+ * List constraint is a special case, so can't use the ConstraintValidator.
+ */
+ private void validateListConstraint(List listValue, String propDataType)
+ {
+ for (String value : listValue)
+ {
+ try
+ {
+ // validate list values
+ this.valueDataTypeValidator.validateValue(propDataType, value);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidArgumentException(ex.getMessage());
+ }
+ }
+ }
+
+ private void setM2Properties(M2Class m2Class, List properties, String namespacePrefix,
+ Set> namespacesToImport)
+ {
+ if (properties != null)
+ {
+ for (CustomModelProperty prop : properties)
+ {
+ validateName(prop.getName(), "cmm.rest_api.property_name_null");
+ M2Property m2Property = m2Class.createProperty(constructName(prop.getName(), namespacePrefix));
+ m2Property.setTitle(prop.getTitle());
+ m2Property.setDescription(prop.getDescription());
+ m2Property.setMandatory(prop.isMandatory());
+ m2Property.setMandatoryEnforced(prop.isMandatoryEnforced());
+ m2Property.setMultiValued(prop.isMultiValued());
+ // Set indexing options
+ m2Property.setIndexed(prop.isIndexed());
+ if (Facetable.TRUE == prop.getFacetable())
+ {
+ m2Property.setFacetable(true);
+ }
+ else if (Facetable.FALSE == prop.getFacetable())
+ {
+ m2Property.setFacetable(false);
+ }
+ m2Property.setIndexTokenisationMode(prop.getIndexTokenisationMode());
+
+ String dataType = prop.getDataType();
+ // Default type is d:text
+ if (StringUtils.isBlank(dataType))
+ {
+ dataType = DEFAULT_DATA_TYPE;
+ }
+ else
+ {
+ if (!dataType.contains(":"))
+ {
+ throw new InvalidArgumentException("cmm.rest_api.property_datatype_invalid", new Object[] { dataType });
+ }
+ }
+ namespacesToImport.add(resolveToUriAndPrefix(dataType));
+ try
+ {
+ // validate default values
+ this.valueDataTypeValidator.validateValue(dataType, prop.getDefaultValue());
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidArgumentException(ex.getMessage());
+ }
+ m2Property.setType(dataType);
+ m2Property.setDefaultValue(prop.getDefaultValue());
+
+ // Check for constraints
+ List constraintRefs = prop.getConstraintRefs();
+ List constraints = prop.getConstraints();
+ if (constraintRefs.size() > 0)
+ {
+ for (String ref : constraintRefs)
+ {
+ Pair prefixLocalName = splitPrefixedQName(ref);
+ if (!namespacePrefix.equals(prefixLocalName.getFirst()))
+ {
+ throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.constraint_ref_not_defined", ref));
+ }
+ m2Property.addConstraintRef(ref);
+ }
+ }
+ if(constraints.size() > 0)
+ {
+ for (CustomModelConstraint modelConstraint : constraints)
+ {
+ String constraintName = null;
+ if (modelConstraint.getName() != null)
+ {
+ validateName(modelConstraint.getName(), CONSTRAINT_NAME_NULL_ERR);
+ constraintName = constructName(modelConstraint.getName(), namespacePrefix);
+ }
+ M2Constraint m2Constraint = m2Property.addConstraint(constraintName, modelConstraint.getType());
+ // Set title, desc and parameters
+ setConstraintOtherData(modelConstraint, m2Constraint, dataType);
+ }
+ }
+ }
+ }
+ }
+
+ private void validateBasicModelInput(CustomModel customModel)
+ {
+ // validate model name
+ validateName(customModel.getName(), MODEL_NAME_NULL_ERR);
+
+ // validate model namespace prefix
+ validateName(customModel.getNamespacePrefix(), "cmm.rest_api.model_namespace_prefix_null");
+
+ // validate model namespace URI
+ if (customModel.getNamespaceUri() == null)
+ {
+ throw new InvalidArgumentException("cmm.rest_api.model_namespace_uri_null");
+ }
+ else
+ {
+ Matcher matcher = URI_PATTERN.matcher(customModel.getNamespaceUri());
+ if (!matcher.find())
+ {
+ throw new InvalidArgumentException("cmm.rest_api.model_namespace_uri_invalid");
+ }
+ }
+ }
+
+ private void validateName(String name, String errMsgId)
+ {
+ if (name == null)
+ {
+ if (errMsgId == null)
+ {
+ errMsgId = InvalidArgumentException.DEFAULT_MESSAGE_ID;
+ }
+ throw new InvalidArgumentException(errMsgId);
+ }
+ else
+ {
+ Matcher matcher = NAME_PATTERN.matcher(name);
+ if (!matcher.find())
+ {
+ throw new InvalidArgumentException("cmm.rest_api.input_validation_err", new Object [] {name});
+ }
+ }
+ }
+
+ /**
+ * Checks the current user access rights and throws
+ * {@link PermissionDeniedException} if the user is not a member of the
+ * ALFRESCO_MODEL_ADMINISTRATORS group
+ */
+ private void validateCurrentUser()
+ {
+ String currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
+ if (!customModelService.isModelAdmin(currentUser))
+ {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ /**
+ * Gets the fully authenticated user's full name
+ *
+ * @return user's full name or the user's id if the full name dose not exit
+ */
+ protected String getCurrentUserFullName()
+ {
+ String userName = AuthenticationUtil.getFullyAuthenticatedUser();
+ NodeRef personRef = personService.getPerson(userName, false);
+
+ String firstName = (String) nodeService.getProperty(personRef, ContentModel.PROP_FIRSTNAME);
+ String lastName = (String) nodeService.getProperty(personRef, ContentModel.PROP_LASTNAME);
+
+ String fullName = (firstName != null ? firstName + " " : "") + (lastName != null ? lastName : "");
+
+ return ((StringUtils.isBlank(fullName) ? userName : fullName)).trim();
+ }
+
+ private String constructName(String name, String prefix)
+ {
+ return new StringBuilder(100).append(prefix).append(QName.NAMESPACE_PREFIX).append(name).toString();
+ }
+
+ /**
+ * Gets the namespace URI and prefix from the parent's name, provided that the
+ * given name is of a valid format. The valid format consist of a
+ * namespace prefix, a colon and a name. E.g. sys:localized
+ *
+ * @param parentName the parent name
+ * @return a pair of namespace URI and prefix object
+ */
+ protected Pair resolveToUriAndPrefix(String parentName)
+ {
+ QName qName = prefixedStringToQname(parentName);
+ Collection prefixes = namespaceService.getPrefixes(qName.getNamespaceURI());
+ if (prefixes.size() == 0)
+ {
+ throw new InvalidArgumentException("cmm.rest_api.prefix_not_registered", new Object[] { qName.getNamespaceURI() });
+ }
+ String prefix = prefixes.iterator().next();
+ return new Pair(qName.getNamespaceURI(), prefix);
+ }
+
+ /**
+ * Creates {@link QName} from a valid prefixed string.
+ */
+ private QName prefixedStringToQname(String prefixedQName)
+ {
+ try
+ {
+ return QName.createQName(prefixedQName, namespaceService);
+ }
+ catch (Exception ex)
+ {
+ String msg = ex.getMessage();
+ if (msg == null)
+ {
+ msg = "";
+ }
+ throw new InvalidArgumentException("cmm.rest_api.prefixed_qname_invalid", new Object[] { prefixedQName, msg });
+ }
+ }
+
+ /**
+ * Validates and sets the type's or aspect's parent name
+ *
+ * @param m2Class the {@link M2Type} or {@link M2Aspect} object
+ * @param parentPrefixedName the parent prefixed name. E.g. prefix:localName
+ * @param namespacesToImport the {@link Set} of namespace pairs to import
+ * @param modelNamespacePrefix the model namespace prefix
+ */
+ private void setParentName(M2Class m2Class, String parentPrefixedName, Set> namespacesToImport, String modelNamespacePrefix)
+ {
+ if (StringUtils.isBlank(parentPrefixedName))
+ {
+ return;
+ }
+
+ Pair prefixLocaNamePair = splitPrefixedQName(parentPrefixedName);
+ if (!modelNamespacePrefix.equals(prefixLocaNamePair.getFirst()))
+ {
+ // Add to the list of imports
+ Pair uriPrefixPair = resolveToUriAndPrefix(parentPrefixedName);
+ namespacesToImport.add(uriPrefixPair);
+ }
+ m2Class.setParentName(parentPrefixedName);
+ }
+
+ private void validateTypeAspectParent(AbstractClassModel typeAspect, CustomModel existingModel)
+ {
+ String parentPrefixedName = typeAspect.getParentName();
+ if (StringUtils.isBlank(parentPrefixedName))
+ {
+ return;
+ }
+
+ Pair prefixLocaNamePair = splitPrefixedQName(parentPrefixedName);
+ String parentPrefix = prefixLocaNamePair.getFirst();
+ String parentLocalName = prefixLocaNamePair.getSecond();
+
+ // Validate parent prefix and localName
+ // We know that the values are not null, we just check against the defined RegEx
+ validateName(parentPrefix, null);
+ validateName(parentLocalName, null);
+
+ final boolean isAspect = (typeAspect instanceof CustomAspect);
+ ClassDefinition classDefinition = null;
+ QName qname = null;
+ if (existingModel.getNamespacePrefix().equals(parentPrefix))
+ {
+ // Check for types/aspects within the model
+ qname = QName.createQName(existingModel.getNamespaceUri(), parentLocalName);
+ classDefinition = (isAspect) ? customModelService.getCustomAspect(qname) : customModelService.getCustomType(qname);
+ }
+ else
+ {
+ // Make sure the namespace URI and Prefix are registered
+ Pair 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");
+ }
+ }
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/AbstractClassModel.java b/source/java/org/alfresco/rest/api/model/AbstractClassModel.java
new file mode 100644
index 0000000000..3acd9adf84
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/AbstractClassModel.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.service.namespace.QName;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public abstract class AbstractClassModel extends AbstractCommonDetails
+{
+ /* package */String parentName;
+ /* package */List properties = Collections.emptyList();
+
+ public String getParentName()
+ {
+ return this.parentName;
+ }
+
+ public void setParentName(String parentName)
+ {
+ this.parentName = parentName;
+ }
+
+ public List getProperties()
+ {
+ return this.properties;
+ }
+
+ public void setProperties(List properties)
+ {
+ this.properties = properties;
+ }
+
+ /* package */ List setList(List sourceList)
+ {
+ if (sourceList == null)
+ {
+ return Collections. emptyList();
+ }
+ return new ArrayList<>(sourceList);
+ }
+
+ /* package */String getParentNameAsString(QName parentQName)
+ {
+ if (parentQName != null)
+ {
+ return parentQName.toPrefixString();
+ }
+ return null;
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/AbstractCommonDetails.java b/source/java/org/alfresco/rest/api/model/AbstractCommonDetails.java
new file mode 100644
index 0000000000..edb0f76bee
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/AbstractCommonDetails.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public abstract class AbstractCommonDetails implements Comparable
+{
+ /* package */String name;
+ /* package */String prefixedName;
+ /* package */String title;
+ /* package */String description;
+
+ public String getName()
+ {
+ return this.name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getPrefixedName()
+ {
+ return this.prefixedName;
+ }
+
+ public String getTitle()
+ {
+ return this.title;
+ }
+
+ public void setTitle(String title)
+ {
+ this.title = title;
+ }
+
+ public String getDescription()
+ {
+ return this.description;
+ }
+
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (!(obj instanceof CustomModelConstraint))
+ {
+ return false;
+ }
+ CustomModelConstraint other = (CustomModelConstraint) obj;
+ if (this.name == null)
+ {
+ if (other.name != null)
+ {
+ return false;
+ }
+ }
+ else if (!this.name.equals(other.name))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(AbstractCommonDetails other)
+ {
+ return this.name.compareTo(other.getName());
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/CustomAspect.java b/source/java/org/alfresco/rest/api/model/CustomAspect.java
new file mode 100644
index 0000000000..65a07a1bf3
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/CustomAspect.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.List;
+
+import org.alfresco.service.cmr.dictionary.AspectDefinition;
+import org.alfresco.service.cmr.i18n.MessageLookup;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomAspect extends AbstractClassModel
+{
+
+ public CustomAspect()
+ {
+ }
+
+ public CustomAspect(AspectDefinition aspectDefinition, MessageLookup messageLookup, List properties)
+ {
+ this.name = aspectDefinition.getName().getLocalName();
+ this.prefixedName = aspectDefinition.getName().toPrefixString();
+ this.title = aspectDefinition.getTitle(messageLookup);
+ this.description = aspectDefinition.getDescription(messageLookup);
+ this.parentName = getParentNameAsString(aspectDefinition.getParentName());
+ this.properties = setList(properties);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(512);
+ builder.append("CustomAspect [name=").append(this.name)
+ .append(", prefixedName=").append(this.prefixedName)
+ .append(", title=").append(this.title)
+ .append(", description=").append(this.description)
+ .append(", parentName=").append(parentName)
+ .append(", properties=").append(properties)
+ .append(']');
+ return builder.toString();
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/CustomModel.java b/source/java/org/alfresco/rest/api/model/CustomModel.java
new file mode 100644
index 0000000000..35e7852da6
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/CustomModel.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.List;
+
+import org.alfresco.service.cmr.dictionary.CustomModelDefinition;
+import org.alfresco.service.cmr.dictionary.NamespaceDefinition;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModel implements Comparable
+{
+ public static enum ModelStatus
+ {
+ ACTIVE, DRAFT
+ }
+
+ private String name;
+ private String author;
+ private String description;
+ private ModelStatus status;
+ private String namespaceUri;
+ private String namespacePrefix;
+ private List types;
+ private List aspects;
+ private List constraints;
+
+ public CustomModel()
+ {
+ }
+
+ public CustomModel(CustomModelDefinition modelDefinition)
+ {
+ this(modelDefinition, null, null, null);
+ }
+
+ public CustomModel(CustomModelDefinition modelDefinition, List types, List aspects, List constraints)
+ {
+ this.name = modelDefinition.getName().getLocalName();
+ this.author = modelDefinition.getAuthor();
+ this.description = modelDefinition.getDescription();
+ this.status = modelDefinition.isActive() ? ModelStatus.ACTIVE : ModelStatus.DRAFT;
+ // we don't need to check for NoSuchElementException, as we don't allow
+ // the model to be saved without a valid namespace
+ NamespaceDefinition nsd = modelDefinition.getNamespaces().iterator().next();
+ this.namespaceUri = nsd.getUri();
+ this.namespacePrefix = nsd.getPrefix();
+ this.types = types;
+ this.aspects = aspects;
+ this.constraints = constraints;
+ }
+
+ public String getName()
+ {
+ return this.name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getAuthor()
+ {
+ return this.author;
+ }
+
+ public void setAuthor(String author)
+ {
+ this.author = author;
+ }
+
+ public String getDescription()
+ {
+ return this.description;
+ }
+
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ public ModelStatus getStatus()
+ {
+ return this.status;
+ }
+
+ public void setStatus(ModelStatus status)
+ {
+ this.status = status;
+ }
+
+ public String getNamespaceUri()
+ {
+ return this.namespaceUri;
+ }
+
+ public void setNamespaceUri(String namespaceUri)
+ {
+ this.namespaceUri = namespaceUri;
+ }
+
+ public String getNamespacePrefix()
+ {
+ return this.namespacePrefix;
+ }
+
+ public void setNamespacePrefix(String namespacePrefix)
+ {
+ this.namespacePrefix = namespacePrefix;
+ }
+
+ 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 getConstraints()
+ {
+ return this.constraints;
+ }
+
+ public void setConstraints(List constraints)
+ {
+ this.constraints = constraints;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (!(obj instanceof CustomModel))
+ {
+ return false;
+ }
+ CustomModel other = (CustomModel) obj;
+ if (this.name == null)
+ {
+ if (other.name != null)
+ {
+ return false;
+ }
+ }
+ else if (!this.name.equals(other.name))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(CustomModel customModel)
+ {
+ return this.name.compareTo(customModel.getName());
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(512);
+ builder.append("CustomModel [name=").append(this.name).append(", author=").append(this.author)
+ .append(", description=").append(this.description).append(", status=").append(this.status)
+ .append(", namespaceUri=").append(this.namespaceUri).append(", namespacePrefix=")
+ .append(this.namespacePrefix).append(']');
+ return builder.toString();
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/CustomModelConstraint.java b/source/java/org/alfresco/rest/api/model/CustomModelConstraint.java
new file mode 100644
index 0000000000..481e025c20
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/CustomModelConstraint.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.alfresco.service.cmr.dictionary.ConstraintDefinition;
+import org.alfresco.service.cmr.i18n.MessageLookup;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModelConstraint extends AbstractCommonDetails
+{
+ private String type;
+ private List parameters = Collections.emptyList();
+
+ public CustomModelConstraint()
+ {
+ }
+
+ public CustomModelConstraint(ConstraintDefinition constraintDefinition, MessageLookup messageLookup)
+ {
+ this.name = constraintDefinition.getName().getLocalName();
+ this.prefixedName = constraintDefinition.getConstraint().getShortName();
+ this.type = constraintDefinition.getConstraint().getType();
+ this.title = constraintDefinition.getTitle(messageLookup);
+ this.description = constraintDefinition.getDescription(messageLookup);
+ this.parameters = convertToNamedValue(constraintDefinition.getConstraint().getParameters());
+ }
+
+ private List convertToNamedValue(Map params)
+ {
+ List list = new ArrayList<>(params.size());
+ for (Entry en : params.entrySet())
+ {
+ list.add(new CustomModelNamedValue(en.getKey(), en.getValue()));
+ }
+
+ return list;
+ }
+
+ public String getType()
+ {
+ return this.type;
+ }
+
+ public void setType(String type)
+ {
+ this.type = type;
+ }
+
+ public List getParameters()
+ {
+ return this.parameters;
+ }
+
+ public void setParameters(List parameters)
+ {
+ this.parameters = parameters;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(300);
+ builder.append("CustomModelConstraint [name=").append(this.name)
+ .append(", prefixedName=").append(this.prefixedName)
+ .append(", type=").append(this.type)
+ .append(", title=").append(this.title)
+ .append(", description=").append(this.description)
+ .append(", parameters=").append(this.parameters)
+ .append(']');
+ return builder.toString();
+ }
+}
\ No newline at end of file
diff --git a/source/java/org/alfresco/rest/api/model/CustomModelDownload.java b/source/java/org/alfresco/rest/api/model/CustomModelDownload.java
new file mode 100644
index 0000000000..99cee5234c
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/CustomModelDownload.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import org.alfresco.service.cmr.repository.NodeRef;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModelDownload implements Comparable
+{
+ private String nodeRef;
+
+ public CustomModelDownload()
+ {
+ }
+
+ public CustomModelDownload(NodeRef nodeRef)
+ {
+ this.nodeRef = nodeRef.toString();
+ }
+
+ public String getNodeRef()
+ {
+ return this.nodeRef;
+ }
+
+ public void setNodeRef(String nodeRef)
+ {
+ this.nodeRef = nodeRef;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.nodeRef == null) ? 0 : this.nodeRef.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (!(obj instanceof CustomModelDownload))
+ {
+ return false;
+ }
+ CustomModelDownload other = (CustomModelDownload) obj;
+ if (this.nodeRef == null)
+ {
+ if (other.nodeRef != null)
+ {
+ return false;
+ }
+ }
+ else if (!this.nodeRef.equals(other.nodeRef))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(CustomModelDownload other)
+ {
+ return this.nodeRef.toString().compareTo(other.getNodeRef().toString());
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/CustomModelNamedValue.java b/source/java/org/alfresco/rest/api/model/CustomModelNamedValue.java
new file mode 100644
index 0000000000..a2c4b26f6e
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/CustomModelNamedValue.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
+import org.alfresco.service.cmr.repository.datatype.TypeConversionException;
+
+/**
+ *
+ *
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModelNamedValue implements Comparable
+{
+ private String name;
+ private String simpleValue = null;
+ private List listValue = null;
+
+ public CustomModelNamedValue()
+ {
+ }
+
+ public CustomModelNamedValue(String name, Object value)
+ {
+ this.name = name;
+ if (value instanceof List>)
+ {
+ List> values = (List>) value;
+ listValue = new ArrayList<>(values.size());
+ for(Object val : values)
+ {
+ listValue.add(convertToString(val));
+ }
+ }
+ else
+ {
+ simpleValue = convertToString(value);
+ }
+ }
+
+ private String convertToString(Object value)
+ {
+ try
+ {
+ return DefaultTypeConverter.INSTANCE.convert(String.class, value);
+ }
+ catch (TypeConversionException e)
+ {
+ throw new InvalidArgumentException("Cannot convert to string '" + value + "'.");
+ }
+ }
+
+ public String getName()
+ {
+ return this.name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getSimpleValue()
+ {
+ return this.simpleValue;
+ }
+
+ public void setSimpleValue(String simpleValue)
+ {
+ this.simpleValue = simpleValue;
+ }
+
+ public List getListValue()
+ {
+ return this.listValue;
+ }
+
+ public void setListValue(List listValue)
+ {
+ this.listValue = listValue;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (!(obj instanceof CustomModelNamedValue))
+ {
+ return false;
+ }
+ CustomModelNamedValue other = (CustomModelNamedValue) obj;
+ if (this.name == null)
+ {
+ if (other.name != null)
+ {
+ return false;
+ }
+ }
+ else if (!this.name.equals(other.name))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(CustomModelNamedValue other)
+ {
+ return name.compareTo(other.getName());
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(120);
+ builder.append("CustomModelNamedValue [name=").append(this.name)
+ .append(", simpleValue=").append(this.simpleValue)
+ .append(", listValue=").append(this.listValue)
+ .append(']');
+ return builder.toString();
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/CustomModelProperty.java b/source/java/org/alfresco/rest/api/model/CustomModelProperty.java
new file mode 100644
index 0000000000..5ae137d9d1
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/CustomModelProperty.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.repo.dictionary.Facetable;
+import org.alfresco.repo.dictionary.IndexTokenisationMode;
+import org.alfresco.service.cmr.dictionary.ConstraintDefinition;
+import org.alfresco.service.cmr.dictionary.PropertyDefinition;
+import org.alfresco.service.cmr.i18n.MessageLookup;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModelProperty extends AbstractCommonDetails
+{
+ private String dataType;
+ private boolean isMandatory;
+ private boolean isMandatoryEnforced;
+ private boolean isMultiValued;
+ private String defaultValue;
+ private boolean isIndexed = true;
+ private Facetable facetable = Facetable.UNSET;
+ private IndexTokenisationMode indexTokenisationMode;
+ private List constraintRefs = Collections.emptyList();
+ private List constraints = Collections.emptyList();
+
+ public CustomModelProperty()
+ {
+ }
+
+ public CustomModelProperty(PropertyDefinition propertyDefinition, MessageLookup messageLookup)
+ {
+ this.name = propertyDefinition.getName().getLocalName();
+ this.prefixedName = propertyDefinition.getName().toPrefixString();
+ this.title = propertyDefinition.getTitle(messageLookup);
+ this.dataType = propertyDefinition.getDataType().getName().toPrefixString();
+ this.description = propertyDefinition.getDescription(messageLookup);
+ this.isMandatory = propertyDefinition.isMandatory();
+ this.isMandatoryEnforced = propertyDefinition.isMandatoryEnforced();
+ this.isMultiValued = propertyDefinition.isMultiValued();
+ this.defaultValue = propertyDefinition.getDefaultValue();
+ this.isIndexed = propertyDefinition.isIndexed();
+ this.facetable = propertyDefinition.getFacetable();
+ this.indexTokenisationMode = propertyDefinition.getIndexTokenisationMode();
+ List constraintDefs = propertyDefinition.getConstraints();
+ if (constraintDefs.size() > 0)
+ {
+ this.constraintRefs = new ArrayList<>();
+ this.constraints = new ArrayList<>();
+ for (ConstraintDefinition cd : constraintDefs)
+ {
+ if (cd.getRef() != null)
+ {
+ constraintRefs.add(cd.getRef().toPrefixString());
+ }
+ else
+ {
+ constraints.add(new CustomModelConstraint(cd, messageLookup));
+ }
+ }
+ }
+ }
+
+ public String getDataType()
+ {
+ return this.dataType;
+ }
+
+ public void setDataType(String dataType)
+ {
+ this.dataType = dataType;
+ }
+
+ public boolean isMandatory()
+ {
+ return this.isMandatory;
+ }
+
+ public void setMandatory(boolean isMandatory)
+ {
+ this.isMandatory = isMandatory;
+ }
+
+ public boolean isMandatoryEnforced()
+ {
+ return this.isMandatoryEnforced;
+ }
+
+ public void setMandatoryEnforced(boolean isMandatoryEnforced)
+ {
+ this.isMandatoryEnforced = isMandatoryEnforced;
+ }
+
+ public boolean isMultiValued()
+ {
+ return this.isMultiValued;
+ }
+
+ public void setMultiValued(boolean isMultiValued)
+ {
+ this.isMultiValued = isMultiValued;
+ }
+
+ public String getDefaultValue()
+ {
+ return this.defaultValue;
+ }
+
+ public void setDefaultValue(String defaultValue)
+ {
+ this.defaultValue = defaultValue;
+ }
+
+ public boolean isIndexed()
+ {
+ return this.isIndexed;
+ }
+
+ public void setIndexed(boolean isIndexed)
+ {
+ this.isIndexed = isIndexed;
+ }
+
+ public Facetable getFacetable()
+ {
+ return this.facetable;
+ }
+
+ public void setFacetable(Facetable facetable)
+ {
+ this.facetable = facetable;
+ }
+
+ public IndexTokenisationMode getIndexTokenisationMode()
+ {
+ return this.indexTokenisationMode;
+ }
+
+ public void setIndexTokenisationMode(IndexTokenisationMode indexTokenisationMode)
+ {
+ this.indexTokenisationMode = indexTokenisationMode;
+ }
+
+ public List getConstraintRefs()
+ {
+ return this.constraintRefs;
+ }
+
+ public void setConstraintRefs(List constraintRefs)
+ {
+ this.constraintRefs = constraintRefs;
+ }
+
+ public List getConstraints()
+ {
+ return this.constraints;
+ }
+
+ public void setConstraints(List constraints)
+ {
+ this.constraints = constraints;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(612);
+ builder.append("CustomModelProperty [name=").append(this.name)
+ .append(", prefixedName=").append(this.prefixedName)
+ .append(", title=").append(this.title)
+ .append(", description=").append(this.description)
+ .append(", dataType=").append(this.dataType)
+ .append(", isMandatory=").append(this.isMandatory)
+ .append(", isMandatoryEnforced=").append(this.isMandatoryEnforced)
+ .append(", isMultiValued=").append(this.isMultiValued)
+ .append(", defaultValue=").append(this.defaultValue)
+ .append(", isIndexed=").append(this.isIndexed)
+ .append(", facetable=").append(this.facetable)
+ .append(", indexTokenisationMode=").append(this.indexTokenisationMode)
+ .append(", constraintRefs=").append(this.constraintRefs)
+ .append(", constraints=").append(this.constraints)
+ .append(']');
+ return builder.toString();
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/model/CustomType.java b/source/java/org/alfresco/rest/api/model/CustomType.java
new file mode 100644
index 0000000000..4e9f0e9d71
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/CustomType.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.List;
+
+import org.alfresco.service.cmr.dictionary.TypeDefinition;
+import org.alfresco.service.cmr.i18n.MessageLookup;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomType extends AbstractClassModel
+{
+
+ public CustomType()
+ {
+ }
+
+ public CustomType(TypeDefinition typeDefinition, MessageLookup messageLookup, List properties)
+ {
+ this.name = typeDefinition.getName().getLocalName();
+ this.prefixedName = typeDefinition.getName().toPrefixString();
+ this.title = typeDefinition.getTitle(messageLookup);
+ this.description = typeDefinition.getDescription(messageLookup);
+ this.parentName = getParentNameAsString(typeDefinition.getParentName());
+ this.properties = setList(properties);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(512);
+ builder.append("CustomType [name=").append(this.name)
+ .append(", prefixedName=").append(this.prefixedName)
+ .append(", title=").append(this.title)
+ .append(", description=").append(this.description)
+ .append(", parentName=").append(parentName)
+ .append(", properties=").append(properties)
+ .append(']');
+ return builder.toString();
+ }
+}
diff --git a/source/test-java/org/alfresco/RemoteApi01TestSuite.java b/source/test-java/org/alfresco/RemoteApi01TestSuite.java
index 642670e5c5..13a3e80117 100644
--- a/source/test-java/org/alfresco/RemoteApi01TestSuite.java
+++ b/source/test-java/org/alfresco/RemoteApi01TestSuite.java
@@ -93,4 +93,9 @@ public class RemoteApi01TestSuite extends TestSuite
suite.addTest(new JUnit4TestAdapter(org.alfresco.rest.workflow.api.tests.ProcessWorkflowApiTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.rest.workflow.api.tests.TaskWorkflowApiTest.class));
}
+
+ static void tests8(TestSuite suite) //
+ {
+ suite.addTest(org.alfresco.rest.api.tests.CMMApiTestSuite.suite());
+ }
}
\ No newline at end of file
diff --git a/source/test-java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java b/source/test-java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java
index ac1177e144..04b99c8c5d 100644
--- a/source/test-java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java
+++ b/source/test-java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java
@@ -28,6 +28,7 @@ import org.alfresco.repo.web.scripts.admin.AdminWebScriptTest;
import org.alfresco.repo.web.scripts.audit.AuditWebScriptTest;
import org.alfresco.repo.web.scripts.blogs.BlogServiceTest;
import org.alfresco.repo.web.scripts.comment.CommentsApiTest;
+import org.alfresco.repo.web.scripts.custommodel.CustomModelImportTest;
import org.alfresco.repo.web.scripts.dictionary.DictionaryRestApiTest;
import org.alfresco.repo.web.scripts.discussion.DiscussionRestApiTest;
import org.alfresco.repo.web.scripts.facet.FacetRestApiTest;
@@ -98,12 +99,13 @@ public class WebScriptTestSuite extends TestSuite
suite.addTestSuite( SOLRWebScriptTest.class );
suite.addTestSuite( SubscriptionServiceRestApiTest.class );
suite.addTestSuite( FacetRestApiTest.class );
- suite.addTestSuite( CommentsApiTest.class );
+ suite.addTestSuite( CommentsApiTest.class );
suite.addTestSuite( DeclarativeSpreadsheetWebScriptTest.class );
suite.addTestSuite( XssVulnerabilityTest.class );
suite.addTestSuite( LinksRestApiTest.class );
suite.addTestSuite( RemoteFileFolderLoaderTest.class );
suite.addTestSuite( ReadOnlyTransactionInGetRestApiTest.class );
+ suite.addTestSuite( CustomModelImportTest.class );
// This uses a slightly different context
// As such, we can't run it in the same suite as the others,
// due to finalisers closing caches when we're not looking
diff --git a/source/test-java/org/alfresco/repo/web/scripts/custommodel/CustomModelImportTest.java b/source/test-java/org/alfresco/repo/web/scripts/custommodel/CustomModelImportTest.java
new file mode 100644
index 0000000000..512e22dcf5
--- /dev/null
+++ b/source/test-java/org/alfresco/repo/web/scripts/custommodel/CustomModelImportTest.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.repo.web.scripts.custommodel;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.dictionary.CustomModelServiceImpl;
+import org.alfresco.repo.dictionary.M2Association;
+import org.alfresco.repo.dictionary.M2Model;
+import org.alfresco.repo.dictionary.M2Type;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.repo.web.scripts.BaseWebScriptTest;
+import org.alfresco.service.cmr.dictionary.CustomModelService;
+import org.alfresco.service.cmr.security.AuthorityService;
+import org.alfresco.service.cmr.security.AuthorityType;
+import org.alfresco.service.cmr.security.MutableAuthenticationService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.PropertyMap;
+import org.alfresco.util.TempFileProvider;
+import org.alfresco.util.XMLUtil;
+import org.apache.commons.httpclient.methods.multipart.FilePart;
+import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
+import org.apache.commons.httpclient.methods.multipart.Part;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.Response;
+import org.springframework.util.ResourceUtils;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+/**
+ * This class tests the custom model upload REST API.
+ *
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CustomModelImportTest extends BaseWebScriptTest
+{
+ private static final String NON_ADMIN_USER = "nonAdminUserName";
+ private static final String CUSTOM_MODEL_ADMIN = "customModelAdmin";
+ private static final String RESOURCE_PREFIX = "custommodel/";
+ private static final String UPLOAD_URL = "/api/cmm/upload";
+ private static final int BUFFER_SIZE = 20 * 1024;
+
+ private MutableAuthenticationService authenticationService;
+ private AuthorityService authorityService;
+ private PersonService personService;
+ private RetryingTransactionHelper transactionHelper;
+ private CustomModelService customModelService;
+ private List importedModels = new ArrayList<>();
+ private List tempFiles = new ArrayList<>();
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+ authenticationService = getServer().getApplicationContext().getBean("AuthenticationService", MutableAuthenticationService.class);
+ authorityService = getServer().getApplicationContext().getBean("AuthorityService", AuthorityService.class);
+ personService = getServer().getApplicationContext().getBean("PersonService", PersonService.class);
+ transactionHelper = getServer().getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class);
+ customModelService = getServer().getApplicationContext().getBean("customModelService", CustomModelService.class);
+
+ AuthenticationUtil.clearCurrentSecurityContext();
+
+ AuthenticationUtil.runAsSystem(new RunAsWork()
+ {
+ @Override
+ public Void doWork() throws Exception
+ {
+ createUser(NON_ADMIN_USER);
+ createUser(CUSTOM_MODEL_ADMIN);
+
+ if (!authorityService.getContainingAuthorities(AuthorityType.GROUP, CUSTOM_MODEL_ADMIN, true).contains(
+ CustomModelServiceImpl.GROUP_ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY))
+ {
+ authorityService.addAuthority(CustomModelServiceImpl.GROUP_ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY, CUSTOM_MODEL_ADMIN);
+ }
+ return null;
+ }
+ });
+ AuthenticationUtil.setFullyAuthenticatedUser(CUSTOM_MODEL_ADMIN);
+ }
+
+ @Override
+ public void tearDown() throws Exception
+ {
+ for (File file : tempFiles)
+ {
+ file.delete();
+ }
+
+ transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
+ {
+ public Void execute() throws Throwable
+ {
+ for (String modelName : importedModels)
+ {
+ customModelService.deleteCustomModel(modelName);
+ }
+ return null;
+ }
+ });
+
+ AuthenticationUtil.runAsSystem(new RunAsWork()
+ {
+ @Override
+ public Void doWork() throws Exception
+ {
+ transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
+ {
+ public Void execute() throws Throwable
+ {
+ deleteUser(NON_ADMIN_USER);
+ deleteUser(CUSTOM_MODEL_ADMIN);
+ return null;
+ }
+ });
+ return null;
+ }
+ });
+
+ AuthenticationUtil.clearCurrentSecurityContext();
+
+ super.tearDown();
+ }
+
+ public void testValidUpload_ModelAndExtModule() throws Exception
+ {
+ File zipFile = getResourceFile("validModelAndExtModule.zip");
+ PostRequest postRequest = buildMultipartPostRequest(zipFile);
+
+ AuthenticationUtil.setFullyAuthenticatedUser(NON_ADMIN_USER);
+ Response response = sendRequest(postRequest, 403);
+
+ AuthenticationUtil.setFullyAuthenticatedUser(CUSTOM_MODEL_ADMIN);
+ response = sendRequest(postRequest, 200);
+
+ JSONObject json = new JSONObject(new JSONTokener(response.getContentAsString()));
+
+ String importedModelName = json.getString("modelName");
+ importedModels.add(importedModelName);
+
+ String extModule = json.getString("shareExtModule");
+ Document document = XMLUtil.parse(extModule);
+ NodeList nodes = document.getElementsByTagName("id");
+ assertEquals(1, nodes.getLength());
+ assertNotNull(nodes.item(0).getTextContent());
+ }
+
+ public void testValidUpload_ModelOnly() throws Exception
+ {
+ File zipFile = getResourceFile("validModel.zip");
+ PostRequest postRequest = buildMultipartPostRequest(zipFile);
+
+ AuthenticationUtil.setFullyAuthenticatedUser(NON_ADMIN_USER);
+ Response response = sendRequest(postRequest, 403);
+
+ AuthenticationUtil.setFullyAuthenticatedUser(CUSTOM_MODEL_ADMIN);
+ response = sendRequest(postRequest, 200);
+
+ JSONObject json = new JSONObject(new JSONTokener(response.getContentAsString()));
+ String importedModelName = json.getString("modelName");
+ importedModels.add(importedModelName);
+
+ assertFalse(json.has("shareExtModule"));
+
+ // Import the same model again
+ sendRequest(postRequest, 409); // name conflict
+ }
+
+ public void testValidUpload_ExtModuleOnly() throws Exception
+ {
+ File zipFile = getResourceFile("validExtModule.zip");
+ PostRequest postRequest = buildMultipartPostRequest(zipFile);
+
+ AuthenticationUtil.setFullyAuthenticatedUser(NON_ADMIN_USER);
+ Response response = sendRequest(postRequest, 403);
+
+ AuthenticationUtil.setFullyAuthenticatedUser(CUSTOM_MODEL_ADMIN);
+ response = sendRequest(postRequest, 200);
+
+ JSONObject json = new JSONObject(new JSONTokener(response.getContentAsString()));
+ assertFalse(json.has("modelName"));
+
+ String extModule = json.getString("shareExtModule");
+ Document document = XMLUtil.parse(extModule);
+ NodeList nodes = document.getElementsByTagName("id");
+ assertEquals(1, nodes.getLength());
+ assertNotNull(nodes.item(0).getTextContent());
+ }
+
+ public void testNotZipFileUpload() throws Exception
+ {
+ File file = getResourceFile("validModel.zip");
+ ZipFile zipFile = new ZipFile(file);
+ ZipEntry zipEntry = zipFile.entries().nextElement();
+
+ File unzippedModelFile = TempFileProvider.createTempFile(zipFile.getInputStream(zipEntry), "validModel", ".xml");
+ tempFiles.add(unzippedModelFile);
+ zipFile.close();
+
+ PostRequest postRequest = buildMultipartPostRequest(unzippedModelFile);
+ sendRequest(postRequest, 400); // CMM upload supports only zip file.
+ }
+
+ public void testInvalidZipUpload() throws Exception
+ {
+ String content = ""
+ + "Jane"
+ + "John"
+ + "Upload test"
+ + "This is an invalid model or a Share extension module"
+ +"";
+
+ ZipEntryContext context = new ZipEntryContext("invalidFormat.xml", content.getBytes());
+ File zipFile = createZip(context);
+
+ PostRequest postRequest = buildMultipartPostRequest(zipFile);
+ sendRequest(postRequest, 400); // Invalid. Neither a model nor a Share extension module file
+ }
+
+ public void testUploadModel_Invalid() throws Exception
+ {
+ long timestamp = System.currentTimeMillis();
+ final String modelName = getClass().getSimpleName() + timestamp;
+ final String prefix = "prefix" + timestamp;
+ final String uri = "uriNamespace" + timestamp;
+
+ M2Model model = M2Model.createModel(prefix + QName.NAMESPACE_PREFIX + modelName);
+ model.setAuthor("Admin");
+ model.setDescription("Desc");
+
+ ByteArrayOutputStream xml = new ByteArrayOutputStream();
+ model.toXML(xml);
+ ZipEntryContext context = new ZipEntryContext(modelName + ".xml", xml.toByteArray());
+ File zipFile = createZip(context);
+
+ PostRequest postRequest = buildMultipartPostRequest(zipFile);
+ sendRequest(postRequest, 409); // no namespace has been defined
+
+ // Create two namespaces
+ model.createNamespace(uri, prefix);
+ model.createNamespace(uri + "anotherUri", prefix + "anotherPrefix");
+ xml = new ByteArrayOutputStream();
+ model.toXML(xml);
+ context = new ZipEntryContext(modelName + ".xml", xml.toByteArray());
+ zipFile = createZip(context);
+
+ postRequest = buildMultipartPostRequest(zipFile);
+ sendRequest(postRequest, 409); // custom model can only have one namespace
+ }
+
+ public void testUploadModel_UnsupportedModelElements() throws Exception
+ {
+ // Note: here we only test a couple of not-supported model elements to check for the correct status code.
+ // This test should be removed when we implement the required support
+
+ long timestamp = System.currentTimeMillis();
+ final String modelName = getClass().getSimpleName() + timestamp;
+ final String prefix = "prefix"+timestamp;
+ final String uri = "uriNamespace"+timestamp;
+ final String aspectName = prefix + QName.NAMESPACE_PREFIX + "testAspec";
+ final String typeName = prefix + QName.NAMESPACE_PREFIX + "testType";
+ final String associationName = prefix + QName.NAMESPACE_PREFIX + "testAssociation";
+
+ M2Model model = M2Model.createModel(prefix + QName.NAMESPACE_PREFIX + modelName);
+ model.createNamespace(uri, prefix);
+ model.setAuthor("John Doe");
+ model.createAspect(aspectName);
+ model.createImport(NamespaceService.CONTENT_MODEL_1_0_URI, NamespaceService.CONTENT_MODEL_PREFIX);
+
+ M2Type type = model.createType(typeName);
+ // Add 'association' not supported yet.
+ M2Association association = type.createAssociation(associationName);
+ association.setSourceMandatory(false);
+ association.setSourceMany(false);
+ association.setTargetMandatory(false);
+ association.setTargetClassName("cm:content");
+
+ ByteArrayOutputStream xml = new ByteArrayOutputStream();
+ model.toXML(xml);
+ ZipEntryContext context = new ZipEntryContext(modelName + ".xml", xml.toByteArray());
+ File zipFile = createZip(context);
+
+ PostRequest postRequest = buildMultipartPostRequest(zipFile);
+ sendRequest(postRequest, 409); // element is not supported yet
+
+ type.removeAssociation(associationName);
+ // Add 'mandatory-aspect' not supported yet.
+ type.addMandatoryAspect(aspectName);
+ xml = new ByteArrayOutputStream();
+ model.toXML(xml);
+ context = new ZipEntryContext(modelName + ".xml", xml.toByteArray());
+ zipFile = createZip(context);
+
+ postRequest = buildMultipartPostRequest(zipFile);
+ sendRequest(postRequest, 409); // element is not supported yet
+ }
+
+ public void testInvalidNumberOfZipEntries() throws Exception
+ {
+ long timestamp = System.currentTimeMillis();
+ String modelName = getClass().getSimpleName() + timestamp;
+ String prefix = "prefix" + timestamp;
+ String uri = "uriNamespace" + timestamp;
+
+ // Model one
+ M2Model modelOne = M2Model.createModel(prefix + QName.NAMESPACE_PREFIX + modelName);
+ modelOne.createNamespace(uri, prefix);
+ modelOne.setDescription("Model 1");
+ ByteArrayOutputStream xml = new ByteArrayOutputStream();
+ modelOne.toXML(xml);
+ ZipEntryContext contextOne = new ZipEntryContext(modelName + ".xml", xml.toByteArray());
+
+ // Model two
+ modelName += "two";
+ prefix += "two";
+ uri += "two";
+ M2Model modelTwo = M2Model.createModel(prefix + QName.NAMESPACE_PREFIX + modelName);
+ modelTwo.createNamespace(uri, prefix);
+ modelTwo.setDescription("Model 2");
+ xml = new ByteArrayOutputStream();
+ modelTwo.toXML(xml);
+ ZipEntryContext contextTwo = new ZipEntryContext(modelName + ".xml", xml.toByteArray());
+
+ // Model three
+ modelName += "three";
+ prefix += "three";
+ uri += "three";
+ M2Model modelThree = M2Model.createModel(prefix + QName.NAMESPACE_PREFIX + modelName);
+ modelThree.createNamespace(uri, prefix);
+ modelThree.setDescription("Model 3");
+ xml = new ByteArrayOutputStream();
+ modelThree.toXML(xml);
+ ZipEntryContext contextThree = new ZipEntryContext(modelName + ".xml", xml.toByteArray());
+
+ File zipFile = createZip(contextOne, contextTwo, contextThree);
+
+ PostRequest postRequest = buildMultipartPostRequest(zipFile);
+ sendRequest(postRequest, 400); // more than two zip entries
+ }
+
+ public PostRequest buildMultipartPostRequest(File file) throws IOException
+ {
+ Part[] parts = { new FilePart("filedata", file.getName(), file, "application/zip", null) };
+
+ MultipartRequestEntity multipartRequestEntity = new MultipartRequestEntity(parts, new HttpMethodParams());
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ multipartRequestEntity.writeRequest(os);
+
+ PostRequest postReq = new PostRequest(UPLOAD_URL, os.toByteArray(), multipartRequestEntity.getContentType());
+ return postReq;
+ }
+
+ private void createUser(String userName)
+ {
+ if (!authenticationService.authenticationExists(userName))
+ {
+ authenticationService.createAuthentication(userName, "PWD".toCharArray());
+ }
+
+ if (!personService.personExists(userName))
+ {
+ PropertyMap ppOne = new PropertyMap(4);
+ ppOne.put(ContentModel.PROP_USERNAME, userName);
+ ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName");
+ ppOne.put(ContentModel.PROP_LASTNAME, "lastName");
+ ppOne.put(ContentModel.PROP_EMAIL, "email@email.com");
+ ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle");
+
+ personService.createPerson(ppOne);
+ }
+ }
+
+ private void deleteUser(String userName)
+ {
+ if (personService.personExists(userName))
+ {
+ personService.deletePerson(userName);
+ }
+ }
+
+ private File getResourceFile(String xmlFileName) throws FileNotFoundException
+ {
+ URL url = CustomModelImportTest.class.getClassLoader().getResource(RESOURCE_PREFIX + xmlFileName);
+ if (url == null)
+ {
+ fail("Cannot get the resource: " + xmlFileName);
+ }
+ return ResourceUtils.getFile(url);
+ }
+
+ private File createZip(ZipEntryContext... zipEntryContexts)
+ {
+ File zipFile = TempFileProvider.createTempFile(getClass().getSimpleName(), ".zip");
+ tempFiles.add(zipFile);
+
+ byte[] buffer = new byte[BUFFER_SIZE];
+ try
+ {
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(zipFile), BUFFER_SIZE);
+ ZipOutputStream zos = new ZipOutputStream(out);
+
+ for (ZipEntryContext context : zipEntryContexts)
+ {
+ ZipEntry zipEntry = new ZipEntry(context.getZipEntryName());
+ zos.putNextEntry(zipEntry);
+
+ InputStream input = context.getEntryContent();
+ int len;
+ while ((len = input.read(buffer)) > 0)
+ {
+ zos.write(buffer, 0, len);
+ }
+ input.close();
+ }
+ zos.closeEntry();
+ zos.close();
+ }
+ catch (IOException ex)
+ {
+ fail("couldn't create zip file.");
+ }
+
+ return zipFile;
+ }
+
+ private static class ZipEntryContext
+ {
+ private final String zipEntryName;
+ private final InputStream entryContent;
+
+ public ZipEntryContext(String zipEntryName, byte[] zipEntryContent)
+ {
+ this.zipEntryName = zipEntryName;
+ this.entryContent = new ByteArrayInputStream(zipEntryContent);
+ }
+
+ public String getZipEntryName()
+ {
+ return this.zipEntryName;
+ }
+
+ public InputStream getEntryContent()
+ {
+ return this.entryContent;
+ }
+ }
+}
diff --git a/source/test-java/org/alfresco/rest/api/tests/BaseCustomModelApiTest.java b/source/test-java/org/alfresco/rest/api/tests/BaseCustomModelApiTest.java
new file mode 100644
index 0000000000..548f2d3285
--- /dev/null
+++ b/source/test-java/org/alfresco/rest/api/tests/BaseCustomModelApiTest.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.tests;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.repo.dictionary.CustomModelServiceImpl;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.rest.api.model.AbstractClassModel;
+import org.alfresco.rest.api.model.CustomAspect;
+import org.alfresco.rest.api.model.CustomModel;
+import org.alfresco.rest.api.model.CustomModelConstraint;
+import org.alfresco.rest.api.model.CustomModelNamedValue;
+import org.alfresco.rest.api.model.CustomModelProperty;
+import org.alfresco.rest.api.model.CustomType;
+import org.alfresco.rest.api.model.CustomModel.ModelStatus;
+import org.alfresco.rest.api.tests.RepoService.TestPerson;
+import org.alfresco.rest.api.tests.client.HttpResponse;
+import org.alfresco.rest.api.tests.client.RequestContext;
+import org.alfresco.rest.api.tests.client.PublicApiClient.Paging;
+import org.alfresco.rest.api.tests.util.RestApiUtil;
+import org.alfresco.service.cmr.dictionary.CustomModelDefinition;
+import org.alfresco.service.cmr.dictionary.CustomModelService;
+import org.alfresco.service.cmr.dictionary.NamespaceDefinition;
+import org.alfresco.service.cmr.security.AuthorityService;
+import org.alfresco.service.cmr.security.MutableAuthenticationService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.util.Pair;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * Base class for CMM API tests
+ *
+ * @author Jamal Kaabi-Mofrad
+ */
+public class BaseCustomModelApiTest extends EnterpriseTestApi
+{
+ public static final String CMM_SCOPE = "private";
+ public static final String SELECT_PROPS_QS = "?select=props";
+ public static final String SELECT_STATUS_QS = "?select=status";
+ public static final String SELECT_ALL = "?select=all";
+ public static final String SELECT_ALL_PROPS = "?select=allProps";
+
+ protected String nonAdminUserName;
+ protected String customModelAdmin;
+
+ protected MutableAuthenticationService authenticationService;
+ protected PersonService personService;
+ protected CustomModelService customModelService;
+
+ private List users = new ArrayList<>();
+
+ @Before
+ public void setup() throws Exception
+ {
+ authenticationService = applicationContext.getBean("authenticationService", MutableAuthenticationService.class);
+ personService = applicationContext.getBean("personService", PersonService.class);
+ customModelService = applicationContext.getBean("customModelService", CustomModelService.class);
+
+ final AuthorityService authorityService = applicationContext.getBean("authorityService", AuthorityService.class);
+
+ this.nonAdminUserName = createUser("nonAdminUser" + System.currentTimeMillis());
+ this.customModelAdmin = createUser("customModelAdmin" + System.currentTimeMillis());
+ users.add(nonAdminUserName);
+ users.add(customModelAdmin);
+ // Add 'customModelAdmin' user into 'ALFRESCO_MODEL_ADMINISTRATORS' group
+ transactionHelper.doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ authorityService.addAuthority(CustomModelServiceImpl.GROUP_ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY, customModelAdmin);
+ return null;
+ }
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser();
+ for (final String user : users)
+ {
+ transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ authenticationService.deleteAuthentication(user);
+ personService.deletePerson(user);
+ return null;
+ }
+ });
+ }
+ users.clear();
+ AuthenticationUtil.clearCurrentSecurityContext();
+ }
+
+ protected String createUser(String username)
+ {
+ PersonInfo personInfo = new PersonInfo(username, username, username, "password", null, null, null, null, null, null, null);
+ TestPerson person = repoService.createUser(personInfo, username, null);
+ return person.getId();
+ }
+
+ protected HttpResponse post(String url, String runAsUser, String body, int expectedStatus) throws Exception
+ {
+ publicApiClient.setRequestContext(new RequestContext(runAsUser));
+
+ HttpResponse response = publicApiClient.post(CMM_SCOPE, url, null, null, null, body);
+ checkStatus(expectedStatus, response.getStatusCode());
+
+ return response;
+ }
+
+ protected HttpResponse post(String url, String runAsUser, String body, String queryString, int expectedStatus) throws Exception
+ {
+ publicApiClient.setRequestContext(new RequestContext(runAsUser));
+ if (queryString != null)
+ {
+ url += queryString;
+ }
+ HttpResponse response = publicApiClient.post(CMM_SCOPE, url, null, null, null, body);
+ checkStatus(expectedStatus, response.getStatusCode());
+
+ return response;
+ }
+
+ protected HttpResponse getAll(String url, String runAsUser, Paging paging, int expectedStatus) throws Exception
+ {
+ publicApiClient.setRequestContext(new RequestContext(runAsUser));
+ Map params = (paging == null) ? null : createParams(paging, null);
+
+ HttpResponse response = publicApiClient.get(CMM_SCOPE, url, null, null, null, params);
+ checkStatus(expectedStatus, response.getStatusCode());
+
+ return response;
+ }
+
+ protected HttpResponse getSingle(String url, String runAsUser, String entityId, int expectedStatus) throws Exception
+ {
+ publicApiClient.setRequestContext(new RequestContext(runAsUser));
+
+ HttpResponse response = publicApiClient.get(CMM_SCOPE, url, entityId, null, null, null);
+ checkStatus(expectedStatus, response.getStatusCode());
+
+ return response;
+ }
+
+ protected HttpResponse put(String url, String runAsUser, String entityId, String body, String queryString, int expectedStatus) throws Exception
+ {
+ publicApiClient.setRequestContext(new RequestContext(runAsUser));
+ if (queryString != null)
+ {
+ entityId += queryString;
+ }
+ HttpResponse response = publicApiClient.put(CMM_SCOPE, url, entityId, null, null, body, null);
+ checkStatus(expectedStatus, response.getStatusCode());
+
+ return response;
+ }
+
+ protected HttpResponse delete(String url, String runAsUser, String entityId, int expectedStatus) throws Exception
+ {
+ publicApiClient.setRequestContext(new RequestContext(runAsUser));
+
+ HttpResponse response = publicApiClient.delete(CMM_SCOPE, url, entityId, null, null);
+ checkStatus(expectedStatus, response.getStatusCode());
+
+ return response;
+ }
+
+ protected void checkStatus(int expectedStatus, int actualStatus)
+ {
+ if (expectedStatus > 0 && expectedStatus != actualStatus)
+ {
+ fail("Status code " + actualStatus + " returned, but expected " + expectedStatus);
+ }
+ }
+
+ protected CustomModel createCustomModel(String modelName, Pair namespacePair, ModelStatus status) throws Exception
+ {
+ return createCustomModel(modelName, namespacePair, status, "Test model description", null);
+ }
+
+ protected CustomModel createCustomModel(String modelName, Pair namespacePair, ModelStatus status, String desc, String author)
+ throws Exception
+ {
+ CustomModel customModel = new CustomModel();
+ customModel.setName(modelName);
+ customModel.setNamespaceUri(namespacePair.getFirst());
+ customModel.setNamespacePrefix(namespacePair.getSecond());
+ customModel.setDescription(desc);
+ customModel.setStatus(status);
+ customModel.setAuthor(author);
+
+ // Create the model as a Model Administrator
+ HttpResponse response = post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 201);
+ CustomModel returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ if (author == null)
+ {
+ // ignore 'author' in the comparison
+ compareCustomModels(customModel, returnedModel, "author");
+ }
+ else
+ {
+ compareCustomModels(customModel, returnedModel);
+ }
+
+ return customModel;
+ }
+
+ protected T createTypeAspect(Class glazz, String modelName, String typeAspectName, String title, String desc,
+ String parent) throws Exception
+ {
+ AbstractClassModel classModel = null;
+ String uri = "cmm/" + modelName;
+ if (glazz.equals(CustomType.class))
+ {
+ classModel = new CustomType();
+ uri += "/types";
+ }
+ else
+ {
+ classModel = new CustomAspect();
+ uri += "/aspects";
+ }
+
+ classModel.setName(typeAspectName);
+ classModel.setDescription(desc);
+ classModel.setTitle(title);
+ classModel.setParentName(parent);
+
+ // Create type as a Model Administrator
+ HttpResponse response = post(uri, customModelAdmin, RestApiUtil.toJsonAsString(classModel), 201);
+ T returnedClassModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), glazz);
+
+ compareCustomTypesAspects(classModel, returnedClassModel, "prefixedName");
+
+ return returnedClassModel;
+ }
+
+ protected void compareCustomModels(CustomModel expectedModel, CustomModel actualModel, String... excludeFields)
+ {
+ boolean result = EqualsBuilder.reflectionEquals(expectedModel, actualModel, excludeFields);
+ assertTrue("Two models are not equal. Expected:<" + expectedModel.toString() + "> but was:<" + actualModel.toString() + ">", result);
+ }
+
+ protected void compareCustomTypesAspects(AbstractClassModel expectedDetails, AbstractClassModel actualDetails, String... excludeFields)
+ {
+ List expectedProps = expectedDetails.getProperties();
+ List actualProps = actualDetails.getProperties();
+ // Sort them
+ sortIfnotNull(expectedProps);
+ sortIfnotNull(actualProps);
+
+ boolean propEqualResult = true;
+ if (expectedProps.size() == actualProps.size())
+ {
+ for (int i = 0, size = expectedProps.size(); i < size; i++)
+ {
+ boolean equalProp = EqualsBuilder.reflectionEquals(expectedProps.get(i), actualProps.get(i), excludeFields);
+ if (!equalProp)
+ {
+ propEqualResult = false;
+ break;
+ }
+ }
+ }
+ else
+ {
+ propEqualResult = false;
+ }
+
+ if (excludeFields.length > 0)
+ {
+ int size = excludeFields.length;
+ excludeFields = Arrays.copyOf(excludeFields, size + 1);
+ excludeFields[size] = "properties";
+ }
+ boolean result = EqualsBuilder.reflectionEquals(expectedDetails, actualDetails, excludeFields);
+
+ String typesAspects = (expectedDetails instanceof CustomAspect) ? "aspects" : "types";
+ assertTrue("Two " + typesAspects + " are not equal. Expected:<" + expectedDetails.toString() + "> but was:<" + actualDetails.toString() + ">",
+ (result && propEqualResult));
+ }
+
+ protected void compareCustomModelConstraints(CustomModelConstraint expectedConstraint, CustomModelConstraint actualConstraint, String... excludeFields)
+ {
+ boolean result = EqualsBuilder.reflectionEquals(expectedConstraint, actualConstraint, excludeFields);
+ assertTrue("Two constraints are not equal. Expected:<" + expectedConstraint.toString() + "> but was:<" + actualConstraint.toString() + ">", result);
+ }
+
+ protected void compareCustomModelProperties(CustomModelProperty expectedProperty, CustomModelProperty actualProperty, String... excludeFields)
+ {
+ boolean result = EqualsBuilder.reflectionEquals(expectedProperty, actualProperty, excludeFields);
+ assertTrue("Two constraints are not equal. Expected:<" + expectedProperty.toString() + "> but was:<" + actualProperty.toString() + ">", result);
+ }
+
+ protected Pair getTestNamespaceUriPrefixPair()
+ {
+ long timeMillis = System.currentTimeMillis();
+ String uri = "http://www.alfresco.org/model/testcmmnamespace" + timeMillis + "/1.0";
+ String prefix = "testcmm" + timeMillis;
+
+ return new Pair(uri, prefix);
+ }
+
+ protected CustomModelDefinition getModelDefinition(final String modelName)
+ {
+ return transactionHelper.doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public CustomModelDefinition execute() throws Throwable
+ {
+ return customModelService.getCustomModel(modelName);
+ }
+ });
+ }
+
+ protected void sortIfnotNull(List list)
+ {
+ if (list != null && list.size() > 0)
+ {
+ Collections.sort(list);
+ }
+ }
+
+ protected boolean hasNamespaceUri(Collection namespaces, String expectedNamespaceUri)
+ {
+ for (NamespaceDefinition ns : namespaces)
+ {
+ if (ns.getUri().equals(expectedNamespaceUri))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected boolean hasNamespacePrefix(Collection namespaces, String expectedNamespacePrefix)
+ {
+ for (NamespaceDefinition ns : namespaces)
+ {
+ if (ns.getPrefix().equals(expectedNamespacePrefix))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected CustomModelProperty getProperty(List properties, String propName)
+ {
+ for (CustomModelProperty prop : properties)
+ {
+ if (prop.getName().equals(propName))
+ {
+ return prop;
+ }
+ }
+ return null;
+ }
+
+ protected CustomModelNamedValue buildNamedValue(String name, String simpleValue, String... listValue)
+ {
+ CustomModelNamedValue namedValue = new CustomModelNamedValue();
+ namedValue.setName(name);
+ namedValue.setSimpleValue(simpleValue);
+ if (listValue.length > 0)
+ {
+ namedValue.setListValue(Arrays.asList(listValue));
+ }
+
+ return namedValue;
+ }
+
+ protected String getParameterSimpleValue(List params, String paramName)
+ {
+ for (CustomModelNamedValue p : params)
+ {
+ if (p.getName().equals(paramName))
+ {
+ return p.getSimpleValue();
+ }
+ }
+ return null;
+ }
+
+ protected List getParameterListValue(List params, String paramName)
+ {
+ for (CustomModelNamedValue p : params)
+ {
+ if (p.getName().equals(paramName))
+ {
+ return p.getListValue();
+ }
+ }
+ return null;
+ }
+}
diff --git a/source/test-java/org/alfresco/rest/api/tests/CMMApiTestSuite.java b/source/test-java/org/alfresco/rest/api/tests/CMMApiTestSuite.java
new file mode 100644
index 0000000000..f85915b0d9
--- /dev/null
+++ b/source/test-java/org/alfresco/rest/api/tests/CMMApiTestSuite.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.tests;
+
+import junit.framework.JUnit4TestAdapter;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class CMMApiTestSuite extends TestSuite
+{
+ /**
+ * Creates the test suite
+ *
+ * @return the test suite
+ */
+ public static Test suite()
+ {
+ TestSuite suite = new TestSuite();
+ suite.addTest(new JUnit4TestAdapter(TestCustomModel.class));
+ suite.addTest(new JUnit4TestAdapter(TestCustomTypeAspect.class));
+ suite.addTest(new JUnit4TestAdapter(TestCustomProperty.class));
+ suite.addTest(new JUnit4TestAdapter(TestCustomConstraint.class));
+ suite.addTest(new JUnit4TestAdapter(TestCustomModelExport.class));
+
+ return suite;
+ }
+}
diff --git a/source/test-java/org/alfresco/rest/api/tests/TestCustomConstraint.java b/source/test-java/org/alfresco/rest/api/tests/TestCustomConstraint.java
new file mode 100644
index 0000000000..12c809f396
--- /dev/null
+++ b/source/test-java/org/alfresco/rest/api/tests/TestCustomConstraint.java
@@ -0,0 +1,1009 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.dictionary.constraint.AbstractConstraint;
+import org.alfresco.repo.tenant.TenantUtil;
+import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork;
+import org.alfresco.rest.api.model.CustomAspect;
+import org.alfresco.rest.api.model.CustomModel;
+import org.alfresco.rest.api.model.CustomModelConstraint;
+import org.alfresco.rest.api.model.CustomModelNamedValue;
+import org.alfresco.rest.api.model.CustomModelProperty;
+import org.alfresco.rest.api.model.CustomType;
+import org.alfresco.rest.api.model.CustomModel.ModelStatus;
+import org.alfresco.rest.api.tests.RepoService.SiteInformation;
+import org.alfresco.rest.api.tests.RepoService.TestNetwork;
+import org.alfresco.rest.api.tests.RepoService.TestPerson;
+import org.alfresco.rest.api.tests.RepoService.TestSite;
+import org.alfresco.rest.api.tests.client.HttpResponse;
+import org.alfresco.rest.api.tests.client.PublicApiClient.Paging;
+import org.alfresco.rest.api.tests.util.RestApiUtil;
+import org.alfresco.service.cmr.dictionary.ConstraintException;
+import org.alfresco.service.cmr.dictionary.CustomModelService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
+import org.alfresco.service.cmr.site.SiteVisibility;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.Pair;
+import org.junit.Test;
+
+/**
+ * Tests the REST API of the constraints of the {@link CustomModelService}.
+ *
+ * @author Jamal Kaabi-Mofrad
+ */
+public class TestCustomConstraint extends BaseCustomModelApiTest
+{
+
+ @Test
+ public void testCreateConstraints() throws Exception
+ {
+ final Paging paging = getPaging(0, Integer.MAX_VALUE);
+
+ String modelName = "testModelConstraint" + System.currentTimeMillis();
+ final Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ createCustomModel(modelName, namespacePair, ModelStatus.DRAFT);
+
+ // Create RegEx constraint
+ {
+ String regExConstraintName = "testFileNameRegEx" + System.currentTimeMillis();
+ CustomModelConstraint regExConstraint = new CustomModelConstraint();
+ regExConstraint.setName(regExConstraintName);
+ regExConstraint.setType("REGEX");
+ regExConstraint.setTitle("test RegEx title");
+ regExConstraint.setDescription("test RegEx desc");
+ // Create the RegEx constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("expression", "(.*[\\\"\\*\\\\\\>\\<\\?\\/\\:\\|]+.*)|(.*[\\.]?.*[\\.]+$)|(.*[ ]+$)"));
+ parameters.add(buildNamedValue("requiresMatch", "false"));
+ // Add the parameters into the constraint
+ regExConstraint.setParameters(parameters);
+
+ // Try to create constraint as a non Admin user
+ post("cmm/" + modelName + "/constraints", nonAdminUserName, RestApiUtil.toJsonAsString(regExConstraint), 403);
+
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(regExConstraint), 201);
+
+ // Retrieve the created RegEx constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, regExConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+ compareCustomModelConstraints(regExConstraint, returnedConstraint, "prefixedName");
+
+ // Try to create a duplicate constraint
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(regExConstraint), 409);
+
+ // Retrieve all the model's constraints
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(1, constraints.size());
+ }
+
+ // Try to create invalid RegEx constraint
+ {
+ String regExConstraintName = "testFileNameInvalidRegEx" + System.currentTimeMillis();
+ CustomModelConstraint regExConstraint = new CustomModelConstraint();
+ regExConstraint.setName(regExConstraintName);
+ regExConstraint.setType("REGEX");
+ // Create the RegEx constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("expression", "*******"));
+ parameters.add(buildNamedValue("requiresMatch", "false"));
+ // Add the parameters into the constraint
+ regExConstraint.setParameters(parameters);
+
+ // Try to create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(regExConstraint), 400);
+ }
+
+ // Create MINMAX constraint
+ {
+ String minMaxConstraintName = "testMinMaxConstraint" + System.currentTimeMillis();
+ CustomModelConstraint minMaxConstraint = new CustomModelConstraint();
+ minMaxConstraint.setName(minMaxConstraintName);
+ minMaxConstraint.setTitle("test MinMax title");
+ minMaxConstraint.setDescription("test MinMax desc");
+ // Create the MinMax constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxValue", "100.0"));
+ parameters.add(buildNamedValue("minValue", "0.0"));
+ // Add the parameters into the constraint
+ minMaxConstraint.setParameters(parameters);
+
+ // Try to create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(minMaxConstraint), 400); // constraint's type is mandatory
+
+ minMaxConstraint.setType("MINMAX");
+ parameters.clear();
+ parameters.add(buildNamedValue("maxValue", "abc")); // invalid number
+ parameters.add(buildNamedValue("minValue", "0.0"));
+ // Add the parameters into the constraint
+ minMaxConstraint.setParameters(parameters);
+ // Try to create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(minMaxConstraint), 400);
+
+ parameters.clear();
+ parameters.add(buildNamedValue("maxValue", "100"));
+ parameters.add(buildNamedValue("minValue", "text")); // invalid number
+ // Add the parameters into the constraint
+ minMaxConstraint.setParameters(parameters);
+ // Try to create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(minMaxConstraint), 400);
+
+ parameters.clear();
+ parameters.add(buildNamedValue("maxValue", "100.0"));
+ parameters.add(buildNamedValue("minValue", "0.0"));
+ // Add the parameters into the constraint
+ minMaxConstraint.setParameters(parameters);
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(minMaxConstraint), 201);
+
+ // Retrieve the created MINMAX constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, minMaxConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+ compareCustomModelConstraints(minMaxConstraint, returnedConstraint, "prefixedName");
+
+ // Retrieve all the model's constraints
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(2, constraints.size());
+ }
+
+ // Create LENGTH constraint
+ {
+ String lengthConstraintName = "testLengthConstraint" + System.currentTimeMillis();
+ CustomModelConstraint lengthConstraint = new CustomModelConstraint();
+ lengthConstraint.setName(lengthConstraintName);
+ lengthConstraint.setType("LENGTH");
+ lengthConstraint.setTitle("test Length title");
+ lengthConstraint.setDescription("test Length desc");
+ // Create the Length constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxLength", "text")); // invalid number
+ parameters.add(buildNamedValue("minLength", "0"));
+ // Add the parameters into the constraint
+ lengthConstraint.setParameters(parameters);
+
+ // Try to create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(lengthConstraint), 400);
+
+ parameters.clear();
+ parameters.add(buildNamedValue("maxLength", "256"));
+ parameters.add(buildNamedValue("minLength", "1.0")); // double number
+ // Add the parameters into the constraint
+ lengthConstraint.setParameters(parameters);
+ // Try to create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(lengthConstraint), 400);
+
+ parameters.clear();
+ parameters.add(buildNamedValue("maxLength", "256"));
+ parameters.add(buildNamedValue("minLength", "0"));
+ // Add the parameters into the constraint
+ lengthConstraint.setParameters(parameters);
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(lengthConstraint), 201);
+
+ // Retrieve the created LENGTH constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, lengthConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+ compareCustomModelConstraints(lengthConstraint, returnedConstraint, "prefixedName");
+
+ // Retrieve all the model's constraints
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(3, constraints.size());
+ }
+
+ // Create LIST constraint
+ {
+ String listConstraintName = "testListConstraint" + System.currentTimeMillis();
+ CustomModelConstraint listConstraint = new CustomModelConstraint();
+ listConstraint.setName(listConstraintName);
+ listConstraint.setType("LIST");
+ listConstraint.setTitle("test List title");
+ listConstraint.setDescription("test List desc");
+ // Create the List constraint's parameters
+ List parameters = new ArrayList<>(3);
+ parameters.add(buildNamedValue("allowedValues", null, "High", "Normal", "Low"));// list value
+ parameters.add(buildNamedValue("sorted", "false"));
+ // Add the parameters into the constraint
+ listConstraint.setParameters(parameters);
+
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(listConstraint), 201);
+
+ // Retrieve the created List constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, listConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+ compareCustomModelConstraints(listConstraint, returnedConstraint, "prefixedName", "parameters");
+ String sorted = getParameterSimpleValue(returnedConstraint.getParameters(), "sorted");
+ assertEquals("false", sorted);
+ List listValues = getParameterListValue(returnedConstraint.getParameters(), "allowedValues");
+ assertNotNull(listValues);
+ assertEquals(3, listValues.size());
+ assertEquals("High", listValues.get(0));
+ assertEquals("Normal", listValues.get(1));
+ assertEquals("Low", listValues.get(2));
+
+ // Retrieve all the model's constraints
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(4, constraints.size());
+ }
+
+ // Create authorityName constraint
+ {
+ String authorityNameConstraintName = "authorityNameConstraint" + System.currentTimeMillis();
+ CustomModelConstraint authorityNameConstraint = new CustomModelConstraint();
+ authorityNameConstraint.setName(authorityNameConstraintName);
+ authorityNameConstraint.setType("org.alfresco.repo.dictionary.constraint.AuthorityNameConstraint");
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(authorityNameConstraint), 201);
+
+ // Retrieve the created authorityName constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, authorityNameConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+ compareCustomModelConstraints(authorityNameConstraint, returnedConstraint, "prefixedName");
+
+ // Retrieve all the model's constraints
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(5, constraints.size());
+ }
+
+ // Create Invalid constraint
+ {
+ String invalidConstraintName = "testInvalidConstraint" + System.currentTimeMillis();
+ CustomModelConstraint invalidConstraint = new CustomModelConstraint();
+ invalidConstraint.setName(invalidConstraintName);
+ invalidConstraint.setType("InvalidConstraintType"+ System.currentTimeMillis());
+ invalidConstraint.setTitle("test Invalid title");
+ invalidConstraint.setDescription("test Invalid desc");
+ // Create the MinMax constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxValue", "100.0"));
+ parameters.add(buildNamedValue("minValue", "0.0"));
+ // Add the parameters into the constraint
+ invalidConstraint.setParameters(parameters);
+
+ // Try to create an invalid constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(invalidConstraint), 400);
+
+ // Retrieve all the model's constraints
+ HttpResponse response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(5, constraints.size());
+ }
+
+ // Activate the model
+ CustomModel updatePayload = new CustomModel();
+ updatePayload.setStatus(ModelStatus.ACTIVE);
+ put("cmm", customModelAdmin, modelName, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 200);
+
+ // Retrieve all the model's constraints
+ HttpResponse response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(5, constraints.size());
+
+ // Deactivate the model
+ updatePayload = new CustomModel();
+ updatePayload.setStatus(ModelStatus.DRAFT);
+ put("cmm", customModelAdmin, modelName, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 200);
+
+ // Retrieve all the model's constraints
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(5, constraints.size());
+ }
+
+ @Test
+ public void testCreateConstraintAndAddToProperty() throws Exception
+ {
+ String modelName = "testModelConstraint" + System.currentTimeMillis();
+ final Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ createCustomModel(modelName, namespacePair, ModelStatus.DRAFT);
+
+ // Create RegEx constraint
+ String regExConstraintName = "testFileNameRegEx" + System.currentTimeMillis();
+ CustomModelConstraint regExConstraint = new CustomModelConstraint();
+ regExConstraint.setName(regExConstraintName);
+ regExConstraint.setType("REGEX");
+ regExConstraint.setTitle("test RegEx title");
+ regExConstraint.setDescription("test RegEx desc");
+ // Create the RegEx constraint's parameters
+ List parameters= new ArrayList<>(2);
+ parameters.add(buildNamedValue("expression", "(.*[\\\"\\*\\\\\\>\\<\\?\\/\\:\\|]+.*)|(.*[\\.]?.*[\\.]+$)|(.*[ ]+$)"));
+ parameters.add(buildNamedValue("requiresMatch", "false"));
+ // Add the parameters into the constraint
+ regExConstraint.setParameters(parameters);
+
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(regExConstraint), 201);
+
+ // Retrieve the created constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, regExConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+
+ // Retrieve all the model's constraints
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(1, constraints.size());
+
+ // Create aspect
+ String aspectName = "testAspect1" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect payload = new CustomAspect();
+ payload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop1" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property title");
+ aspectProp.setDataType("d:text");
+ aspectProp.setConstraintRefs(Arrays.asList(returnedConstraint.getPrefixedName()));// Add the constraint ref
+ List props = new ArrayList<>(1);
+ props.add(aspectProp);
+ payload.setProperties(props);
+
+ // Create the property
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(payload), SELECT_PROPS_QS, 200);
+
+ // Activate the model
+ CustomModel updatePayload = new CustomModel();
+ updatePayload.setStatus(ModelStatus.ACTIVE);
+ put("cmm", customModelAdmin, modelName, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 200);
+
+ // Retrieve all the model's constraints
+ // Test to see if the API took care of duplicate constraints when referencing a constraint within a property.
+ response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals(1, constraints.size());
+
+ // Test RegEx constrain enforcement
+ {
+ final NodeService nodeService = repoService.getNodeService();
+ final QName aspectQName = QName.createQName("{" + namespacePair.getFirst() + "}" + aspectName);
+
+ TestNetwork testNetwork = getTestFixture().getRandomNetwork();
+ TestPerson person = testNetwork.createUser();
+ final String siteName = "site" + System.currentTimeMillis();
+
+ TenantUtil.runAsUserTenant(new TenantRunAsWork()
+ {
+ @Override
+ public Void doWork() throws Exception
+ {
+ SiteInformation siteInfo = new SiteInformation(siteName, siteName, siteName, SiteVisibility.PRIVATE);
+ TestSite site = repoService.createSite(null, siteInfo);
+
+ NodeRef nodeRef = repoService.createDocument(site.getContainerNodeRef("documentLibrary"), "Test Doc", "Test Content");
+
+ nodeService.addAspect(nodeRef, aspectQName, null);
+ assertTrue(nodeService.hasAspect(nodeRef, aspectQName));
+
+ try
+ {
+ QName propQName = QName.createQName("{" + namespacePair.getFirst() + "}" + aspectPropName);
+ nodeService.setProperty(nodeRef, propQName, "Invalid$Char.");
+ fail("Invalid property value. Should have caused integrity violations.");
+ }
+ catch (Exception e)
+ {
+ // Expected
+ }
+
+ // Permanently remove model from repository
+ nodeService.addAspect(nodeRef, ContentModel.ASPECT_TEMPORARY, null);
+ nodeService.deleteNode(nodeRef);
+
+ return null;
+ }
+ }, person.getId(), testNetwork.getId());
+ }
+
+ // Deactivate the model
+ updatePayload = new CustomModel();
+ updatePayload.setStatus(ModelStatus.DRAFT);
+ put("cmm", customModelAdmin, modelName, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 200);
+
+ // Test update the namespace prefix (test to see if the API updates the constraints refs with this new prefix)
+ CustomModel updateModelPayload = new CustomModel();
+ String modifiedPrefix = namespacePair.getSecond() + "Modified";
+ updateModelPayload.setNamespacePrefix(modifiedPrefix);
+ updateModelPayload.setNamespaceUri(namespacePair.getFirst());
+ response = put("cmm", customModelAdmin, modelName, RestApiUtil.toJsonAsString(updateModelPayload), null, 200);
+ CustomModel returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(modifiedPrefix, returnedModel.getNamespacePrefix());
+ assertEquals("The namespace URI shouldn't have changed.", namespacePair.getFirst(), returnedModel.getNamespaceUri());
+
+ // Test update the namespace URI
+ updateModelPayload = new CustomModel();
+ updateModelPayload.setNamespacePrefix(modifiedPrefix);
+ String modifiedURI = namespacePair.getFirst() + "Modified";
+ updateModelPayload.setNamespaceUri(modifiedURI);
+ response = put("cmm", customModelAdmin, modelName, RestApiUtil.toJsonAsString(updateModelPayload), null, 200);
+ returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(modifiedURI, returnedModel.getNamespaceUri());
+ assertEquals("The namespace prefix shouldn't have changed.", modifiedPrefix, returnedModel.getNamespacePrefix());
+ }
+
+ @Test
+ public void testCreateInlineConstraint() throws Exception
+ {
+ String modelName = "testModelInlineConstraint" + System.currentTimeMillis();
+ final Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ createCustomModel(modelName, namespacePair, ModelStatus.DRAFT);
+
+ String regExConstraintName = "testInlineFileNameRegEx" + System.currentTimeMillis();
+ {
+ // Create RegEx constraint
+ CustomModelConstraint inlineRegExConstraint = new CustomModelConstraint();
+ inlineRegExConstraint.setName(regExConstraintName);
+ inlineRegExConstraint.setType("REGEX");
+ // Create the inline RegEx constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("expression", "(.*[\\\"\\*\\\\\\>\\<\\?\\/\\:\\|]+.*)|(.*[\\.]?.*[\\.]+$)|(.*[ ]+$)"));
+ parameters.add(buildNamedValue("requiresMatch", "false"));
+ // Add the parameters into the constraint
+ inlineRegExConstraint.setParameters(parameters);
+
+ // Create aspect
+ String aspectName = "testAspect1" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect aspectPayload = new CustomAspect();
+ aspectPayload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop1" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property title");
+ aspectProp.setDataType("d:text");
+ aspectProp.setConstraints(Arrays.asList(inlineRegExConstraint));// Add inline constraint
+ List props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Create the property
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 200);
+
+ // Retrieve all the model's constraints
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+ HttpResponse response = getAll("cmm/" + modelName + "/constraints", customModelAdmin, paging, 200);
+ List constraints = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModelConstraint.class);
+ assertEquals("Inline constraints should not be included with the model defined constraints.", 0, constraints.size());
+
+ // Retrieve the updated aspect
+ response = getSingle("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, 200);
+ CustomAspect returnedAspect = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomAspect.class);
+
+ // Check the aspect's added property
+ assertEquals(1, returnedAspect.getProperties().size());
+ CustomModelProperty customModelProperty = returnedAspect.getProperties().get(0);
+ assertEquals(aspectPropName, customModelProperty.getName());
+
+ assertEquals(0, customModelProperty.getConstraintRefs().size());
+ List inlineConstraints = customModelProperty.getConstraints();
+ assertEquals(1, inlineConstraints.size());
+ compareCustomModelConstraints(inlineRegExConstraint, inlineConstraints.get(0), "prefixedName");
+ }
+
+ // Create inline and referenced constraint
+ {
+ // Create RegEx constraint
+ CustomModelConstraint regExConstraint = new CustomModelConstraint();
+ regExConstraint.setName(regExConstraintName); // duplicate name
+ regExConstraint.setType("REGEX");
+ regExConstraint.setTitle("test RegEx title");
+ // Create the RegEx constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("expression", "(.*[\\\"\\*\\\\\\>\\<\\?\\/\\:\\|]+.*)|(.*[\\.]?.*[\\.]+$)|(.*[ ]+$)"));
+ parameters.add(buildNamedValue("requiresMatch", "false"));
+ // Add the parameters into the constraint
+ regExConstraint.setParameters(parameters);
+
+ // Try to create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(regExConstraint), 409); // duplicate name
+
+ String newRegExConstraintName = "testFileNameRegEx" + System.currentTimeMillis();
+ regExConstraint.setName(newRegExConstraintName);
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(regExConstraint), 201);
+ // Retrieve the created RegEx constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, newRegExConstraintName, 200);
+ CustomModelConstraint returnedRegExConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+
+ // Create inline anonymous LENGTH constraint
+ CustomModelConstraint inlineAnonymousLengthConstraint = new CustomModelConstraint();
+ inlineAnonymousLengthConstraint.setType("LENGTH");
+ inlineAnonymousLengthConstraint.setTitle("test Length title");
+ // Create the Length constraint's parameters
+ parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxLength", "256"));
+ parameters.add(buildNamedValue("minLength", "0"));
+ // Add the parameters into the constraint
+ inlineAnonymousLengthConstraint.setParameters(parameters);
+
+ // Create type
+ String typeName = "testType1" + System.currentTimeMillis();
+ CustomType type = createTypeAspect(CustomType.class, modelName, typeName, "test type1 title", "test type1 Desc", "cm:content");
+
+ // Update the Type by adding property
+ CustomType typePayload = new CustomType();
+ typePayload.setName(typeName);
+ String typePropName = "testType1Prop1" + System.currentTimeMillis();
+ CustomModelProperty typeProp = new CustomModelProperty();
+ typeProp.setName(typePropName);
+ typeProp.setTitle("property title");
+ typeProp.setDataType("d:int");
+ typeProp.setConstraintRefs(Arrays.asList(returnedRegExConstraint.getPrefixedName())); // Constraint Ref
+ typeProp.setConstraints(Arrays.asList(inlineAnonymousLengthConstraint)); // inline constraint
+ List props = new ArrayList<>(1);
+ props.add(typeProp);
+ typePayload.setProperties(props);
+
+ // Try to create the property - LENGTH constraint can only be used with textual data type
+ put("cmm/" + modelName + "/types", customModelAdmin, typeName, RestApiUtil.toJsonAsString(typePayload), SELECT_PROPS_QS, 400);
+
+ typeProp.setDataType("d:double");
+ // CTry to create the property - LENGTH constraint can only be used with textual data type
+ put("cmm/" + modelName + "/types", customModelAdmin, typeName, RestApiUtil.toJsonAsString(typePayload), SELECT_PROPS_QS, 400);
+
+ typeProp.setDataType("d:text");
+ put("cmm/" + modelName + "/types", customModelAdmin, typeName, RestApiUtil.toJsonAsString(typePayload), SELECT_PROPS_QS, 200);
+
+ // Retrieve the updated type
+ response = getSingle("cmm/" + modelName + "/types", customModelAdmin, type.getName(), 200);
+ CustomType returnedType = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomType.class);
+
+ // Check the type's added property
+ assertEquals(1, returnedType.getProperties().size());
+ CustomModelProperty customModelProperty = returnedType.getProperties().get(0);
+ assertEquals(typePropName, customModelProperty.getName());
+
+ assertEquals(1, customModelProperty.getConstraintRefs().size());
+ assertEquals(returnedRegExConstraint.getPrefixedName(), customModelProperty.getConstraintRefs().get(0));
+
+ assertEquals(1, customModelProperty.getConstraints().size());
+ assertNotNull(customModelProperty.getConstraints().get(0).getName()); // M2PropertyDefinition will add a name
+ compareCustomModelConstraints(inlineAnonymousLengthConstraint, customModelProperty.getConstraints().get(0), "prefixedName", "name");
+ }
+ }
+
+ @Test
+ public void testCreateListConstraintInvalid() throws Exception
+ {
+ String modelName = "testModelConstraintInvalid" + System.currentTimeMillis();
+ final Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ createCustomModel(modelName, namespacePair, ModelStatus.DRAFT);
+
+ // Create aspect
+ String aspectName = "testAspect" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect aspectPayload = new CustomAspect();
+ aspectPayload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property title");
+ aspectProp.setDataType("d:int");
+
+ //Create LIST constraint
+ String inlineListConstraintName = "testListConstraint" + System.currentTimeMillis();
+ CustomModelConstraint inlineListConstraint = new CustomModelConstraint();
+ inlineListConstraint.setName(inlineListConstraintName);
+ inlineListConstraint.setType("LIST");
+ inlineListConstraint.setTitle("test List title");
+ inlineListConstraint.setDescription("test List desc");
+ // Create the List constraint's parameters
+ List parameters = new ArrayList<>(3);
+ parameters.add(buildNamedValue("allowedValues", null, "a", "b", "c"));// text list value, but the the property data type is d:int
+ parameters.add(buildNamedValue("sorted", "false"));
+ // Add the parameters into the constraint
+ inlineListConstraint.setParameters(parameters);
+ aspectProp.setConstraints(Arrays.asList(inlineListConstraint));// Add inline constraint
+ List props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Try to create the property - Invalid LIST values
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 400);
+
+ // Test d:double LIST values with d:int property data type
+ parameters = new ArrayList<>(3);
+ parameters.add(buildNamedValue("allowedValues", null, "1.0", "2.0", "3.0"));// double list value, but the the property data type is d:int
+ parameters.add(buildNamedValue("sorted", "false"));
+ // Add the parameters into the constraint
+ inlineListConstraint.setParameters(parameters);
+ aspectProp.setConstraints(Arrays.asList(inlineListConstraint));// Add inline constraint
+ props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Try to create the property - Invalid LIST values
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 400);
+ }
+
+ @Test
+ public void testCreateMinMaxConstraintInvalid() throws Exception
+ {
+ String modelName = "testModelMinMaxInvalid" + System.currentTimeMillis();
+ final Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ createCustomModel(modelName, namespacePair, ModelStatus.DRAFT);
+
+ // Create aspect
+ String aspectName = "testAspect" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect aspectPayload = new CustomAspect();
+ aspectPayload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property title");
+ aspectProp.setDataType("d:text");
+
+ String minMaxConstraintName = "testMinMaxConstraint" + System.currentTimeMillis();
+ CustomModelConstraint minMaxConstraint = new CustomModelConstraint();
+ minMaxConstraint.setType("MINMAX");
+ minMaxConstraint.setName(minMaxConstraintName);
+ minMaxConstraint.setTitle("test MinMax title");
+ minMaxConstraint.setDescription("test MinMax desc");
+ // Create the MinMax constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxValue", "100.0"));
+ parameters.add(buildNamedValue("minValue", "0.0"));
+ // Add the parameters into the constraint
+ minMaxConstraint.setParameters(parameters);
+
+ aspectProp.setConstraints(Arrays.asList(minMaxConstraint));// Add inline constraint
+ List props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Try to create constraint as a Model Administrator
+ // MINMAX constraint can only be used with numeric data type.
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 400);
+
+ // Change type
+ aspectProp.setDataType("d:datetime");
+ // MINMAX constraint can only be used with numeric data type.
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 400);
+
+ // SHA-1126
+ {
+ //Change type
+ aspectProp.setDataType("d:double");
+ parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxValue", "0.0"));
+ parameters.add(buildNamedValue("minValue", "-5.0"));
+ // Add the parameters into the constraint
+ minMaxConstraint.setParameters(parameters);
+
+ aspectProp.setConstraints(Arrays.asList(minMaxConstraint));// Add inline constraint
+ props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+ // Maximum value of the MINMAX constraint must be a positive nonzero value.
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 400);
+ }
+ }
+
+ @Test
+ public void testPropDefaultValueWithInlineConstraint() throws Exception
+ {
+ String modelName = "testModelInlineConstraint" + System.currentTimeMillis();
+ final Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ createCustomModel(modelName, namespacePair, ModelStatus.DRAFT);
+
+ {
+ // Create RegEx constraint
+ String regExConstraintName = "testInlineFileNameRegEx" + System.currentTimeMillis();
+ CustomModelConstraint inlineRegExConstraint = new CustomModelConstraint();
+ inlineRegExConstraint.setName(regExConstraintName);
+ inlineRegExConstraint.setType("REGEX");
+ // Create the inline RegEx constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("expression", "(.*[\\\"\\*\\\\\\>\\<\\?\\/\\:\\|]+.*)|(.*[\\.]?.*[\\.]+$)|(.*[ ]+$)"));
+ parameters.add(buildNamedValue("requiresMatch", "false"));
+ // Add the parameters into the constraint
+ inlineRegExConstraint.setParameters(parameters);
+
+ // Create aspect
+ String aspectName = "testAspect1" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect aspectPayload = new CustomAspect();
+ aspectPayload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop1" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property with REGEX constraint");
+ aspectProp.setDataType("d:text");
+ aspectProp.setDefaultValue("invalid props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Try to create the property - constraint violation
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 409);
+ }
+
+ {
+ // Create inline anonymous LENGTH constraint
+ CustomModelConstraint inlineAnonymousLengthConstraint = new CustomModelConstraint();
+ inlineAnonymousLengthConstraint.setType("LENGTH");
+ // Create the Length constraint's parameters
+ Listparameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxLength", "4"));
+ parameters.add(buildNamedValue("minLength", "0"));
+ // Add the parameters into the constraint
+ inlineAnonymousLengthConstraint.setParameters(parameters);
+
+ // Create type
+ String typeName = "testType1" + System.currentTimeMillis();
+ createTypeAspect(CustomType.class, modelName, typeName, "test type1 title", "test type1 Desc", "cm:content");
+
+ // Update the Type by adding property
+ CustomType typePayload = new CustomType();
+ typePayload.setName(typeName);
+ String typePropName = "testType1Prop1" + System.currentTimeMillis();
+ CustomModelProperty typeProp = new CustomModelProperty();
+ typeProp.setName(typePropName);
+ typeProp.setTitle("property with LENGTH constraint");
+ typeProp.setDataType("d:text");
+ typeProp.setDefaultValue("abcdef"); // Invalid length
+ typeProp.setConstraints(Arrays.asList(inlineAnonymousLengthConstraint)); // inline constraint
+ List props = new ArrayList<>(1);
+ props.add(typeProp);
+ typePayload.setProperties(props);
+
+ // Try to create the property - constraint violation
+ put("cmm/" + modelName + "/types", customModelAdmin, typeName, RestApiUtil.toJsonAsString(typePayload), SELECT_PROPS_QS, 409);
+ }
+
+ {
+ // Create inline anonymous MINMAX constraint
+ CustomModelConstraint inlineAnonymousMinMaxConstraint = new CustomModelConstraint();
+ inlineAnonymousMinMaxConstraint.setType("MINMAX");
+ // Create the MinMax constraint's parameters
+ Listparameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxValue", "10"));
+ parameters.add(buildNamedValue("minValue", "0"));
+ // Add the parameters into the constraint
+ inlineAnonymousMinMaxConstraint.setParameters(parameters);
+
+ // Create type
+ String typeName = "testType1" + System.currentTimeMillis();
+ createTypeAspect(CustomType.class, modelName, typeName, "test type1 title", "test type1 Desc", "cm:content");
+
+ // Update the Type by adding property
+ CustomType typePayload = new CustomType();
+ typePayload.setName(typeName);
+ String typePropName = "testType1Prop1" + System.currentTimeMillis();
+ CustomModelProperty typeProp = new CustomModelProperty();
+ typeProp.setName(typePropName);
+ typeProp.setTitle("property with MINMAX constraint");
+ typeProp.setDataType("d:int");
+ typeProp.setDefaultValue("20"); // Not in the defined range [0,10]
+ typeProp.setConstraints(Arrays.asList(inlineAnonymousMinMaxConstraint)); // inline constraint
+ List props = new ArrayList<>(1);
+ props.add(typeProp);
+ typePayload.setProperties(props);
+
+ // Try to create the property - constraint violation
+ put("cmm/" + modelName + "/types", customModelAdmin, typeName, RestApiUtil.toJsonAsString(typePayload), SELECT_PROPS_QS, 409);
+ }
+
+ {
+ // Create LIST constraint
+ String listConstraintName = "testListConstraint" + System.currentTimeMillis();
+ CustomModelConstraint inlineListConstraint = new CustomModelConstraint();
+ inlineListConstraint.setName(listConstraintName);
+ inlineListConstraint.setType("LIST");
+ // Create the List constraint's parameters
+ List parameters = new ArrayList<>(3);
+ parameters.add(buildNamedValue("allowedValues", null, "one", "two", "three"));
+ parameters.add(buildNamedValue("sorted", "false"));
+ // Add the parameters into the constraint
+ inlineListConstraint.setParameters(parameters);
+
+ // Create aspect
+ String aspectName = "testAspect" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect aspectPayload = new CustomAspect();
+ aspectPayload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property with LIST constraint");
+ aspectProp.setDataType("d:text");
+ aspectProp.setDefaultValue("four"); // Not in the list
+ aspectProp.setConstraints(Arrays.asList(inlineListConstraint));// Add inline constraint
+ List props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Try to create the property - constraint violation
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 409);
+ }
+
+ {
+ // Create Java Class constraint
+ String inlineJavaClassConstraintName = "testJavaClassConstraint" + System.currentTimeMillis();
+ CustomModelConstraint inlineListConstraint = new CustomModelConstraint();
+ inlineListConstraint.setName(inlineJavaClassConstraintName);
+ inlineListConstraint.setType("org.alfresco.rest.api.tests.TestCustomConstraint$DummyJavaClassConstraint");
+
+ // Create aspect
+ String aspectName = "testAspect" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect aspectPayload = new CustomAspect();
+ aspectPayload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property with Java Class constraint");
+ aspectProp.setDataType("d:text");
+ aspectProp.setDefaultValue("invalid#value"); // Invalid default value
+ aspectProp.setConstraints(Arrays.asList(inlineListConstraint));// Add inline constraint
+ List props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Try to create the property - constraint violation
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 409);
+ }
+ }
+
+ @Test
+ public void testPropDefaultValueWithConstraintRef() throws Exception
+ {
+ String modelName = "testModelConstraintRef" + System.currentTimeMillis();
+ final Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ createCustomModel(modelName, namespacePair, ModelStatus.DRAFT);
+
+ {
+ // Create List constraint
+ String listConstraintName = "testListConstraint" + System.currentTimeMillis();
+ CustomModelConstraint listConstraint = new CustomModelConstraint();
+ listConstraint.setName(listConstraintName);
+ listConstraint.setType("LIST");
+ // Create the List constraint's parameters
+ List parameters = new ArrayList<>(3);
+ parameters.add(buildNamedValue("allowedValues", null, "London", "Paris", "New York"));// list value
+ parameters.add(buildNamedValue("sorted", "false"));
+ // Add the parameters into the constraint
+ listConstraint.setParameters(parameters);
+
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(listConstraint), 201);
+ // Retrieve the created List constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, listConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+
+ // Create aspect
+ String aspectName = "testAspect" + System.currentTimeMillis();
+ createTypeAspect(CustomAspect.class, modelName, aspectName, "title", "desc", null);
+
+ // Update the Aspect by adding property
+ CustomAspect aspectPayload = new CustomAspect();
+ aspectPayload.setName(aspectName);
+ final String aspectPropName = "testAspect1Prop" + System.currentTimeMillis();
+ CustomModelProperty aspectProp = new CustomModelProperty();
+ aspectProp.setName(aspectPropName);
+ aspectProp.setTitle("property with LIST constraint ref");
+ aspectProp.setDataType("d:text");
+ aspectProp.setDefaultValue("Berlin"); // Not in the list
+ aspectProp.setConstraintRefs(Arrays.asList(returnedConstraint.getPrefixedName())); // constrain ref
+ List props = new ArrayList<>(1);
+ props.add(aspectProp);
+ aspectPayload.setProperties(props);
+
+ // Try to create the property - constraint violation
+ put("cmm/" + modelName + "/aspects", customModelAdmin, aspectName, RestApiUtil.toJsonAsString(aspectPayload), SELECT_PROPS_QS, 409);
+ }
+
+ {
+ // Create MINMAX constraint
+ String minMaxConstraintName = "testMinMaxConstraint" + System.currentTimeMillis();
+ CustomModelConstraint minMaxConstraint = new CustomModelConstraint();
+ minMaxConstraint.setName(minMaxConstraintName);
+ minMaxConstraint.setType("MINMAX");
+ // Create the MinMax constraint's parameters
+ List parameters = new ArrayList<>(2);
+ parameters.add(buildNamedValue("maxValue", "100"));
+ parameters.add(buildNamedValue("minValue", "50"));
+ // Add the parameters into the constraint
+ minMaxConstraint.setParameters(parameters);
+
+ // Create constraint as a Model Administrator
+ post("cmm/" + modelName + "/constraints", customModelAdmin, RestApiUtil.toJsonAsString(minMaxConstraint), 201);
+ // Retrieve the created MinMax constraint
+ HttpResponse response = getSingle("cmm/" + modelName + "/constraints", customModelAdmin, minMaxConstraintName, 200);
+ CustomModelConstraint returnedConstraint = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModelConstraint.class);
+
+ // Create type
+ String typeName = "testType1" + System.currentTimeMillis();
+ createTypeAspect(CustomType.class, modelName, typeName, "test type1 title", "test type1 Desc", "cm:content");
+
+ // Update the Type by adding property
+ CustomType typePayload = new CustomType();
+ typePayload.setName(typeName);
+ String typePropName = "testType1Prop1" + System.currentTimeMillis();
+ CustomModelProperty typeProp = new CustomModelProperty();
+ typeProp.setName(typePropName);
+ typeProp.setTitle("property with MINMAX constraint ref");
+ typeProp.setDataType("d:int");
+ typeProp.setDefaultValue("35"); // Not in the defined range [50,100]
+ typeProp.setConstraintRefs(Arrays.asList(returnedConstraint.getPrefixedName())); // constrain ref
+ List props = new ArrayList<>(1);
+ props.add(typeProp);
+ typePayload.setProperties(props);
+
+ // Try to create the property - constraint violation
+ put("cmm/" + modelName + "/types", customModelAdmin, typeName, RestApiUtil.toJsonAsString(typePayload), SELECT_PROPS_QS, 409);
+ }
+ }
+
+ public static class DummyJavaClassConstraint extends AbstractConstraint
+ {
+ @Override
+ protected void evaluateSingleValue(Object value)
+ {
+ String checkValue = DefaultTypeConverter.INSTANCE.convert(String.class, value);
+
+ if (checkValue.contains("#"))
+ {
+ throw new ConstraintException("The value must not contain '#'");
+ }
+ }
+ }
+}
diff --git a/source/test-java/org/alfresco/rest/api/tests/TestCustomModel.java b/source/test-java/org/alfresco/rest/api/tests/TestCustomModel.java
new file mode 100644
index 0000000000..4061bef4c4
--- /dev/null
+++ b/source/test-java/org/alfresco/rest/api/tests/TestCustomModel.java
@@ -0,0 +1,661 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.alfresco.rest.api.model.CustomAspect;
+import org.alfresco.rest.api.model.CustomModel;
+import org.alfresco.rest.api.model.CustomModel.ModelStatus;
+import org.alfresco.rest.api.model.CustomType;
+import org.alfresco.rest.api.tests.client.HttpResponse;
+import org.alfresco.rest.api.tests.client.PublicApiClient.Paging;
+import org.alfresco.rest.api.tests.util.RestApiUtil;
+import org.alfresco.service.cmr.dictionary.CustomModelService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.Pair;
+import org.junit.Test;
+
+/**
+ * Tests the REST API of the models of the {@link CustomModelService}.
+ *
+ * @author Jamal Kaabi-Mofrad
+ */
+public class TestCustomModel extends BaseCustomModelApiTest
+{
+
+ @Test
+ public void testCreateBasicModel() throws Exception
+ {
+ String modelName = "testModel" + System.currentTimeMillis();
+ Pair namespacePair = getTestNamespaceUriPrefixPair();
+
+ CustomModel customModel = new CustomModel();
+ customModel.setName(modelName);
+ customModel.setNamespaceUri(namespacePair.getFirst());
+ customModel.setNamespacePrefix(namespacePair.getSecond());
+ customModel.setDescription("Test model description");
+ customModel.setStatus(CustomModel.ModelStatus.DRAFT);
+
+ // Try to create the model as a non Admin user
+ post("cmm", nonAdminUserName, RestApiUtil.toJsonAsString(customModel), 403);
+
+ // Create the model as a Model Administrator
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 201);
+
+ // Retrieve the created model
+ HttpResponse response = getSingle("cmm", customModelAdmin, modelName, 200);
+ CustomModel returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ // Check the retrieved model is the expected model.
+ // Note: since we didn't specify the Author when created the Model,
+ // we have to exclude it from the objects comparison. Because,
+ // the system will add the current authenticated user as the author
+ // of the model, if the Author hasn't been set.
+ compareCustomModels(customModel, returnedModel, "author");
+ }
+
+ @Test
+ public void testCreateBasicModel_Invalid() throws Exception
+ {
+ String modelName = "testModel" + System.currentTimeMillis();
+ Pair namespacePair = getTestNamespaceUriPrefixPair();
+
+ CustomModel customModel = new CustomModel();
+ customModel.setName(modelName);
+ customModel.setNamespaceUri(namespacePair.getFirst());
+ customModel.setNamespacePrefix(namespacePair.getSecond());
+
+ // Test invalid inputs
+ {
+ customModel.setName(modelName + "");
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400);
+
+ customModel.setName("prefix:" + modelName);
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // Invalid name. Contains ':'
+
+ customModel.setName("prefix " + modelName);
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // Invalid name. Contains space
+
+ customModel.setName(modelName);
+ customModel.setNamespacePrefix(namespacePair.getSecond()+" space");
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // Invalid prefix. Contains space
+
+ customModel.setNamespacePrefix(namespacePair.getSecond()+"invalid/");
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // Invalid prefix. Contains '/'
+
+ customModel.setNamespacePrefix(namespacePair.getSecond());
+ customModel.setNamespaceUri(namespacePair.getFirst()+" space");
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // Invalid URI. Contains space
+
+ customModel.setNamespaceUri(namespacePair.getFirst()+"\\");
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // Invalid URI. Contains '\'
+ }
+
+ // Test mandatory properties of the model
+ {
+ customModel.setName("");
+ customModel.setNamespacePrefix(namespacePair.getSecond());
+ customModel.setNamespaceUri(namespacePair.getFirst());
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // name is mandatory
+
+ customModel.setName(modelName);
+ customModel.setNamespaceUri(null);
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // namespaceUri is mandatory
+
+ customModel.setName(modelName);
+ customModel.setNamespaceUri(namespacePair.getFirst());
+ customModel.setNamespacePrefix(null);
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 400); // namespacePrefix is mandatory
+ }
+
+ // Test duplicate model name
+ {
+ // Test create a model with the same name as the bootstrapped model
+ customModel.setName("contentmodel");
+ customModel.setNamespaceUri(namespacePair.getFirst());
+ customModel.setNamespacePrefix(namespacePair.getSecond());
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 409);
+
+ // Create the model
+ customModel.setName(modelName);
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 201);
+
+ // Create a duplicate model
+ // Set a new namespace to make sure the 409 status code is returned
+ // because of a name conflict rather than namespace URI
+ namespacePair = getTestNamespaceUriPrefixPair();
+ customModel.setNamespaceUri(namespacePair.getFirst());
+ customModel.setNamespacePrefix(namespacePair.getSecond());
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModel), 409);
+ }
+
+ // Test duplicate namespaceUri
+ {
+ String modelNameTwo = "testModelTwo" + System.currentTimeMillis();
+ Pair namespacePairTwo = getTestNamespaceUriPrefixPair();
+
+ CustomModel customModelTwo = new CustomModel();
+ customModelTwo.setName(modelNameTwo);
+ customModelTwo.setNamespaceUri(namespacePairTwo.getFirst());
+ customModelTwo.setNamespacePrefix(namespacePairTwo.getSecond());
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModelTwo), 201);
+
+ String modelNameThree = "testModelThree" + System.currentTimeMillis();
+ Pair namespacePairThree = getTestNamespaceUriPrefixPair();
+ CustomModel customModelThree = new CustomModel();
+ customModelThree.setName(modelNameThree);
+ customModelThree.setNamespaceUri(namespacePairTwo.getFirst()); // duplicate URI
+ customModelThree.setNamespacePrefix(namespacePairThree.getSecond());
+
+ // Try to create a model with a namespace uri which has already been used.
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModelThree), 409);
+
+ customModelThree.setNamespaceUri(namespacePairThree.getFirst());
+ customModelThree.setNamespacePrefix(namespacePairTwo.getSecond()); // duplicate prefix
+
+ // Try to create a model with a namespace prefix which has already been used.
+ post("cmm", customModelAdmin, RestApiUtil.toJsonAsString(customModelThree), 409);
+ }
+ }
+
+ @Test
+ public void testListBasicModels() throws Exception
+ {
+ String modelName_1 = "testModel1" + System.currentTimeMillis();
+ // Create the model as a Model Administrator
+ CustomModel customModel_1 = createCustomModel(modelName_1, getTestNamespaceUriPrefixPair(), ModelStatus.DRAFT);
+
+ String modelName_2 = "testModel2" + System.currentTimeMillis();
+ CustomModel customModel_2 = createCustomModel(modelName_2, getTestNamespaceUriPrefixPair(), ModelStatus.DRAFT);
+
+ String modelName_3 = "testModel3" + System.currentTimeMillis();
+ CustomModel customModel_3 = createCustomModel(modelName_3, getTestNamespaceUriPrefixPair(), ModelStatus.DRAFT);
+
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+ HttpResponse response = getAll("cmm", customModelAdmin, paging, 200);
+ List models = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), CustomModel.class);
+
+ assertTrue(models.size() >= 3);
+ assertTrue(models.contains(customModel_1));
+ assertTrue(models.contains(customModel_2));
+ assertTrue(models.contains(customModel_3));
+ }
+
+ @Test
+ public void testActivateCustomModel() throws Exception
+ {
+ String modelNameOne = "testActivateModelOne" + System.currentTimeMillis();
+ Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ CustomModel customModelOne = createCustomModel(modelNameOne, namespacePair, ModelStatus.DRAFT, "Test model description", "Jane Doe");
+
+ // Retrieve the created model and check its status (the default is DRAFT)
+ HttpResponse response = getSingle("cmm", customModelAdmin, modelNameOne, 200);
+ CustomModel returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(ModelStatus.DRAFT, returnedModel.getStatus());
+
+ // We only want to update the status, so ignore the other properties
+ CustomModel updatePayload = new CustomModel();
+ updatePayload.setStatus(ModelStatus.ACTIVE);
+
+ // Try to activate the model as a non Admin user
+ put("cmm", nonAdminUserName, modelNameOne, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 403);
+
+ // Activate the model as a Model Administrator
+ put("cmm", customModelAdmin, modelNameOne, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 200);
+
+ response = getSingle("cmm", customModelAdmin, modelNameOne, 200);
+ returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(ModelStatus.ACTIVE, returnedModel.getStatus());
+ // Check other properties have not been modified
+ compareCustomModels(customModelOne, returnedModel, "status");
+
+ // Try to activate the already activated model as a Model Administrator
+ put("cmm", customModelAdmin, modelNameOne, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 500);
+
+ // Create another Model
+ String modelNameTwo = "testActivateModelTwo" + System.currentTimeMillis();
+ Pair namespacePairTwo = getTestNamespaceUriPrefixPair();
+ CustomModel customModelTwo = createCustomModel(modelNameTwo, namespacePairTwo, ModelStatus.DRAFT, null, "John Doe");
+
+ // Activate the model as a Model Administrator
+ customModelTwo.setStatus(ModelStatus.ACTIVE);
+ put("cmm", customModelAdmin, modelNameTwo, RestApiUtil.toJsonAsString(customModelTwo), SELECT_STATUS_QS, 200);
+
+ response = getSingle("cmm", customModelAdmin, modelNameTwo, 200);
+ returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(ModelStatus.ACTIVE, returnedModel.getStatus());
+ // Check other properties have not been modified
+ compareCustomModels(customModelTwo, returnedModel, "status");
+ }
+
+ @Test
+ public void testDeactivateCustomModel() throws Exception
+ {
+ String modelNameOne = "testDeactivateModelOne" + System.currentTimeMillis();
+ Pair namespacePair = getTestNamespaceUriPrefixPair();
+ // Create the model as a Model Administrator
+ CustomModel customModelOne = createCustomModel(modelNameOne, namespacePair, ModelStatus.ACTIVE, null, "Mark Moe");
+
+ // Retrieve the created model and check its status
+ HttpResponse response = getSingle("cmm", customModelAdmin, modelNameOne, 200);
+ CustomModel returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(ModelStatus.ACTIVE, returnedModel.getStatus());
+
+ // We only want to update the status (Deactivate), so ignore the other properties
+ CustomModel updatePayload = new CustomModel();
+ updatePayload.setStatus(ModelStatus.DRAFT);
+
+ // Try to deactivate the model as a non Admin user
+ put("cmm", nonAdminUserName, modelNameOne, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 403);
+
+ // Deactivate the model as a Model Administrator
+ put("cmm", customModelAdmin, modelNameOne, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 200);
+
+ response = getSingle("cmm", customModelAdmin, modelNameOne, 200);
+ returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(ModelStatus.DRAFT, returnedModel.getStatus());
+ // Check other properties have not been modified
+ compareCustomModels(customModelOne, returnedModel, "status");
+
+ // Try to deactivate the already deactivated model as a Model Administrator
+ put("cmm", customModelAdmin, modelNameOne, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 500);
+
+ // Activate/Deactivate a model with an aspect
+ {
+ // Create another Model
+ final String modelNameTwo = "testDeactivateModelTwo" + System.currentTimeMillis();
+ Pair namespacePairTwo = getTestNamespaceUriPrefixPair();
+ CustomModel customModelTwo = createCustomModel(modelNameTwo, namespacePairTwo, ModelStatus.DRAFT, null, "Mark Moe");
+
+ // Aspect
+ CustomAspect aspect = new CustomAspect();
+ aspect.setName("testMarkerAspect");
+ post("cmm/" + modelNameTwo + "/aspects", customModelAdmin, RestApiUtil.toJsonAsString(aspect), 201);
+ // Retrieve the created aspect
+ getSingle("cmm/" + modelNameTwo + "/aspects", customModelAdmin, aspect.getName(), 200);
+
+ // Activate the model as a Model Administrator
+ customModelTwo.setStatus(ModelStatus.ACTIVE);
+ put("cmm", customModelAdmin, modelNameTwo, RestApiUtil.toJsonAsString(customModelTwo), SELECT_STATUS_QS, 200);
+
+ response = getSingle("cmm", customModelAdmin, modelNameTwo, 200);
+ returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(ModelStatus.ACTIVE, returnedModel.getStatus());
+
+ updatePayload = new CustomModel();
+ updatePayload.setStatus(ModelStatus.DRAFT);
+ // Deactivate the model as a Model Administrator
+ put("cmm", customModelAdmin, modelNameTwo, RestApiUtil.toJsonAsString(updatePayload), SELECT_STATUS_QS, 200);
+
+ response = getSingle("cmm", customModelAdmin, modelNameTwo, 200);
+ returnedModel = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), CustomModel.class);
+ assertEquals(ModelStatus.DRAFT, returnedModel.getStatus());
+ }
+ }
+
+ @Test
+ public void testDeleteCustomModel() throws Exception
+ {
+ String modelName = "testDeleteModel" + System.currentTimeMillis();
+ Pair