/* * Copyright (C) 2009 eXo Platform SAS. * * This 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 2.1 of * the License, or (at your option) any later version. * * This software 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 this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.services.jcr.impl.core.nodetype.registration; import org.exoplatform.services.jcr.core.ExtendedPropertyType; import org.exoplatform.services.jcr.core.nodetype.NodeTypeData; import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager; import org.exoplatform.services.jcr.core.nodetype.PropertyDefinitionData; import org.exoplatform.services.jcr.dataflow.ItemDataConsumer; import org.exoplatform.services.jcr.dataflow.PlainChangesLog; import org.exoplatform.services.jcr.dataflow.PlainChangesLogImpl; import org.exoplatform.services.jcr.datamodel.InternalQName; import org.exoplatform.services.jcr.datamodel.ItemData; import org.exoplatform.services.jcr.datamodel.ItemType; import org.exoplatform.services.jcr.datamodel.NodeData; import org.exoplatform.services.jcr.datamodel.PropertyData; import org.exoplatform.services.jcr.datamodel.QPathEntry; import org.exoplatform.services.jcr.datamodel.ValueData; import org.exoplatform.services.jcr.impl.Constants; import org.exoplatform.services.jcr.impl.core.LocationFactory; import org.exoplatform.services.jcr.impl.core.nodetype.ItemAutocreator; import org.exoplatform.services.jcr.impl.core.value.ValueConstraintsMatcher; import org.exoplatform.services.jcr.impl.dataflow.ValueDataUtil; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.nodetype.ConstraintViolationException; /** * Created by The eXo Platform SAS. * * @author <a href="mailto:Sergey.Kabashnyuk@gmail.com">Sergey Kabashnyuk</a> * @version $Id: $ */ public class PropertyDefinitionComparator extends AbstractDefinitionComparator<PropertyDefinitionData> { /** * Class logger. */ private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.PropertyDefinitionComparator"); private final List<NodeData> affectedNodes; private final LocationFactory locationFactory; private final NodeTypeDataManager nodeTypeDataManager; private final ItemDataConsumer dataConsumer; private final ItemAutocreator itemAutocreator; /** * @param nodeTypeDataManager * @param dataConsumer * @param itemAutocreator * @param affectedNodes * @param locationFactory */ public PropertyDefinitionComparator(NodeTypeDataManager nodeTypeDataManager, ItemDataConsumer dataConsumer, ItemAutocreator itemAutocreator, List<NodeData> affectedNodes, LocationFactory locationFactory) { this.nodeTypeDataManager = nodeTypeDataManager; this.dataConsumer = dataConsumer; this.itemAutocreator = itemAutocreator; this.affectedNodes = affectedNodes; this.locationFactory = locationFactory; } @Override public PlainChangesLog compare(NodeTypeData registeredNodeType, PropertyDefinitionData[] ancestorDefinition, PropertyDefinitionData[] recipientDefinition) throws RepositoryException { List<PropertyDefinitionData> sameDefinitionData = new ArrayList<PropertyDefinitionData>(); List<RelatedDefinition<PropertyDefinitionData>> changedDefinitionData = new ArrayList<RelatedDefinition<PropertyDefinitionData>>(); List<PropertyDefinitionData> newDefinitionData = new ArrayList<PropertyDefinitionData>(); List<PropertyDefinitionData> removedDefinitionData = new ArrayList<PropertyDefinitionData>(); init(ancestorDefinition, recipientDefinition, sameDefinitionData, changedDefinitionData, newDefinitionData, removedDefinitionData); // create changes log PlainChangesLog changesLog = new PlainChangesLogImpl(); // removing properties validateRemoved(registeredNodeType, removedDefinitionData, recipientDefinition, affectedNodes); // new property definition validateAdded(registeredNodeType, newDefinitionData, recipientDefinition, affectedNodes); // changed validateChanged(registeredNodeType, changedDefinitionData, affectedNodes, recipientDefinition); // doAdd(newDefinitionData, changesLog, affectedNodes, registeredNodeType); return changesLog; } /** * @param registeredNodeType * @param recipientDefinitionData * @param allRecipientDefinition * @throws RepositoryException * @throws ConstraintViolationException */ private void checkIsMultiple(NodeTypeData registeredNodeType, PropertyDefinitionData recipientDefinitionData, PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException, ConstraintViolationException { List<NodeData> checkIsMultipleNodes; if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName())) { checkIsMultipleNodes = nodesData; } else { checkIsMultipleNodes = getNodes(nodesData, new InternalQName[]{recipientDefinitionData.getName()}, new InternalQName[]{}); } for (NodeData nodeData : checkIsMultipleNodes) { if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName())) { List<PropertyData> propertyDatas = dataConsumer.getChildPropertiesData(nodeData); for (PropertyData propertyData : propertyDatas) { // skip mixin and primary type if (isResidualMatch(propertyData.getQPath().getName(), allRecipientDefinition)) { if (propertyData.getValues().size() > 1) { throw new ConstraintViolationException("Can't change property definition " + recipientDefinitionData.getName().getAsString() + " to isMultiple = false because property " + propertyData.getQPath().getAsString() + " contains more then one value"); } } } } else { PropertyData propertyData = (PropertyData)dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0), ItemType.PROPERTY); if (propertyData.getValues().size() > 1) { throw new ConstraintViolationException("Can't change property definition " + recipientDefinitionData.getName().getAsString() + " to isMultiple = false because property " + propertyData.getQPath().getAsString() + " contains more then one value"); } } } } /** * @param registeredNodeType * @param nodesData * @param recipientDefinitionData * @throws RepositoryException */ private void checkMandatory(NodeTypeData registeredNodeType, List<NodeData> nodesData, PropertyDefinitionData recipientDefinitionData) throws RepositoryException { if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()) && recipientDefinitionData.isMandatory()) throw new ConstraintViolationException("Invalid property definition " + recipientDefinitionData.getName() + ". Residual definition can't be mandatory"); List<NodeData> mandatoryNodes = getNodes(nodesData, new InternalQName[]{}, new InternalQName[]{recipientDefinitionData.getName()}); if (mandatoryNodes.size() > 0) { StringBuilder message = new StringBuilder("Can not change ").append(recipientDefinitionData.getName().getAsString()).append( " property definition from mandatory=false to mandatory = true , because ").append(" the following nodes "); for (NodeData nodeData : mandatoryNodes) { message.append(nodeData.getQPath().getAsString()).append(" "); } message.append(" doesn't have these properties "); throw new ConstraintViolationException(message.toString()); } } /** * @param registeredNodeType * @param recipientDefinitionData * @param allRecipientDefinition * @throws RepositoryException */ private void checkRequiredType(NodeTypeData registeredNodeType, PropertyDefinitionData recipientDefinitionData, PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException { List<NodeData> requiredTypeNodes; if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName())) { requiredTypeNodes = nodesData; } else { requiredTypeNodes = getNodes(nodesData, new InternalQName[]{recipientDefinitionData.getName()}, new InternalQName[]{}); } for (NodeData nodeData : requiredTypeNodes) { if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName())) { List<PropertyData> propertyDatas = dataConsumer.getChildPropertiesData(nodeData); for (PropertyData propertyData : propertyDatas) { // skip mixin and primary type if (isResidualMatch(propertyData.getQPath().getName(), allRecipientDefinition)) { if (recipientDefinitionData.getRequiredType() != PropertyType.UNDEFINED && propertyData.getType() != recipientDefinitionData.getRequiredType()) { throw new ConstraintViolationException("Can not change requiredType to " + ExtendedPropertyType.nameFromValue(recipientDefinitionData.getRequiredType()) + " in " + recipientDefinitionData.getName().getAsString() + " because " + propertyData.getQPath().getAsString() + " have " + ExtendedPropertyType.nameFromValue(propertyData.getType())); } } } } else { PropertyData propertyData = (PropertyData)dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0), ItemType.PROPERTY); if (recipientDefinitionData.getRequiredType() != PropertyType.UNDEFINED && propertyData.getType() != recipientDefinitionData.getRequiredType()) { throw new ConstraintViolationException("Can not change requiredType to " + ExtendedPropertyType.nameFromValue(recipientDefinitionData.getRequiredType()) + " in " + recipientDefinitionData.getName().getAsString() + " because " + propertyData.getQPath().getAsString() + " have " + ExtendedPropertyType.nameFromValue(propertyData.getType())); } } } } /** * @param registeredNodeType * @param recipientDefinitionData * @param allRecipientDefinition * @throws RepositoryException * @throws ConstraintViolationException */ private void checkValueConstraints(NodeTypeData registeredNodeType, PropertyDefinitionData recipientDefinitionData, PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException, ConstraintViolationException { List<NodeData> checkValueConstraintsNodes; if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName())) { checkValueConstraintsNodes = nodesData; } else { checkValueConstraintsNodes = getNodes(nodesData, new InternalQName[]{recipientDefinitionData.getName()}, new InternalQName[]{}); } for (NodeData nodeData : checkValueConstraintsNodes) { if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName())) { List<PropertyData> propertyDatas = dataConsumer.getChildPropertiesData(nodeData); for (PropertyData propertyData : propertyDatas) { // skip mixin and primary type if (isResidualMatch(propertyData.getQPath().getName(), allRecipientDefinition)) { checkValueConstraints(recipientDefinitionData, propertyData); } } } else { PropertyData propertyData = (PropertyData)dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0), ItemType.PROPERTY); checkValueConstraints(recipientDefinitionData, propertyData); } } } private void checkValueConstraints(PropertyDefinitionData def, PropertyData propertyData) throws RepositoryException { ValueConstraintsMatcher constraints = new ValueConstraintsMatcher(def.getValueConstraints(), locationFactory, dataConsumer, nodeTypeDataManager); for (ValueData value : propertyData.getValues()) { if (!constraints.match(value, propertyData.getType())) { String strVal = null; try { if (propertyData.getType() != PropertyType.BINARY) { // may have large size strVal = ValueDataUtil.getString(value); } else { strVal = "PropertyType.BINARY"; } } catch (IllegalStateException e) { LOG.error("Error of value read: " + e.getMessage(), e); } throw new ConstraintViolationException("Value " + strVal + " for property " + propertyData.getQPath().getAsString() + " doesn't match new constraint "); } } } /** * @param toAddList * @param changesLog * @param nodesData * @param registeredNodeType * @throws RepositoryException */ private void doAdd(List<PropertyDefinitionData> toAddList, PlainChangesLog changesLog, List<NodeData> nodesData, NodeTypeData registeredNodeType) throws RepositoryException { for (NodeData nodeData : nodesData) { // added properties for (PropertyDefinitionData newPropertyDefinitionData : toAddList) { if (!newPropertyDefinitionData.getName().equals(Constants.JCR_ANY_NAME) && newPropertyDefinitionData.isAutoCreated()) { ItemData pdata = dataConsumer.getItemData(nodeData, new QPathEntry(newPropertyDefinitionData.getName(), 0), ItemType.UNKNOWN); if (pdata == null || (pdata != null && pdata.isNode())) { PlainChangesLog autoCreatedChanges = itemAutocreator.makeAutoCreatedProperties(nodeData, registeredNodeType.getName(), new PropertyDefinitionData[]{newPropertyDefinitionData}, dataConsumer, nodeData.getACL() .getOwner()); if (autoCreatedChanges.getSize() == 0) { throw new ConstraintViolationException("Fail to add property by definition: " + newPropertyDefinitionData.getName().getAsString() + " Possible no default values defined."); } changesLog.addAll(autoCreatedChanges.getAllStates()); } } } } } /** * @param nodes * @param includeProperties * @param excludeProperties * @return All nodes from list nodes, what include properties from * includeProperties, and doesn't include properties from * excludeProperties. * @throws RepositoryException */ private List<NodeData> getNodes(List<NodeData> nodes, InternalQName[] includeProperties, InternalQName[] excludeProperties) throws RepositoryException { List<NodeData> result = new ArrayList<NodeData>(); for (NodeData nodeData : nodes) { // search all properties List<PropertyData> childProperties = dataConsumer.listChildPropertiesData(nodeData); boolean toAdd = includeProperties.length == 0; // check included for (int i = 0; i < includeProperties.length; i++) { for (PropertyData propertyData : childProperties) { if (propertyData.getQPath().getName().equals(includeProperties[i])) { toAdd = true; break; } } } if (toAdd) { // check excluded for (int i = 0; i < excludeProperties.length; i++) { for (PropertyData propertyData : childProperties) { if (propertyData.getQPath().getName().equals(excludeProperties[i])) { toAdd = false; break; } } } if (toAdd) result.add(nodeData); } } return result; } /** * @param registeredNodeType * @param newDefinitionData * @param allRecipientDefinition * @param nodesData * @throws RepositoryException */ private void validateAdded(NodeTypeData registeredNodeType, List<PropertyDefinitionData> newDefinitionData, PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException { if (newDefinitionData.size() > 0) { for (PropertyDefinitionData propertyDefinitionData : newDefinitionData) { if (propertyDefinitionData.getName().equals(Constants.JCR_ANY_NAME)) { // Required type change checkRequiredType(registeredNodeType, propertyDefinitionData, allRecipientDefinition, nodesData); // ValueConstraints checkValueConstraints(registeredNodeType, propertyDefinitionData, allRecipientDefinition, nodesData); // multiple change checkIsMultiple(registeredNodeType, propertyDefinitionData, allRecipientDefinition, nodesData); } } } } /** * @param registeredNodeType * @param changedDefinitionData * @param nodesData * @param allRecipientDefinition * @throws RepositoryException */ private void validateChanged(NodeTypeData registeredNodeType, List<RelatedDefinition<PropertyDefinitionData>> changedDefinitionData, List<NodeData> nodesData, PropertyDefinitionData[] allRecipientDefinition) throws RepositoryException { for (RelatedDefinition<PropertyDefinitionData> relatedDefinitions : changedDefinitionData) { PropertyDefinitionData ancestorDefinitionData = relatedDefinitions.getAncestorDefinition(); PropertyDefinitionData recipientDefinitionData = relatedDefinitions.getRecepientDefinition(); // change from mandatory=false to mandatory = true if (!ancestorDefinitionData.isMandatory() && recipientDefinitionData.isMandatory()) { checkMandatory(registeredNodeType, nodesData, recipientDefinitionData); } // No need to check protected // change from Protected=false to Protected = true // if (!ancestorDefinitionData.isProtected() && // recipientDefinitionData.isProtected()) { // checkProtected(registeredNodeType, nodesData, recipientDefinitionData); // } // Required type change if (ancestorDefinitionData.getRequiredType() != recipientDefinitionData.getRequiredType() && recipientDefinitionData.getRequiredType() != PropertyType.UNDEFINED) { checkRequiredType(registeredNodeType, recipientDefinitionData, allRecipientDefinition, nodesData); } // ValueConstraints if (!Arrays.deepEquals(ancestorDefinitionData.getValueConstraints(), recipientDefinitionData.getValueConstraints())) { checkValueConstraints(registeredNodeType, recipientDefinitionData, allRecipientDefinition, nodesData); } // multiple change if (ancestorDefinitionData.isMultiple() && !recipientDefinitionData.isMultiple()) { checkIsMultiple(registeredNodeType, recipientDefinitionData, allRecipientDefinition, nodesData); } } } /** * @param registeredNodeType * @param recipientDefinition * @param recipientDefinition * @param nodesData * @throws RepositoryException */ private void validateRemoved(NodeTypeData registeredNodeType, List<PropertyDefinitionData> removedDefinitionData, PropertyDefinitionData[] recipientDefinition, List<NodeData> nodesData) throws RepositoryException { for (PropertyDefinitionData removePropertyDefinitionData : removedDefinitionData) { if (removePropertyDefinitionData.getName().equals(Constants.JCR_ANY_NAME)) { for (NodeData nodeData : nodesData) { List<PropertyData> childs = dataConsumer.getChildPropertiesData(nodeData); // more then mixin and primary type for (PropertyData propertyData : childs) { if (!isNonResidualMatch(propertyData.getQPath().getName(), recipientDefinition)) { throw new ConstraintViolationException("Can't remove residual property definition for " + registeredNodeType.getName().getAsString() + " node type, because node " + nodeData.getQPath().getAsString() + " contains property " + propertyData.getQPath().getName().getAsString()); } } } } else if (!isResidualMatch(removePropertyDefinitionData.getName(), recipientDefinition)) { List<NodeData> nodes = getNodes(nodesData, new InternalQName[]{removePropertyDefinitionData.getName()}, new InternalQName[]{}); if (nodes.size() > 0) { StringBuilder message = new StringBuilder("Can not remove ").append(removePropertyDefinitionData.getName().getAsString()) .append(" PropertyDefinitionData, because the following nodes have these properties: "); for (NodeData nodeData : nodes) { message.append(nodeData.getQPath().getAsString()).append(" "); } throw new ConstraintViolationException(message.toString()); } } } } }