Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * 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 <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.dictionary; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathFactory; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.query.EmptyPagingResults; import org.alfresco.query.PageDetails; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; import org.alfresco.repo.admin.RepoAdminServiceImpl; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.download.DownloadModel; import org.alfresco.repo.download.DownloadStorage; import org.alfresco.repo.model.filefolder.HiddenAspect; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.admin.RepoAdminService; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.Constraint; 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.DictionaryService; import org.alfresco.service.cmr.dictionary.ModelDefinition; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.dictionary.TypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryException.DuplicateDefinitionException; import org.alfresco.service.cmr.download.DownloadService; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; import org.alfresco.util.Pair; import org.alfresco.util.ParameterCheck; import org.alfresco.util.PropertyCheck; import org.alfresco.util.TempFileProvider; import org.alfresco.util.XMLUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; /** * Custom Model Service Implementation * * @author Jamal Kaabi-Mofrad */ public class CustomModelServiceImpl implements CustomModelService { private static final Log logger = LogFactory.getLog(CustomModelServiceImpl.class); public static final String DEFAULT_CUSTOM_MODEL_ASPECT = "hasAspect('cmm:customModelManagement')"; public static final QName ASPECT_CUSTOM_MODEL = QName .createQName("http://www.alfresco.org/model/custommodelmanagement/1.0", "customModelManagement"); public static final String ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY = "ALFRESCO_MODEL_ADMINISTRATORS"; public static final String GROUP_ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY = PermissionService.GROUP_PREFIX + ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY; public static final String SHARE_EXT_MODULE_SUFFIX = "_module.xml"; /** Messages */ private static final String MSG_NAME_ALREADY_IN_USE = "cmm.service.name_already_in_use"; private static final String MSG_CREATE_MODEL_ERR = "cmm.service.create_model_err"; private static final String MSG_UPDATE_MODEL_ERR = "cmm.service.update_model_err"; private static final String MSG_MULTIPLE_MODELS = "cmm.service.multiple_models"; private static final String MSG_RETRIEVE_MODEL = "cmm.service.retrieve_model"; private static final String MSG_MODEL_NOT_EXISTS = "cmm.service.model_not_exists"; private static final String MSG_NAMESPACE_NOT_EXISTS = "cmm.service.namespace_not_exists"; private static final String MSG_NAMESPACE_MANY_EXIST = "cmm.service.namespace_many_exist"; private static final String MSG_NAMESPACE_URI_ALREADY_IN_USE = "cmm.service.namespace_uri_already_in_use"; private static final String MSG_NAMESPACE_PREFIX_ALREADY_IN_USE = "cmm.service.namespace_prefix_already_in_use"; private static final String MSG_UNABLE_DELETE_ACTIVE_MODEL = "cmm.service.unable_delete_active_model"; private static final String MSG_UNABLE_MODEL_DELETE = "cmm.service.unable_model_delete"; private static final String MSG_UNABLE_MODEL_DEACTIVATE = "cmm.service.unable_model_deactivate"; private static final String MSG_UNABLE_MODEL_ACTIVATE = "cmm.service.unable_model_activate"; private static final String MSG_INVALID_MODEL = "cmm.service.invalid_model"; private static final String MSG_NAMESPACE_ACTIVE_MODEL = "cmm.service.namespace_active_model"; private static final String MSG_FAILED_DEACTIVATION_TYPE_DEPENDENCY = "cmm.service.failed.deactivation.type.dependency"; private static final String MSG_FAILED_DEACTIVATION_ASPECT_DEPENDENCY = "cmm.service.failed.deactivation.aspect.dependency"; private static final String MSG_DOWNLOAD_COPY_MODEL_ERR = "cmm.service.download.create_model_copy_err"; private static final String MSG_DOWNLOAD_CREATE_SHARE_EXT_ERR = "cmm.service.download.create_share_ext_err"; private NodeService nodeService; private DictionaryDAOImpl dictionaryDAO; private ContentService contentService; private SearchService searchService; private RepositoryLocation repoModelsLocation; private NamespaceDAO namespaceDAO; private DictionaryService dictionaryService; private RetryingTransactionHelper retryingTransactionHelper; private RepoAdminService repoAdminService; private AuthorityService authorityService; private HiddenAspect hiddenAspect; private DownloadService downloadService; private DownloadStorage downloadStorage; private String shareExtModulePath; public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } public void setDictionaryDAO(DictionaryDAOImpl dictionaryDAO) { this.dictionaryDAO = dictionaryDAO; } public void setContentService(ContentService contentService) { this.contentService = contentService; } public void setSearchService(SearchService searchService) { this.searchService = searchService; } public void setRepositoryModelsLocation(RepositoryLocation repoModelsLocation) { this.repoModelsLocation = repoModelsLocation; } public void setDictionaryService(DictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } public void setNamespaceDAO(NamespaceDAO namespaceDAO) { this.namespaceDAO = namespaceDAO; } public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) { this.retryingTransactionHelper = retryingTransactionHelper; } public void setRepoAdminService(RepoAdminService repoAdminService) { this.repoAdminService = repoAdminService; } public void setAuthorityService(AuthorityService authorityService) { this.authorityService = authorityService; } public void setHiddenAspect(HiddenAspect hiddenAspect) { this.hiddenAspect = hiddenAspect; } public void setDownloadService(DownloadService downloadSerivce) { this.downloadService = downloadSerivce; } public void setDownloadStorage(DownloadStorage downloadStorage) { this.downloadStorage = downloadStorage; } public void setShareExtModulePath(String shareExtModulePath) { this.shareExtModulePath = shareExtModulePath; } /** * Checks that all necessary properties and services have been provided. */ public void init() { PropertyCheck.mandatory(this, "nodeService", nodeService); PropertyCheck.mandatory(this, "dictionaryDAO", dictionaryDAO); PropertyCheck.mandatory(this, "contentService", contentService); PropertyCheck.mandatory(this, "searchService", searchService); PropertyCheck.mandatory(this, "repoModelsLocation", repoModelsLocation); PropertyCheck.mandatory(this, "namespaceDAO", namespaceDAO); PropertyCheck.mandatory(this, "dictionaryService", dictionaryService); PropertyCheck.mandatory(this, "retryingTransactionHelper", retryingTransactionHelper); PropertyCheck.mandatory(this, "repoAdminService", repoAdminService); PropertyCheck.mandatory(this, "authorityService", authorityService); PropertyCheck.mandatory(this, "hiddenAspect", hiddenAspect); PropertyCheck.mandatory(this, "shareExtModulePath", shareExtModulePath); PropertyCheck.mandatory(this, "downloadService", downloadService); PropertyCheck.mandatory(this, "downloadStorage", downloadStorage); } private NodeRef getRootNode() { StoreRef storeRef = repoModelsLocation.getStoreRef(); return nodeService.getRootNode(storeRef); } @Override public NodeRef getModelNodeRef(String modelName) { ParameterCheck.mandatoryString("modelName", modelName); StringBuilder builder = new StringBuilder(120); builder.append(repoModelsLocation.getPath()).append("//.[@cm:name='").append(modelName).append("' and ") .append(RepoAdminServiceImpl.defaultSubtypeOfDictionaryModel).append(']'); List<NodeRef> nodeRefs = searchService.selectNodes(getRootNode(), builder.toString(), null, namespaceDAO, false); if (nodeRefs.size() == 0) { return null; } else if (nodeRefs.size() > 1) { // unexpected: should not find multiple nodes with same name throw new CustomModelException(MSG_MULTIPLE_MODELS, new Object[] { modelName }); } return nodeRefs.get(0); } private M2Model getM2Model(final NodeRef modelNodeRef) { ContentReader reader = contentService.getReader(modelNodeRef, ContentModel.PROP_CONTENT); if (reader == null) { return null; } InputStream in = reader.getContentInputStream(); try { return M2Model.createModel(in); } finally { if (in != null) { try { in.close(); } catch (IOException e) { logger.error("Failed to close input stream for " + modelNodeRef); } } } } @Override public CustomModelDefinition getCustomModel(String modelName) { ParameterCheck.mandatoryString("modelName", modelName); Pair<CompiledModel, Boolean> compiledModelPair = getCustomCompiledModel(modelName); CustomModelDefinition result = (compiledModelPair == null) ? null : new CustomModelDefinitionImpl(compiledModelPair.getFirst(), compiledModelPair.getSecond(), dictionaryService); return result; } @Override public ModelDefinition getCustomModelByUri(String namespaceUri) { ParameterCheck.mandatoryString("namespaceUri", namespaceUri); CompiledModel compiledModel = getModelByUri(namespaceUri); if (compiledModel != null) { return compiledModel.getModelDefinition(); } return null; } private CompiledModel getModelByUri(String uri) { for (CompiledModel model : getAllCustomM2Models(false)) { if (uri.equals(getModelNamespaceUriPrefix(model.getM2Model()).getFirst())) { return model; } } return null; } /** * Returns compiled custom model and whether the model is active or not as a {@code Pair} object * * @param modelName the name of the custom model to retrieve * @return the {@code Pair<CompiledModel, Boolean>} (or null, if it doesn't exist) */ protected Pair<CompiledModel, Boolean> getCustomCompiledModel(String modelName) { ParameterCheck.mandatoryString("modelName", modelName); final NodeRef modelNodeRef = getModelNodeRef(modelName); if (modelNodeRef == null || !nodeService.exists(modelNodeRef)) { return null; } M2Model model = null; final boolean isActive = Boolean.TRUE .equals(nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_ACTIVE)); if (isActive) { QName modelQName = (QName) nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_NAME); if (modelQName == null) { return null; } try { CompiledModel compiledModel = dictionaryDAO.getCompiledModel(modelQName); model = compiledModel.getM2Model(); } catch (Exception e) { throw new CustomModelException(MSG_RETRIEVE_MODEL, new Object[] { modelName }, e); } } else { model = getM2Model(modelNodeRef); } Pair<CompiledModel, Boolean> result = (model == null) ? null : new Pair<>(compileModel(model), isActive); return result; } @Override public PagingResults<CustomModelDefinition> getCustomModels(PagingRequest pagingRequest) { ParameterCheck.mandatory("pagingRequest", pagingRequest); List<CustomModelDefinition> result = getAllCustomModels(); return result.isEmpty() ? new EmptyPagingResults<CustomModelDefinition>() : wrapResult(pagingRequest, result); } protected List<CustomModelDefinition> getAllCustomModels() { List<CustomModelDefinition> result = new ArrayList<>(); Collection<QName> models = dictionaryDAO.getModels(true); List<String> dictionaryModels = new ArrayList<>(); for (QName model : models) { dictionaryModels.add(model.toPrefixString()); } List<CompiledModel> compiledModels = getAllCustomM2Models(false); if (compiledModels.size() > 0) { for (CompiledModel model : compiledModels) { // check against models loaded in dictionary boolean isActive = false; if (dictionaryModels.contains(model.getM2Model().getName())) { isActive = true; } result.add(new CustomModelDefinitionImpl(model, isActive, dictionaryService)); } } return result; } private List<CompiledModel> getAllCustomM2Models(boolean onlyInactiveModels) { List<CompiledModel> result = new ArrayList<>(); StringBuilder builder = new StringBuilder(160); builder.append(repoModelsLocation.getPath()).append(RepoAdminServiceImpl.CRITERIA_ALL).append("[(") .append(RepoAdminServiceImpl.defaultSubtypeOfDictionaryModel).append(" and ") .append(DEFAULT_CUSTOM_MODEL_ASPECT); if (onlyInactiveModels) { builder.append(" and @cm:modelActive='false'"); } builder.append(")]"); List<NodeRef> nodeRefs = searchService.selectNodes(getRootNode(), builder.toString(), null, namespaceDAO, false, SearchService.LANGUAGE_XPATH); if (nodeRefs.size() > 0) { for (NodeRef nodeRef : nodeRefs) { try { M2Model m2Model = getM2Model(nodeRef); if (m2Model == null) { logger.warn("Couldn't construct M2Model from nodeRef:" + nodeRef); continue; } result.add(compileModel(m2Model)); } catch (Throwable t) { logger.warn("Skip model (" + t.getMessage() + ")"); } } } return result; } @Override public AspectDefinition getCustomAspect(QName name) { ParameterCheck.mandatory("name", name); CompiledModel compiledModel = getModelByUri(name.getNamespaceURI()); if (compiledModel != null) { return compiledModel.getAspect(name); } return null; } @Override public PagingResults<AspectDefinition> getAllCustomAspects(PagingRequest pagingRequest) { ParameterCheck.mandatory("pagingRequest", pagingRequest); List<AspectDefinition> result = new ArrayList<>(); List<CompiledModel> list = getAllCustomM2Models(false); for (CompiledModel model : list) { result.addAll(model.getAspects()); } return wrapResult(pagingRequest, result); } @Override public TypeDefinition getCustomType(QName name) { ParameterCheck.mandatory("name", name); CompiledModel compiledModel = getModelByUri(name.getNamespaceURI()); if (compiledModel != null) { return compiledModel.getType(name); } return null; } @Override public PagingResults<TypeDefinition> getAllCustomTypes(PagingRequest pagingRequest) { ParameterCheck.mandatory("pagingRequest", pagingRequest); List<TypeDefinition> result = new ArrayList<>(); List<CompiledModel> list = getAllCustomM2Models(false); for (CompiledModel model : list) { result.addAll(model.getTypes()); } return wrapResult(pagingRequest, result); } @Override public ConstraintDefinition getCustomConstraint(QName name) { ParameterCheck.mandatory("name", name); CompiledModel compiledModel = getModelByUri(name.getNamespaceURI()); if (compiledModel != null) { return compiledModel.getConstraint(name); } return null; } @Override public CustomModelDefinition createCustomModel(M2Model m2Model, boolean activate) { ParameterCheck.mandatory("m2Model", m2Model); String modelName = m2Model.getName(); int colonIndex = modelName.indexOf(QName.NAMESPACE_PREFIX); final String modelFileName = (colonIndex == -1) ? modelName : modelName.substring(colonIndex + 1); if (isModelExists(modelFileName)) { throw new CustomModelException.ModelExistsException(MSG_NAME_ALREADY_IN_USE, new Object[] { modelFileName }); } // Validate the model namespace URI validateModelNamespaceUri(getModelNamespaceUriPrefix(m2Model).getFirst()); // Validate the model namespace prefix validateModelNamespacePrefix(getModelNamespaceUriPrefix(m2Model).getSecond()); // Return the created model definition CompiledModel compiledModel = createUpdateModel(modelFileName, m2Model, activate, MSG_CREATE_MODEL_ERR, false); CustomModelDefinition modelDef = new CustomModelDefinitionImpl(compiledModel, activate, dictionaryService); if (logger.isDebugEnabled()) { logger.debug(modelFileName + " model has been created."); } return modelDef; } @Override public CustomModelDefinition updateCustomModel(String modelFileName, M2Model m2Model, boolean activate) { ParameterCheck.mandatory("m2Model", m2Model); final NodeRef existingModelNodeRef = getModelNodeRef(modelFileName); if (existingModelNodeRef == null) { throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelFileName }); } // Existing model property and namespace uri-prefix pair final boolean isActive = Boolean.TRUE .equals(nodeService.getProperty(existingModelNodeRef, ContentModel.PROP_MODEL_ACTIVE)); final M2Model existingModel = getM2Model(existingModelNodeRef); final Pair<String, String> existingNamespacePair = getModelNamespaceUriPrefix(existingModel); // New model namespace uri-prefix pair final Pair<String, String> newNamespacePair = getModelNamespaceUriPrefix(m2Model); if (isActive && !(existingNamespacePair.equals(newNamespacePair))) { throw new CustomModelException.ActiveModelConstraintException(MSG_NAMESPACE_ACTIVE_MODEL); } // if the prefix has changed, then check the new prefix is not in use. if (!existingNamespacePair.getSecond().equals(newNamespacePair.getSecond())) { validateModelNamespacePrefix(newNamespacePair.getSecond()); } // if the URI has changed, then check the new URI is not in use. if (!existingNamespacePair.getFirst().equals(newNamespacePair.getFirst())) { validateModelNamespaceUri(newNamespacePair.getFirst()); } /* * We set the requiresNewTx = true, in order to catch any exception * thrown within the low level content model management. * For example, deleting a property of an active model, where the * property has been applied to a node will cause the * ModelValidatorImpl to throw an exception. * Without starting a new TX, we can't catch that exception. */ CompiledModel compiledModel = createUpdateModel(modelFileName, m2Model, activate, MSG_UPDATE_MODEL_ERR, true); CustomModelDefinition modelDef = new CustomModelDefinitionImpl(compiledModel, activate, dictionaryService); if (logger.isDebugEnabled()) { logger.debug(modelFileName + " model has been updated."); } return modelDef; } private CompiledModel createUpdateModel(final String modelFileName, final M2Model m2Model, final boolean activate, String errMsgId, boolean requiresNewTx) { // Validate model CompiledModel compiledModel = compileModel(m2Model); // Validate properties default values validatePropsDefaultValues(compiledModel); ByteArrayOutputStream xml = new ByteArrayOutputStream(); m2Model.toXML(xml); final InputStream modelStream = new ByteArrayInputStream(xml.toByteArray()); // Create the model node NodeRef nodeRef = doInTransaction(errMsgId, requiresNewTx, new RetryingTransactionCallback<NodeRef>() { public NodeRef execute() throws Exception { return repoAdminService.deployModel(modelStream, modelFileName, activate); } }); if (!nodeService.hasAspect(nodeRef, ASPECT_CUSTOM_MODEL)) { // Add the 'customModelManagement' marker aspect, to // indicate that this model has been created dynamically by this service nodeService.addAspect(nodeRef, ASPECT_CUSTOM_MODEL, null); } // Add hidden aspect if (!hiddenAspect.hasHiddenAspect(nodeRef)) { hiddenAspect.hideNode(nodeRef, false, false, false); } return compiledModel; } /** * Validates the properties' non-null default values against the defined property constraints. * * @param compiledModel the compiled model * @throws CustomModelException.CustomModelConstraintException if there is constraint evaluation * exception */ private void validatePropsDefaultValues(CompiledModel compiledModel) { for (PropertyDefinition propertyDef : compiledModel.getProperties()) { if (propertyDef.getDefaultValue() != null && propertyDef.getConstraints().size() > 0) { for (ConstraintDefinition constraintDef : propertyDef.getConstraints()) { Constraint constraint = constraintDef.getConstraint(); try { constraint.evaluate(propertyDef.getDefaultValue()); } catch (AlfrescoRuntimeException ex) { String message = getRootCauseMsg(ex, false, "cmm.service.constraint.default_prop_value_err"); throw new CustomModelException.CustomModelConstraintException(message); } } } } } @Override public CompiledModel compileModel(M2Model m2Model) { try { // Validate model dependencies, constraints and etc. before creating a node return m2Model.compile(dictionaryDAO, namespaceDAO, true); } catch (Exception ex) { AlfrescoRuntimeException alf = null; if (ex instanceof AlfrescoRuntimeException) { alf = (AlfrescoRuntimeException) ex; } else { alf = AlfrescoRuntimeException.create(ex, ex.getMessage()); } Throwable cause = alf.getRootCause(); String message = null; if (cause instanceof DuplicateDefinitionException) { message = getRootCauseMsg(cause, false, MSG_INVALID_MODEL); throw new CustomModelException.CustomModelConstraintException(message); } else { message = getRootCauseMsg(cause, true, null); throw new CustomModelException.InvalidCustomModelException(MSG_INVALID_MODEL, new Object[] { message }, ex); } } } protected <T> PagingResults<T> wrapResult(PagingRequest pagingRequest, List<T> result) { final int totalSize = result.size(); final PageDetails pageDetails = PageDetails.getPageDetails(pagingRequest, totalSize); final List<T> page = new ArrayList<>(pageDetails.getPageSize()); Iterator<T> it = result.iterator(); for (int counter = 0; counter < pageDetails.getEnd() && it.hasNext(); counter++) { T element = it.next(); if (counter < pageDetails.getSkipCount()) { continue; } if (counter > pageDetails.getEnd() - 1) { break; } page.add(element); } return new PagingResults<T>() { @Override public List<T> getPage() { return page; } @Override public boolean hasMoreItems() { return pageDetails.hasMoreItems(); } @Override public Pair<Integer, Integer> getTotalResultCount() { Integer total = Integer.valueOf(totalSize); return new Pair<>(total, total); } @Override public String getQueryExecutionId() { return null; } }; } @Override public boolean isModelAdmin(String userName) { if (userName == null) { return false; } return this.authorityService.isAdminAuthority(userName) || this.authorityService .getAuthoritiesForUser(userName).contains(GROUP_ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY); } @Override public void activateCustomModel(String modelName) { try { repoAdminService.activateModel(modelName); } catch (Exception ex) { throw new CustomModelException(MSG_UNABLE_MODEL_ACTIVATE, new Object[] { modelName }, ex); } } @Override public void deactivateCustomModel(final String modelName) { CustomModelDefinition customModelDefinition = getCustomModel(modelName); if (customModelDefinition == null) { throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelName }); } Collection<TypeDefinition> modelTypes = customModelDefinition.getTypeDefinitions(); Collection<AspectDefinition> modelAspects = customModelDefinition.getAspectDefinitions(); for (CompiledModel cm : getAllCustomM2Models(false)) { // Ignore type/aspect dependency check within the model itself if (!customModelDefinition.getName().equals(cm.getModelDefinition().getName())) { // Check if the type of the model being deactivated is the parent of another model's type validateTypeAspectDependency(modelTypes, cm.getTypes()); // Check if the aspect of the model being deactivated is the parent of another model's aspect validateTypeAspectDependency(modelAspects, cm.getAspects()); } } // requiresNewTx = true, in order to catch any exception thrown within // "DictionaryModelType$DictionaryModelTypeTransactionListener" model validation. doInTransaction(MSG_UNABLE_MODEL_DEACTIVATE, true, new RetryingTransactionCallback<Void>() { public Void execute() throws Exception { repoAdminService.deactivateModel(modelName); return null; } }); } private void validateTypeAspectDependency(Collection<? extends ClassDefinition> parentDefs, Collection<? extends ClassDefinition> childDefs) { for (ClassDefinition parentClassDef : parentDefs) { for (ClassDefinition childClassDef : childDefs) { if (parentClassDef.getName().equals(childClassDef.getParentName())) { Object[] msgParams = new Object[] { parentClassDef.getName().toPrefixString(), childClassDef.getName().toPrefixString(), childClassDef.getModel().getName().getLocalName() }; if (parentClassDef instanceof TypeDefinition) { throw new CustomModelException.CustomModelConstraintException( MSG_FAILED_DEACTIVATION_TYPE_DEPENDENCY, msgParams); } else { throw new CustomModelException.CustomModelConstraintException( MSG_FAILED_DEACTIVATION_ASPECT_DEPENDENCY, msgParams); } } } } } @Override public void deleteCustomModel(String modelName) { NodeRef nodeRef = getModelNodeRef(modelName); if (nodeRef == null) { throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelName }); } final boolean isActive = Boolean.TRUE .equals(nodeService.getProperty(nodeRef, ContentModel.PROP_MODEL_ACTIVE)); if (isActive) { throw new CustomModelException.ActiveModelConstraintException(MSG_UNABLE_DELETE_ACTIVE_MODEL); } try { repoAdminService.undeployModel(modelName); } catch (Exception ex) { throw new CustomModelException(MSG_UNABLE_MODEL_DELETE, new Object[] { modelName }, ex); } } @Override public boolean isNamespaceUriExists(String modelNamespaceUri) { ParameterCheck.mandatoryString("modelNamespaceUri", modelNamespaceUri); Collection<String> uris = namespaceDAO.getURIs(); if (uris.contains(modelNamespaceUri)) { return true; } for (CompiledModel model : getAllCustomM2Models(false)) { if (modelNamespaceUri.equals(getModelNamespaceUriPrefix(model.getM2Model()).getFirst())) { return true; } } return false; } @Override public boolean isNamespacePrefixExists(String modelNamespacePrefix) { ParameterCheck.mandatoryString("modelNamespacePrefix", modelNamespacePrefix); Collection<String> prefixes = namespaceDAO.getPrefixes(); if (prefixes.contains(modelNamespacePrefix)) { return true; } for (CompiledModel model : getAllCustomM2Models(false)) { if (modelNamespacePrefix.equals(getModelNamespaceUriPrefix(model.getM2Model()).getSecond())) { return true; } } return false; } @Override public boolean isModelExists(String modelFileName) { NodeRef nodeRef = getModelNodeRef(modelFileName); if (nodeRef != null) { return true; } // Also check against the bootstrapped models for (QName qname : dictionaryService.getAllModels()) { if (qname.getLocalName().equalsIgnoreCase(modelFileName)) { return true; } } return false; } private Pair<String, String> getModelNamespaceUriPrefix(M2Model model) { ParameterCheck.mandatory("model", model); List<M2Namespace> namespaces = model.getNamespaces(); if (namespaces.isEmpty()) { throw new CustomModelException.InvalidNamespaceException(MSG_NAMESPACE_NOT_EXISTS, new Object[] { model.getName() }); } if (namespaces.size() > 1) { throw new CustomModelException.InvalidNamespaceException(MSG_NAMESPACE_MANY_EXIST, new Object[] { model.getName() }); } M2Namespace ns = namespaces.iterator().next(); return new Pair<>(ns.getUri(), ns.getPrefix()); } private void validateModelNamespaceUri(String uri) { if (isNamespaceUriExists(uri)) { throw new CustomModelException.NamespaceConstraintException(MSG_NAMESPACE_URI_ALREADY_IN_USE, new Object[] { uri }); } } private void validateModelNamespacePrefix(String prefix) { if (isNamespacePrefixExists(prefix)) { throw new CustomModelException.NamespaceConstraintException(MSG_NAMESPACE_PREFIX_ALREADY_IN_USE, new Object[] { prefix }); } } /** * A helper method to run a unit of work in a transaction. * * @param errMsgId message id for the new wrapper exception ({@link CustomModelException}) * when an exception occurs * @param requiresNewTx <tt>true</tt> to force a new transaction or * <tt>false</tt> to partake in any existing transaction * @param cb The callback containing the unit of work * @return Returns the result of the unit of work */ private <R> R doInTransaction(String errMsgId, boolean requiresNewTx, RetryingTransactionCallback<R> cb) { try { return retryingTransactionHelper.doInTransaction(cb, false, requiresNewTx); } catch (Exception ex) { AlfrescoRuntimeException alf = null; if (ex instanceof AlfrescoRuntimeException) { alf = (AlfrescoRuntimeException) ex; } else { alf = AlfrescoRuntimeException.create(ex, ex.getMessage()); } Throwable cause = alf.getRootCause(); String message = getRootCauseMsg(cause, true, null); throw new CustomModelException(errMsgId, new Object[] { message }, ex); } } private static String getRootCauseMsg(Throwable cause, boolean withAlfLogNum, String defaultMsg) { if (defaultMsg == null) { defaultMsg = ""; } String message = cause.getMessage(); if (message == null) { return defaultMsg; } else { return ((withAlfLogNum) ? message : message.replaceFirst("\\d+", "").trim()); } } @Override public NodeRef createDownloadNode(final String modelFileName, boolean withAssociatedForm) { List<NodeRef> nodesToBeDownloaded = new ArrayList<>(2); NodeRef customModelNodeRef = getModelNodeRef(modelFileName); if (customModelNodeRef == null) { throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelFileName }); } // We create a copy of the model, so we can rename it, change its // content type and move it to the download container in order to be // cleaned by the download service cleanup job. customModelNodeRef = createCustomModelCopy(modelFileName + ".xml", customModelNodeRef); nodesToBeDownloaded.add(customModelNodeRef); if (withAssociatedForm) { NodeRef shareExtModuleNodeRef = null; try { shareExtModuleNodeRef = createCustomModelShareExtModuleRef(modelFileName); nodesToBeDownloaded.add(shareExtModuleNodeRef); if (logger.isDebugEnabled()) { StringBuilder msg = new StringBuilder(); msg.append("Temp nodes created for download: Custom model nodeRef [").append(customModelNodeRef) .append("] and its associated Share form nodeRef [").append(shareExtModuleNodeRef) .append(']'); logger.debug(msg.toString()); } } catch (CustomModelException ex) { // We don't throw the exception as the Model might be a // draft model and might have never been activated or never had any forms created for it. // So in this case we just construct the zip containing only the model. StringBuilder msg = new StringBuilder(); msg.append("Constructing CMM zip file containing only the model [").append(modelFileName) .append(".xml] without its associated share extension module, because: ") .append(ex.getMessage()); logger.warn(msg.toString()); } } else { if (logger.isDebugEnabled()) { StringBuilder msg = new StringBuilder(); msg.append("Temp node created for download: Custom model nodeRef [").append(customModelNodeRef) .append(']'); logger.debug(msg.toString()); } } try { NodeRef archiveNodeRef = downloadService .createDownload(nodesToBeDownloaded.toArray(new NodeRef[nodesToBeDownloaded.size()]), false); if (logger.isDebugEnabled()) { StringBuilder msg = new StringBuilder(); msg.append("Created download nodeRef [").append(archiveNodeRef).append(']'); logger.debug(msg.toString()); } return archiveNodeRef; } catch (Exception ex) { throw new CustomModelException("cmm.service.download.create_err", ex); } } /** * Finds the {@code module} element within the Share persisted-extension * XML file and then writes the XML fragment as the content of a newly created node. * * @param modelName the model name * @return the created nodeRef */ protected NodeRef createCustomModelShareExtModuleRef(final String modelName) { final String moduleId = "CMM_" + modelName; final NodeRef formNodeRef = getShareExtModule(); ContentReader reader = contentService.getReader(formNodeRef, ContentModel.PROP_CONTENT); if (reader == null) { throw new CustomModelException("cmm.service.download.share_ext_node_read_err"); } InputStream in = reader.getContentInputStream(); Node moduleIdXmlNode = null; try { Document document = XMLUtil.parse(in); // the stream will be closed final String xpathQuery = "/extension//modules//module//id[.= '" + moduleId + "']"; XPath xPath = XPathFactory.newInstance().newXPath(); XPathExpression expression = xPath.compile(xpathQuery); moduleIdXmlNode = (Node) expression.evaluate(document, XPathConstants.NODE); } catch (Exception ex) { throw new CustomModelException("cmm.service.download.share_ext_file_parse_err", ex); } if (moduleIdXmlNode == null) { throw new CustomModelException("cmm.service.download.share_ext_module_not_found", new Object[] { moduleId }); } final File moduleFile = TempFileProvider.createTempFile(moduleId, ".xml"); try { XMLUtil.print(moduleIdXmlNode.getParentNode(), moduleFile); } catch (IOException error) { throw new CustomModelException("cmm.service.download.share_ext_write_err", new Object[] { moduleId }, error); } return doInTransaction(MSG_DOWNLOAD_CREATE_SHARE_EXT_ERR, true, new RetryingTransactionCallback<NodeRef>() { @Override public NodeRef execute() throws Exception { final NodeRef nodeRef = createDownloadTypeNode(moduleId + SHARE_EXT_MODULE_SUFFIX); ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); writer.setMimetype(MimetypeMap.MIMETYPE_XML); writer.setEncoding("UTF-8"); writer.putContent(moduleFile); return nodeRef; } }); } /** * Gets Share persisted-extension nodeRef */ protected NodeRef getShareExtModule() { List<NodeRef> results = searchService.selectNodes(getRootNode(), this.shareExtModulePath, null, this.namespaceDAO, false, SearchService.LANGUAGE_XPATH); if (results.isEmpty()) { throw new CustomModelException("cmm.service.download.share_ext_file_not_found"); } return results.get(0); } /** * Creates a copy of the custom model where the created node will be a child * of download container. * * @param newName the model new name * @param modelNodeRef existing model nodeRef * @return the created nodeRef */ protected NodeRef createCustomModelCopy(final String newName, final NodeRef modelNodeRef) { return doInTransaction(MSG_DOWNLOAD_COPY_MODEL_ERR, true, new RetryingTransactionCallback<NodeRef>() { @Override public NodeRef execute() throws Exception { final NodeRef newNodeRef = createDownloadTypeNode(newName); Serializable content = nodeService.getProperty(modelNodeRef, ContentModel.PROP_CONTENT); nodeService.setProperty(newNodeRef, ContentModel.PROP_CONTENT, content); return newNodeRef; } }); } /** * Creates node with a type {@link DownloadModel#TYPE_DOWNLOAD} within the * download container (see * {@link DownloadStorage#getOrCreateDowloadContainer()} ) * <p> * Also, the {@code IndexControlAspect} is applied to the created node. * * @param name the node name * @return the created nodeRef */ private NodeRef createDownloadTypeNode(final String name) { final NodeRef newNodeRef = nodeService.createNode(downloadStorage.getOrCreateDowloadContainer(), ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, DownloadModel.TYPE_DOWNLOAD, Collections.<QName, Serializable>singletonMap(ContentModel.PROP_NAME, name)).getChildRef(); Map<QName, Serializable> aspectProperties = new HashMap<>(2); aspectProperties.put(ContentModel.PROP_IS_INDEXED, Boolean.FALSE); aspectProperties.put(ContentModel.PROP_IS_CONTENT_INDEXED, Boolean.FALSE); nodeService.addAspect(newNodeRef, ContentModel.ASPECT_INDEX_CONTROL, aspectProperties); return newNodeRef; } @Override public CustomModelsInfo getCustomModelsInfo() { List<CustomModelDefinition> page = getCustomModels(new PagingRequest(0, Integer.MAX_VALUE)).getPage(); int activeModels = 0; int activeTypes = 0; int activeAspects = 0; for (CustomModelDefinition cm : page) { if (cm.isActive()) { activeModels++; activeTypes += cm.getTypeDefinitions().size(); activeAspects += cm.getAspectDefinitions().size(); } } CustomModelsInfo info = new CustomModelsInfo(); info.setNumberOfActiveModels(activeModels); info.setNumberOfActiveTypes(activeTypes); info.setNumberOfActiveAspects(activeAspects); return info; } }