content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  package org.apache.chemistry.opencmis.server.support;
  
  import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.*;
  
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
  import org.apache.chemistry.opencmis.commons.PropertyIds;
  import org.apache.chemistry.opencmis.commons.data.Acl;
  import org.apache.chemistry.opencmis.commons.data.Properties;
  import org.apache.chemistry.opencmis.commons.data.PropertyData;
  import org.apache.chemistry.opencmis.commons.definitions.Choice;
  import org.apache.chemistry.opencmis.commons.definitions.DocumentTypeDefinition;
  import org.apache.chemistry.opencmis.commons.definitions.PropertyDecimalDefinition;
  import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
  import org.apache.chemistry.opencmis.commons.definitions.PropertyIntegerDefinition;
  import org.apache.chemistry.opencmis.commons.definitions.PropertyStringDefinition;
  import org.apache.chemistry.opencmis.commons.definitions.RelationshipTypeDefinition;
  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
  import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
  import org.apache.chemistry.opencmis.commons.enums.Cardinality;
  import org.apache.chemistry.opencmis.commons.enums.ContentStreamAllowed;
  import org.apache.chemistry.opencmis.commons.enums.VersioningState;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
  
  public final class TypeValidator {
  
      private TypeValidator() {
      }
  
      public static void validateRequiredSystemProperties(Properties properties) {
          if (properties == null || properties.getProperties() == null) {
              throw new CmisInvalidArgumentException("Cannot create object, no properties are given");
          }
  
          if (!properties.getProperties().containsKey(PropertyIds.OBJECT_TYPE_ID)) {
              throw new CmisInvalidArgumentException("Cannot create object, type id is missing");
          }
      }
  
      private static boolean isMandatorySystemProperty(String propertyId) {
          return propertyId.equals(PropertyIds.OBJECT_TYPE_ID);
      }
  
      @SuppressWarnings("unchecked")
      static <T> PropertyValidator<T> createPropertyValidator(PropertyDefinition<?> propDef) {
          PropertyValidator<T> result = null;
          if (propDef instanceof PropertyIntegerDefinition) {
              result = (PropertyValidator<T>) new PropertyValidatorInteger();
          } else if (propDef instanceof PropertyDecimalDefinition) {
              result = (PropertyValidator<T>) new PropertyValidatorDecimal();
          } else if (propDef instanceof PropertyStringDefinition) {
              result = (PropertyValidator<T>) new PropertyValidatorString();
          } else {
              result = new PropertyValidator<T>();
          }
          return result;
      }
  
      /*
       * property validations: not readonly, all required are given, all are known
       * in type cardinality: no multi values for single value, def min max check
       * for Integer and Decimal, choices and in list Strings, max length set
       * default value for omitted properties
       */
      static class PropertyValidator<T> {
  
          public void validate(PropertyDefinition<T> propDef, PropertyData<T> prop) {
  
              // check general constraints for all property types
              if (propDef.getCardinality() == Cardinality.SINGLE) {
                  if (prop.getValues() != null && prop.getValues().size() > 1) {
                      throw new CmisConstraintException("The property with id " + propDef.getId()
                              + " is single valued, but multiple values are passed " + prop.getValues());
                  }
              }
  
              if (isNotEmpty(propDef.getChoices())) {
                  validateChoices(propDef, prop);
              }
          }
  
          private void validateChoices(PropertyDefinition<T> propDef, PropertyData<T> prop) {
              boolean isAllowedValue = true;
              boolean hasMultiValueChoiceLists = false;
              for (Choice<?> allowedValue : propDef.getChoices()) {
                  if (allowedValue.getValue() != null && allowedValue.getValue().size() > 1) {
                      hasMultiValueChoiceLists = true;
                  }
              }
  
              if (propDef.isOpenChoice() != null && propDef.isOpenChoice()) {
              	isAllowedValue = true;
              } else if (hasMultiValueChoiceLists) {
                  // check if value is in list
  
              	// do a complex check if this combination of actual values is
                  // allowed check if value is in list
                  isAllowedValue = false;
                  List<?> actualValues = prop.getValues();
                  for (Choice<?> allowedValue : propDef.getChoices()) {
                      if (allowedValue.getValue().size() == actualValues.size()) {
                          boolean listValuesAreEqual = true;
                          Iterator<?> it = allowedValue.getValue().iterator();
                          for (Object actualValue : actualValues) {
                              if (!actualValue.equals(it.next())) {
                                  listValuesAreEqual = false;
                                  break;
                              }
                          }
                          if (listValuesAreEqual) {
                              isAllowedValue = true;
                          }
                      }
  
                      if (isAllowedValue) {
                          break;
                      }
                  }
  
              } else {
                  List<T> allowedValues = getAllowedValues(propDef.getChoices());
                  // do a simpler check if all values are choice elements
  
                  for (Object actualValue : prop.getValues()) {
                      if (!allowedValues.contains(actualValue)) {
                          isAllowedValue = false;
                          break;
                      }
                  }
              }
  
              if (!isAllowedValue) {
                  throw new CmisConstraintException("The property with id " + propDef.getId()
                          + " has a fixed set of values. Value(s) " + prop.getValues() + " are not listed.");
              }
          }
  
          /**
           * Calculate the list of allowed values for this property definition by
           * recursively collecting all choice values from property definition
           * 
           * @param propDef
           *            property definition
           * @return list of possible values in complete hierarchy
           */
          private List<T> getAllowedValues(List<Choice<T>> choices) {
              List<T> allowedValues = new ArrayList<T>(choices.size());
              for (Choice<T> choice : choices) {
                  if (isNotEmpty(choice.getValue())) {
                      allowedValues.add(choice.getValue().get(0));
                  }
                  if (isNotEmpty(choice.getChoice())) {
                      List<Choice<T>> x = choice.getChoice();
                      allowedValues.addAll(getAllowedValues(x));
                  }
              }
              return allowedValues;
          }
      }
  
      static class PropertyValidatorInteger extends PropertyValidator<BigInteger> {
  
          @Override
          public void validate(PropertyDefinition<BigInteger> propDef, PropertyData<BigInteger> property) {
  
              super.validate(propDef, property);
  
              BigInteger propVal = property.getFirstValue();
              BigInteger minVal = ((PropertyIntegerDefinition) propDef).getMinValue();
              BigInteger maxVal = ((PropertyIntegerDefinition) propDef).getMaxValue();
  
              // check min and max
              if (minVal != null && propVal != null && propVal.compareTo(minVal) == -1) {
                  throw new CmisConstraintException("For property with id " + propDef.getId() + " the value " + propVal
                          + " is less than the minimum value " + minVal);
              }
              if (maxVal != null && propVal != null && propVal.compareTo(maxVal) == 1) {
                  throw new CmisConstraintException("For property with id " + propDef.getId() + " the value " + propVal
                          + " is bigger than the maximum value " + maxVal);
              }
          }
      }
  
      static class PropertyValidatorDecimal extends PropertyValidator<BigDecimal> {
  
          @Override
          public void validate(PropertyDefinition<BigDecimal> propDef, PropertyData<BigDecimal> property) {
  
              super.validate(propDef, property);
  
              BigDecimal propVal = property.getFirstValue();
              BigDecimal minVal = ((PropertyDecimalDefinition) propDef).getMinValue();
              BigDecimal maxVal = ((PropertyDecimalDefinition) propDef).getMaxValue();
  
              // check min and max
              if (minVal != null && propVal != null && propVal.compareTo(minVal) == -1) {
                  throw new CmisConstraintException("For property with id " + propDef.getId() + " the value " + propVal
                          + " is less than the minimum value " + minVal);
              }
              if (maxVal != null && propVal != null && propVal.compareTo(maxVal) == 1) {
                  throw new CmisConstraintException("For property with id " + propDef.getId() + " the value " + propVal
                          + " is bigger than the maximum value " + maxVal);
              }
          }
      }
  
      static class PropertyValidatorString extends PropertyValidator<String> {
  
          @Override
          public void validate(PropertyDefinition<String> propDef, PropertyData<String> property) {
  
              super.validate(propDef, property);
  
              long maxLen = ((PropertyStringDefinition) propDef).getMaxLength() == null ? -1
                      : ((PropertyStringDefinition) propDef).getMaxLength().longValue();
              long len = property.getFirstValue() == null ? -1 : property.getFirstValue().length();
  
              // check max length
              if (maxLen >= 0 && len >= 0 && maxLen < len) {
                  throw new CmisConstraintException("For property with id " + propDef.getId() + " the length of " + len
                          + " is bigger than the maximum allowed length  " + maxLen);
              }
          }
      }
  
      public static <T> void validateProperties(TypeDefinition typeDef, Properties properties, boolean checkMandatory) {
          validateProperties(typeDef, properties, checkMandatory, false);
      }
  
      @SuppressWarnings("unchecked")
      public static <T> void validateProperties(TypeDefinition typeDef, Properties properties, boolean checkMandatory,
              boolean cmis11) {
          List<String> propDefsRequired = getMandatoryPropDefs(typeDef.getPropertyDefinitions());
  
          if (properties != null) {
              for (PropertyData<?> prop : properties.getProperties().values()) {
                  String propertyId = prop.getId();
                  if (null == propertyId) {
                      throw new CmisInvalidArgumentException("Property id cannot be null");
                  }
                  BaseTypeId baseTypeId = typeDef.getBaseTypeId();
  
                  // check that all mandatory attributes are present
                  if (checkMandatory && propDefsRequired.contains(propertyId)) {
                      propDefsRequired.remove(propertyId);
                  }
  
                  if (isSystemProperty(baseTypeId, propertyId, cmis11)) {
                      continue; // ignore system properties for validation
                  }
  
                  // Check if all properties are known in the type
                  if (typeContainsProperty(typeDef, propertyId)) {
                      // check all type specific constraints:
                      PropertyDefinition<T> propDef = getPropertyDefinition(typeDef, propertyId);
                      PropertyValidator<T> validator = createPropertyValidator(propDef);
                      validator.validate(propDef, (PropertyData<T>) prop);
                  } else {
                      throw new CmisConstraintException("Unknown property " + propertyId + " in type " + typeDef.getId());
                  }
              }
          }
  
          if (checkMandatory && !propDefsRequired.isEmpty()) {
              throw new CmisConstraintException("The following mandatory properties are missing: " + propDefsRequired);
          }
      }
  
      @SuppressWarnings("unchecked")
      public static <T> void validateProperties(List<TypeDefinition> typeDefs, Properties properties,
              boolean checkMandatory) {
          if (properties == null) {
              return;
          }
  
          Map<String, Boolean> checkedProperties = new HashMap<String, Boolean>();
          for (String propId : properties.getProperties().keySet()) {
              checkedProperties.put(propId, false);
          }
  
          for (TypeDefinition typeDef : typeDefs) {
  
              List<String> propDefsRequired = getMandatoryPropDefs(typeDef.getPropertyDefinitions());
  
              for (PropertyData<?> prop : properties.getProperties().values()) {
                  String propertyId = prop.getId();
                  if (null == propertyId) {
                      throw new CmisInvalidArgumentException("Property id cannot be null");
                  }
                  BaseTypeId baseTypeId = typeDef.getBaseTypeId();
  
                  // check that all mandatory attributes are present
                  if (checkMandatory && propDefsRequired.contains(propertyId)) {
                      propDefsRequired.remove(propertyId);
                  }
  
                  if (isSystemProperty(baseTypeId, propertyId, true)) {
                      checkedProperties.put(prop.getId(), true); // ignore system
                                                                 // properties for
                                                                 // validation
                  } else if (typeContainsProperty(typeDef, propertyId)) {
                      // Check if all properties are known in the type
                      // marked the property as found in a type of primary or
                      // secondary types
                      checkedProperties.put(prop.getId(), true);
  
                      // check all type specific constraints:
                      PropertyDefinition<T> propDef = getPropertyDefinition(typeDef, propertyId);
                      PropertyValidator<T> validator = createPropertyValidator(propDef);
                      validator.validate(propDef, (PropertyData<T>) prop);
                  }
              }
  
              if (checkMandatory && !propDefsRequired.isEmpty()) {
                  throw new CmisConstraintException("The following mandatory properties are missing: " + propDefsRequired);
              }
          }
  
          // check if all properties are known in a type definition
          List<String> unknownProperties = new ArrayList<String>();
          for (String propId : properties.getProperties().keySet()) {
              if (!checkedProperties.get(propId)) {
                  unknownProperties.add(propId);
              }
          }
          if (!unknownProperties.isEmpty()) {
              throw new CmisConstraintException(
                      "The following properties are not known in any of the types of this object: " + unknownProperties);
          }
      }
  
      public static void validateVersionStateForCreate(DocumentTypeDefinition typeDef, VersioningState verState) {
          if (null == verState) {
              return;
          }
          if (typeDef.isVersionable() && verState.equals(VersioningState.NONE) || !typeDef.isVersionable()
                  && !verState.equals(VersioningState.NONE)) {
              throw new CmisConstraintException("The versioning state flag is imcompatible to the type definition.");
          }
  
      }
  
      public static void validateAllowedChildObjectTypes(TypeDefinition childTypeDef, List<String> allowedChildTypes) {
  
          validateAllowedTypes(childTypeDef, allowedChildTypes, "in this folder");
      }
  
      public static void validateAllowedRelationshipTypes(RelationshipTypeDefinition relationshipTypeDef,
              TypeDefinition sourceTypeDef, TypeDefinition targetTypeDef) {
          List<String> allowedSourceTypes = relationshipTypeDef.getAllowedSourceTypeIds();
          validateAllowedTypes(sourceTypeDef, allowedSourceTypes, " as source type in this relationship");
          List<String> allowedTargetTypes = relationshipTypeDef.getAllowedTargetTypeIds();
          validateAllowedTypes(targetTypeDef, allowedTargetTypes, " as target type in this relationship");
      }
  
      protected static void validateAllowedTypes(TypeDefinition typeDef, List<String> allowedTypes, String description) {
          if (isNullOrEmpty(allowedTypes)) {
              return; // all types are allowed
          }
  
          for (String allowedType : allowedTypes) {
              if (allowedType.equals(typeDef.getId())) {
                  return;
              }
          }
          throw new CmisConstraintException("The requested type " + typeDef.getId() + " is not allowed " + description);
      }
  
      public static void validateAcl(TypeDefinition typeDef, Acl addACEs, Acl removeACEs) {
          if (!typeDef.isControllableAcl() && (addACEs != null || removeACEs != null)) {
              throw new CmisConstraintException("acl set for type: " + typeDef.getDisplayName()
                      + " that is not controllableACL");
          }
      }
  
      public static void validateContentAllowed(DocumentTypeDefinition typeDef, boolean hasContent) {
          ContentStreamAllowed contentAllowed = typeDef.getContentStreamAllowed();
          if (ContentStreamAllowed.REQUIRED == contentAllowed && !hasContent) {
              throw new CmisConstraintException("Type " + typeDef.getId()
                      + " requires content but document has no content.");
          } else if (ContentStreamAllowed.NOTALLOWED == contentAllowed && hasContent) {
              throw new CmisConstraintException("Type " + typeDef.getId()
                      + " does not allow content but document has content.");
          }
      }
  
      private static List<String> getMandatoryPropDefs(Map<String, PropertyDefinition<?>> propDefs) {
          List<String> res = new ArrayList<String>();
          if (null != propDefs) {
              for (PropertyDefinition<?> propDef : propDefs.values()) {
                  if (propDef.isRequired() && !isMandatorySystemProperty(propDef.getId())) {
                      res.add(propDef.getId());
                  }
              }
          }
          return res;
      }
  
      public static boolean typeContainsProperty(TypeDefinition typeDef, String propertyId) {
  
          Map<String, PropertyDefinition<?>> propDefs = typeDef.getPropertyDefinitions();
          if (null == propDefs) {
              return false;
          }
  
          PropertyDefinition<?> propDef = propDefs.get(propertyId);
  
          return propDef != null;
      }
  
      public static boolean typeContainsPropertyWithQueryName(TypeDefinition typeDef, String propertyQueryName) {
  
          Map<String, PropertyDefinition<?>> propDefs = typeDef.getPropertyDefinitions();
          if (null == propDefs) {
              return false;
          }
  
          for (PropertyDefinition<?> propDef : propDefs.values()) {
              if (propDef.getQueryName().equalsIgnoreCase(propertyQueryName)) {
                  return true;
              }
          }
  
          return false; // unknown property query name in this type
      }
  
      @SuppressWarnings("unchecked")
      private static <T> PropertyDefinition<T> getPropertyDefinition(TypeDefinition typeDef, String propertyId) {
  
          Map<String, PropertyDefinition<?>> propDefs = typeDef.getPropertyDefinitions();
          if (null == propDefs) {
              return null;
          }
  
          PropertyDefinition<?> propDef = propDefs.get(propertyId);
  
          if (null == propDef) {
              return null; // not found
          } else {
              return (PropertyDefinition<T>) propDef;
          }
      }
  
      private static boolean isSystemProperty(BaseTypeId baseTypeId, String propertyId, boolean cmis11) {
  
          if (propertyId.equals(PropertyIds.NAME)) {
              return true;
          } else if (propertyId.equals(PropertyIds.OBJECT_ID)) {
              return true;
          } else if (propertyId.equals(PropertyIds.OBJECT_TYPE_ID)) {
              return true;
          } else if (propertyId.equals(PropertyIds.BASE_TYPE_ID)) {
              return true;
          } else if (propertyId.equals(PropertyIds.CREATED_BY)) {
              return true;
          } else if (propertyId.equals(PropertyIds.CREATION_DATE)) {
              return true;
          } else if (propertyId.equals(PropertyIds.LAST_MODIFIED_BY)) {
              return true;
          } else if (propertyId.equals(PropertyIds.LAST_MODIFICATION_DATE)) {
              return true;
          } else if (propertyId.equals(PropertyIds.CHANGE_TOKEN)) {
              return true;
          } else if (cmis11 && propertyId.equals(PropertyIds.DESCRIPTION)) {
              return true;
          } else if (cmis11 && propertyId.equals(PropertyIds.SECONDARY_OBJECT_TYPE_IDS)) {
              return true;
          }
  
          if (baseTypeId.equals(BaseTypeId.CMIS_DOCUMENT)) {
              if (propertyId.equals(PropertyIds.IS_IMMUTABLE)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.IS_LATEST_VERSION)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.IS_MAJOR_VERSION)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.VERSION_SERIES_ID)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.IS_LATEST_MAJOR_VERSION)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.VERSION_LABEL)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.IS_VERSION_SERIES_CHECKED_OUT)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.VERSION_SERIES_CHECKED_OUT_BY)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.VERSION_SERIES_CHECKED_OUT_ID)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.CHECKIN_COMMENT)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.CONTENT_STREAM_LENGTH)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.CONTENT_STREAM_MIME_TYPE)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.CONTENT_STREAM_FILE_NAME)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.CONTENT_STREAM_ID)) {
                  return true;
              }
  
              return false;
          } else if (baseTypeId.equals(BaseTypeId.CMIS_FOLDER)) {
              if (propertyId.equals(PropertyIds.PARENT_ID)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.ALLOWED_CHILD_OBJECT_TYPE_IDS)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.PATH)) {
                  return true;
              }
  
              return false;
          } else if (baseTypeId.equals(BaseTypeId.CMIS_POLICY)) {
              if (propertyId.equals(PropertyIds.SOURCE_ID)) {
                  return true;
              } else if (propertyId.equals(PropertyIds.TARGET_ID)) {
                  return true;
              }
              return false;
          } else { // relationship
              if (propertyId.equals(PropertyIds.POLICY_TEXT)) {
                  return true;
              }
  
              return false;
          }
      }
  }
  