/* * Copyright (c) 2010-2015 Evolveum * * 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 com.evolveum.midpoint.repo.sql.query2.definition; import com.evolveum.midpoint.prism.path.IdentifierPathSegment; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.path.ParentPathSegment; import com.evolveum.midpoint.repo.sql.data.Marker; import com.evolveum.midpoint.repo.sql.data.common.ObjectReference; import com.evolveum.midpoint.repo.sql.data.common.RObject; import com.evolveum.midpoint.repo.sql.data.common.embedded.RPolyString; import com.evolveum.midpoint.repo.sql.query.definition.*; import com.evolveum.midpoint.repo.sql.util.ClassMapper; import com.evolveum.midpoint.schema.SchemaConstantsGenerated; import com.evolveum.midpoint.schema.constants.ObjectTypes; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import org.apache.commons.lang.StringUtils; import org.hibernate.annotations.Index; import javax.persistence.Embeddable; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.Enumerated; import javax.persistence.Lob; import javax.persistence.Transient; import javax.xml.namespace.QName; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * @author lazyman * @author mederly */ public class ClassDefinitionParser { private static final Trace LOGGER = TraceManager.getTrace(ClassDefinitionParser.class); public JpaEntityDefinition parseRootClass(Class jpaClass) { return parseClass(jpaClass); } private JpaEntityDefinition parseClass(Class jpaClass) { Class jaxbClass = getJaxbClassForEntity(jpaClass); JpaEntityDefinition entity = new JpaEntityDefinition(jpaClass, jaxbClass); LOGGER.trace("### {}", entity); addVirtualDefinitions(jpaClass, entity); Method[] methods = jpaClass.getMethods(); for (Method method : methods) { String methodName = method.getName(); if (Modifier.isStatic(method.getModifiers()) || "getClass".equals(methodName) || (!methodName.startsWith("is") && !methodName.startsWith("get")) || method.getAnnotation(NotQueryable.class) != null) { //it's not getter for queryable property continue; } if (method.isAnnotationPresent(Transient.class)) { continue; } LOGGER.trace("# {}", method); JpaLinkDefinition linkDefinition; OwnerGetter ownerGetter = method.getAnnotation(OwnerGetter.class); if (ownerGetter != null) { String jpaName = getJpaName(method); JpaDataNodeDefinition nodeDefinition = new JpaEntityPointerDefinition(ownerGetter.ownerClass()); // Owner is considered as not embedded, so we generate left outer join to access it // (instead of implicit inner join that would be used if we would do x.owner.y = '...') linkDefinition = new JpaLinkDefinition(new ParentPathSegment(), jpaName, null, false, nodeDefinition); } else { linkDefinition = parseMethod(method); } entity.addDefinition(linkDefinition); } return entity; } private JpaLinkDefinition parseMethod(Method method) { CollectionSpecification collectionSpecification; // non-null if return type is Set<X>, null if it's X Type returnedContentType; // X in return type, which is either X or Set<X> if (Set.class.isAssignableFrom(method.getReturnType())) { // e.g. Set<RObject> or Set<String> or Set<REmbeddedReference<RFocus>> Type returnType = method.getGenericReturnType(); if (!(returnType instanceof ParameterizedType)) { throw new IllegalStateException("Method " + method + " returns a non-parameterized collection"); } returnedContentType = ((ParameterizedType) returnType).getActualTypeArguments()[0]; collectionSpecification = new CollectionSpecification(); } else { returnedContentType = method.getReturnType(); collectionSpecification = null; } ItemPath itemPath = getJaxbName(method); String jpaName = getJpaName(method); Class jpaClass = getClass(returnedContentType); // sanity check if (Set.class.isAssignableFrom(jpaClass)) { throw new IllegalStateException("Collection within collection is not supported: method=" + method); } JpaLinkDefinition<? extends JpaDataNodeDefinition> linkDefinition; Any any = method.getAnnotation(Any.class); if (any != null) { JpaAnyContainerDefinition targetDefinition = new JpaAnyContainerDefinition(jpaClass); QName jaxbNameForAny = new QName(any.jaxbNameNamespace(), any.jaxbNameLocalPart()); linkDefinition = new JpaLinkDefinition<>(jaxbNameForAny, jpaName, collectionSpecification, false, targetDefinition); } else if (ObjectReference.class.isAssignableFrom(jpaClass)) { boolean embedded = method.isAnnotationPresent(Embedded.class); // computing referenced entity type from returned content type like RObjectReference<RFocus> or REmbeddedReference<RRole> Class referencedJpaClass; if (returnedContentType instanceof ParameterizedType) { referencedJpaClass = getClass(((ParameterizedType) returnedContentType).getActualTypeArguments()[0]); } else { referencedJpaClass = RObject.class; } JpaReferenceDefinition targetDefinition = new JpaReferenceDefinition(jpaClass, referencedJpaClass); linkDefinition = new JpaLinkDefinition<>(itemPath, jpaName, collectionSpecification, embedded, targetDefinition); } else if (isEntity(jpaClass)) { JpaEntityDefinition content = parseClass(jpaClass); boolean embedded = method.isAnnotationPresent(Embedded.class) || jpaClass.isAnnotationPresent(Embeddable.class); linkDefinition = new JpaLinkDefinition<JpaDataNodeDefinition>(itemPath, jpaName, collectionSpecification, embedded, content); } else { boolean lob = method.isAnnotationPresent(Lob.class); boolean enumerated = method.isAnnotationPresent(Enumerated.class); //todo implement also lookup for @Table indexes boolean indexed = method.isAnnotationPresent(Index.class); boolean count = method.isAnnotationPresent(Count.class); Class jaxbClass = getJaxbClass(method, jpaClass); if (method.isAnnotationPresent(IdQueryProperty.class)) { if (collectionSpecification != null) { throw new IllegalStateException("ID property is not allowed to be multivalued; for method " + method); } itemPath = new ItemPath(new IdentifierPathSegment()); } else if (method.isAnnotationPresent(OwnerIdGetter.class)) { if (collectionSpecification != null) { throw new IllegalStateException("Owner ID property is not allowed to be multivalued; for method " + method); } itemPath = new ItemPath(new ParentPathSegment(), new IdentifierPathSegment()); } JpaPropertyDefinition propertyDefinition = new JpaPropertyDefinition(jpaClass, jaxbClass, lob, enumerated, indexed, count); // Note that properties are considered to be embedded linkDefinition = new JpaLinkDefinition<JpaDataNodeDefinition>(itemPath, jpaName, collectionSpecification, true, propertyDefinition); } return linkDefinition; } private Class<?> getClass(Type type) { if (type instanceof Class) { return ((Class) type); } else if (type instanceof ParameterizedType) { return getClass(((ParameterizedType) type).getRawType()); } else { throw new IllegalStateException("Unsupported type: " + type); } } private void addVirtualDefinitions(Class jpaClass, JpaEntityDefinition entityDef) { addVirtualDefinitionsForClass(jpaClass, entityDef); while ((jpaClass = jpaClass.getSuperclass()) != null) { addVirtualDefinitionsForClass(jpaClass, entityDef); } } private void addVirtualDefinitionsForClass(Class jpaClass, JpaEntityDefinition entityDef) { if (!jpaClass.isAnnotationPresent(QueryEntity.class)) { return; } QueryEntity qEntity = (QueryEntity) jpaClass.getAnnotation(QueryEntity.class); for (VirtualAny any : qEntity.anyElements()) { QName jaxbName = new QName(any.jaxbNameNamespace(), any.jaxbNameLocalPart()); VirtualAnyContainerDefinition def = new VirtualAnyContainerDefinition(any.ownerType()); JpaLinkDefinition linkDefinition = new JpaLinkDefinition(jaxbName, null, null, false, def); entityDef.addDefinition(linkDefinition); } for (VirtualCollection collection : qEntity.collections()) { // only collections of entities expected at this moment VirtualCollectionSpecification colSpec = new VirtualCollectionSpecification(collection.additionalParams()); QName jaxbName = createQName(collection.jaxbName()); String jpaName = collection.jpaName(); JpaEntityDefinition content = parseClass(collection.collectionType()); JpaLinkDefinition linkDefinition = new JpaLinkDefinition(jaxbName, jpaName, colSpec, false, content); entityDef.addDefinition(linkDefinition); } for (VirtualEntity entity : qEntity.entities()) { QName jaxbName = createQName(entity.jaxbName()); String jpaName = normalizeJpaName(entity.jpaName()); if (jpaName != null) { throw new IllegalStateException("Only self-pointing virtual entities are supported for now; this one is not: " + jaxbName + " in " + entityDef); } JpaDataNodeDefinition target = new JpaEntityPointerDefinition(entityDef); // pointer to avoid loops JpaLinkDefinition linkDefinition = new JpaLinkDefinition(jaxbName, jpaName, null, false, target); entityDef.addDefinition(linkDefinition); } } private QName createQName(JaxbName name) { return new QName(name.namespace(), name.localPart()); } private String normalizeJpaName(String name) { if (StringUtils.isEmpty(name)) { return null; // "" -> null } else { return name; } } private boolean isEntity(Class type) { if (RPolyString.class.isAssignableFrom(type)) { //it's hibernate entity but from prism point of view it's property return false; } return type.getAnnotation(Entity.class) != null || type.getAnnotation(Embeddable.class) != null; } private ItemPath getJaxbName(Method method) { if (method.isAnnotationPresent(JaxbName.class)) { JaxbName jaxbName = method.getAnnotation(JaxbName.class); return new ItemPath(new QName(jaxbName.namespace(), jaxbName.localPart())); } else if (method.isAnnotationPresent(JaxbPath.class)) { JaxbPath jaxbPath = method.getAnnotation(JaxbPath.class); List<QName> names = new ArrayList<>(jaxbPath.itemPath().length); for (JaxbName jaxbName : jaxbPath.itemPath()) { names.add(new QName(jaxbName.namespace(), jaxbName.localPart())); } return new ItemPath(names.toArray(new QName[0])); } else { String propertyName = getPropertyName(method.getName()); if (method.isAnnotationPresent(Count.class)) { propertyName = StringUtils.removeEnd(propertyName, "Count"); } return new ItemPath(new QName(SchemaConstantsGenerated.NS_COMMON, propertyName)); } } // second parameter is just to optimize private Class getJaxbClass(Method method, Class returnedClass) { JaxbType annotation = (JaxbType) method.getAnnotation(JaxbType.class); if (annotation != null) { return annotation.type(); } Class classFromEntity = getJaxbClassForEntity(returnedClass); if (classFromEntity != null) { return classFromEntity; } Package returnedClassPkg = returnedClass.getPackage(); Package dataObjectsPkg = Marker.class.getPackage(); if (returnedClassPkg != null && returnedClassPkg.getName().startsWith(dataObjectsPkg.getName())) { return null; } else { return returnedClass; // probably the JAXB value } } private Class getJaxbClassForEntity(Class clazz) { if (RObject.class.isAssignableFrom(clazz)) { ObjectTypes objectType = ClassMapper.getObjectTypeForHQLType(clazz); return objectType.getClassDefinition(); } JaxbType annotation = (JaxbType) clazz.getAnnotation(JaxbType.class); if (annotation != null) { return annotation.type(); } return null; } private String getJpaName(Method method) { String methodName = method.getName(); return getPropertyName(methodName); } private String getPropertyName(String methodName) { int startIndex = 3; //method name starts with "get" if (methodName.startsWith("is")) { startIndex = 2; } char first = Character.toLowerCase(methodName.charAt(startIndex)); return Character.toString(first) + StringUtils.substring(methodName, startIndex + 1, methodName.length()); } }