/** * This file is part of the JCROM project. * Copyright (C) 2008-2014 - All rights reserved. * Authors: Olafur Gauti Gudmundsson, Nicolas Dos Santos * * Licensed 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.jcrom; import java.io.Serializable; import java.lang.reflect.Field; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javafx.beans.property.*; import org.jcrom.annotations.JcrBaseVersionCreated; import org.jcrom.annotations.JcrBaseVersionName; import org.jcrom.annotations.JcrCheckedout; import org.jcrom.annotations.JcrChildNode; import org.jcrom.annotations.JcrCreated; import org.jcrom.annotations.JcrFileNode; import org.jcrom.annotations.JcrIdentifier; import org.jcrom.annotations.JcrName; import org.jcrom.annotations.JcrParentNode; import org.jcrom.annotations.JcrPath; import org.jcrom.annotations.JcrProperty; import org.jcrom.annotations.JcrProtectedProperty; import org.jcrom.annotations.JcrReference; import org.jcrom.annotations.JcrSerializedProperty; import org.jcrom.annotations.JcrUUID; import org.jcrom.annotations.JcrVersionCreated; import org.jcrom.annotations.JcrVersionName; import org.jcrom.util.ReflectionUtils; import static org.jcrom.util.JavaFXUtils.*; /** * This class is used by Jcrom to validate that the classes being mapped are correctly annotated as valid JCR classes. * * @author Olafur Gauti Gudmundsson * @author Nicolas Dos Santos */ class Validator { private static final Logger logger = Logger.getLogger(Validator.class.getName()); private final Jcrom jcrom; public Validator(Jcrom jcrom) { this.jcrom = jcrom; } /** * Takes a class, validates it, and adds it to a Set which is then returned. All annotated classes referenced from * this class (e.g. child nodes) are also validated and added to the set. Throws a JcrMappingException if invalid * mapping is found. * * @param c * the Class to be validated * @param dynamicInstantiation * when dynamic instantiation is on, we allow interfaces * @return a Set of the input class and referenced classes, validated and ready for mapping */ Set<Class<?>> validate(Class<?> c, boolean dynamicInstantiation) { Set<Class<?>> validClasses = new HashSet<Class<?>>(); validateInternal(c, validClasses, dynamicInstantiation); return validClasses; } private void validateInternal(Class<?> c, Set<Class<?>> validClasses, boolean dynamicInstantiation) { if (!validClasses.contains(c)) { if (logger.isLoggable(Level.FINE)) { logger.finer("Processing class: " + c.getName()); } validClasses.add(c); // when dynamic instantiation is turned on, we ignore interfaces if (!(c.isInterface() && dynamicInstantiation)) { validateFields(c, ReflectionUtils.getDeclaredAndInheritedFields(c, true), validClasses, dynamicInstantiation); } } } private void validateFields(Class<?> c, Field[] fields, Set<Class<?>> validClasses, boolean dynamicInstantiation) { boolean foundNameField = false; boolean foundPathField = false; for (Field field : fields) { field.setAccessible(true); if (logger.isLoggable(Level.FINE)) { logger.finer("In [" + c.getName() + "]: Processing field: " + field.getName()); } if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrProperty.class)) { // make sure that the property type is supported if (isList(field)) { if (!ReflectionUtils.isFieldParameterizedWithPropertyType(field)) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is a List annotated as @JcrProperty is not parameterized with a property type."); } } else if (isMap(field)) { // special case, mapping a Map of properties, so we must // make sure that it is properly parameterized: // first parameter must be a String Class<?> keyParamClass = ReflectionUtils.getParameterizedClass(field, 0); if (keyParamClass == null || keyParamClass != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrProperty is a java.util.Map that is not parameterised with a java.lang.String key type."); } // the value class must be a valid property type, or an array // of valid property types Class<?> valueParamClass = ReflectionUtils.getParameterizedClass(field, 1); if (valueParamClass == null || !ReflectionUtils.isValidMapValueType(valueParamClass)) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrProperty is a java.util.Map that is not parameterised with a valid value property type."); } } else if (!ReflectionUtils.isPropertyType(getType(field, null))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrProperty is not a valid JCR property (type is " + field.getType().getName() + ")."); } } else if (field.isAnnotationPresent(JcrProtectedProperty.class)) { if (!ReflectionUtils.isPropertyType(getType(field, null))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrProtectedProperty is not a valid JCR property (type is " + field.getType().getName() + ")."); } } else if (field.isAnnotationPresent(JcrSerializedProperty.class)) { // make sure field is Serializable if (!ReflectionUtils.implementsInterface(getType(field,null), Serializable.class)) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrSerializedProperty does not implement java.io.Serializable (type is " + field.getType().getName() + ")."); } } else if (field.isAnnotationPresent(JcrName.class)) { // make sure this is a String field if (isNotString(field)) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrName must be of type java.lang.String, but is of type: " + field.getType().getName()); } foundNameField = true; } else if (field.isAnnotationPresent(JcrUUID.class)) { // make sure this is a String field if (getType(field, null) != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrUUID must be of type java.lang.String, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrIdentifier.class)) { // make sure this is a String field if (getType(field, null) != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrIdentifier must be of type java.lang.String, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrPath.class)) { // make sure this is a String field if (isNotString(field)) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrPath must be of type java.lang.String, but is of type: " + field.getType().getName()); } foundPathField = true; } else if (field.isAnnotationPresent(JcrBaseVersionName.class)) { // make sure this is a String field if (getType(field, null) != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrBaseVersionName must be of type java.lang.String, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrBaseVersionCreated.class)) { // make sure this is a Date/Calendar/Timestamp field if (!ReflectionUtils.isDateType(getType(field, null))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrBaseVersionCreated must be of type java.util.Date / java.util.Calendar / java.sql.Timestamp, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrVersionName.class)) { // make sure this is a String field if (getType(field, null) != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrVersionName must be of type java.lang.String, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrVersionCreated.class)) { // make sure this is a Date/Calendar/Timestamp field if (!ReflectionUtils.isDateType(getType(field, null))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrVersionCreated must be of type java.util.Date / java.util.Calendar / java.sql.Timestamp, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrCheckedout.class)) { // make sure this i a boolean field if (getType(field, null) != boolean.class && getType(field, null) != Boolean.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrCheckedout must be of type boolean, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrCreated.class)) { // make sure this is a Date/Calendar/Timestamp field if (!ReflectionUtils.isDateType(getType(field, null))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrCreated must be of type java.util.Date / java.util.Calendar / java.sql.Timestamp, but is of type: " + field.getType().getName()); } } else if (field.isAnnotationPresent(JcrParentNode.class)) { // make sure that the parent node type is a valid JCR class validateInternal(getType(field, null), validClasses, dynamicInstantiation); } else if (field.isAnnotationPresent(JcrChildNode.class)) { // make sure that the child node type are valid JCR classes if (isList(field)) { // map a List of child nodes, here we must make sure that // the List is parameterized Class<?> paramClass = ReflectionUtils.getParameterizedClass(field); if (paramClass != null) { validateInternal(ReflectionUtils.getParameterizedClass(field), validClasses, dynamicInstantiation); } else { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrChildNode is a java.util.List that is not parameterised with a valid class type."); } } else if (isMap(field)) { // special case, mapping a Map of child nodes, so we must // make sure that it is properly parameterized: // first parameter must be a String Class<?> keyParamClass = ReflectionUtils.getParameterizedClass(field, 0); if (keyParamClass == null || keyParamClass != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrChildNode is a java.util.Map that is not parameterised with a java.lang.String key type."); } // the value class must be Object, or List of Objects Class<?> valueParamClass = ReflectionUtils.getParameterizedClass(field, 1); Class<?> valueParamParamClass = ReflectionUtils.getTypeArgumentOfParameterizedClass(field, 1, 0); if (valueParamClass == null || (valueParamClass != Object.class && !(ReflectionUtils.implementsInterface(valueParamClass, List.class) && (valueParamParamClass != null && valueParamParamClass == Object.class)))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrChildNode is a java.util.Map that is not parameterised with a valid value type (Object or List<Object>)."); } } else { if (ObjectProperty.class.isAssignableFrom(field.getType())) { validateInternal(ReflectionUtils.getObjectPropertyGeneric(null, field), validClasses, dynamicInstantiation); } else { validateInternal(field.getType(), validClasses, dynamicInstantiation); } } } else if (field.isAnnotationPresent(JcrFileNode.class)) { // make sure that the file node type is a JcrFile if (isList(field)) { if (!ReflectionUtils.extendsClass(ReflectionUtils.getParameterizedClass(field), JcrFile.class)) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is a List annotated as @JcrFileNode is not parameterized with a JcrFile implementation."); } } else if (isMap(field)) { // special case, mapping a Map of file nodes, so we must // make sure that it is properly parameterized: // first parameter must be a String Class<?> keyParamClass = ReflectionUtils.getParameterizedClass(field, 0); if (keyParamClass == null || keyParamClass != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrFileNode is a java.util.Map that is not parameterised with a java.lang.String key type."); } // the value class must be JcrFile extension, or List of JcrFile extensions Class<?> valueParamClass = ReflectionUtils.getParameterizedClass(field, 1); Class<?> valueParamParamClass = ReflectionUtils.getTypeArgumentOfParameterizedClass(field, 1, 0); if (valueParamClass == null || (!ReflectionUtils.extendsClass(valueParamClass, JcrFile.class) && !(ReflectionUtils.implementsInterface(valueParamClass, List.class) && (valueParamParamClass != null && ReflectionUtils.extendsClass(valueParamParamClass, JcrFile.class))))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrFileNode is a java.util.Map that is not parameterised with a valid value type (JcrFile or List<JcrFile>)."); } } else { if (!ReflectionUtils.extendsClass(getType(field, null), JcrFile.class) && !ObjectProperty.class.isAssignableFrom(field.getType())) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrFileNode is of type that does not extend JcrFile: " + field.getType().getName()); } } } else if (field.isAnnotationPresent(JcrReference.class)) { Class<?> fieldType; if (isList(field)) { fieldType = ReflectionUtils.getParameterizedClass(field); } else if (isMap(field)) { // special case, mapping a Map of references, so we must // make sure that it is properly parameterized: // first parameter must be a String Class<?> keyParamClass = ReflectionUtils.getParameterizedClass(field, 0); if (keyParamClass == null || keyParamClass != String.class) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrReference is a java.util.Map that is not parameterised with a java.lang.String key type."); } // the value class must be Object, or List of Objects Class<?> valueParamClass = ReflectionUtils.getParameterizedClass(field, 1); Class<?> valueParamParamClass = ReflectionUtils.getTypeArgumentOfParameterizedClass(field, 1, 0); if (valueParamClass == null || (valueParamClass != Object.class && !(ReflectionUtils.implementsInterface(valueParamClass, List.class) && (valueParamParamClass != null && valueParamParamClass == Object.class)))) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrReference is a java.util.Map that is not parameterised with a valid value type (Object or List<Object>)."); } fieldType = null; } else if (ObjectProperty.class.isAssignableFrom(field.getType())) { fieldType = ReflectionUtils.getObjectPropertyGeneric(null, field); } else { fieldType = field.getType(); } if (fieldType != null) { JcrReference jcrReference = jcrom.getAnnotationReader().getAnnotation(field, JcrReference.class); // when dynamic instantiation is turned on, we ignore interfaces if (!jcrReference.byPath() && !(fieldType.isInterface() && dynamicInstantiation)) { // make sure the class has a @JcrUUID boolean foundUUID = false; boolean foundId = false; for (Field refField : ReflectionUtils.getDeclaredAndInheritedFields(fieldType, true)) { if (refField.isAnnotationPresent(JcrUUID.class)) { foundUUID = true; } if (refField.isAnnotationPresent(JcrIdentifier.class)) { foundId = true; } } if (!foundUUID && !foundId) { throw new JcrMappingException("In [" + c.getName() + "]: Field [" + field.getName() + "] which is annotated as @JcrReference is of type that has no @JcrUUID or @JcrIdentifier: " + field.getType().getName()); } } // validate the class validateInternal(fieldType, validClasses, dynamicInstantiation); } } } if (!foundNameField) { throw new JcrMappingException("In [" + c.getName() + "]: No field is annotated with @JcrName."); } if (!foundPathField) { throw new JcrMappingException("In [" + c.getName() + "]: No field is annotated with @JcrPath."); } } }