Java tutorial
/** * Copyright 2005-2014 The Kuali Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php * * 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.kuali.rice.krad.data.jpa.eclipselink; import org.apache.commons.lang.StringUtils; import org.eclipse.persistence.descriptors.ClassDescriptor; import org.eclipse.persistence.expressions.Expression; import org.eclipse.persistence.internal.expressions.FunctionExpression; import org.eclipse.persistence.internal.helper.DatabaseField; import org.eclipse.persistence.internal.jpa.metamodel.EmbeddableTypeImpl; import org.eclipse.persistence.internal.jpa.metamodel.EntityTypeImpl; import org.eclipse.persistence.internal.jpa.metamodel.ManagedTypeImpl; import org.eclipse.persistence.internal.jpa.metamodel.PluralAttributeImpl; import org.eclipse.persistence.internal.jpa.metamodel.SingularAttributeImpl; import org.eclipse.persistence.jpa.JpaEntityManager; import org.eclipse.persistence.mappings.AggregateObjectMapping; import org.eclipse.persistence.mappings.CollectionMapping; import org.eclipse.persistence.mappings.DatabaseMapping; import org.eclipse.persistence.mappings.DirectToFieldMapping; import org.eclipse.persistence.mappings.ForeignReferenceMapping; import org.eclipse.persistence.mappings.ManyToOneMapping; import org.eclipse.persistence.mappings.OneToManyMapping; import org.eclipse.persistence.mappings.OneToOneMapping; import org.eclipse.persistence.mappings.converters.Converter; import org.eclipse.persistence.mappings.converters.ConverterClass; import org.eclipse.persistence.queries.ObjectLevelReadQuery; import org.kuali.rice.krad.data.jpa.JpaMetadataProviderImpl; import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship; import org.kuali.rice.krad.data.metadata.DataObjectCollectionSortAttribute; import org.kuali.rice.krad.data.metadata.DataObjectMetadata; import org.kuali.rice.krad.data.metadata.DataObjectRelationship; import org.kuali.rice.krad.data.metadata.MetadataConfigurationException; import org.kuali.rice.krad.data.metadata.SortDirection; import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeImpl; import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeRelationshipImpl; import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionImpl; import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionSortAttributeImpl; import org.kuali.rice.krad.data.metadata.impl.DataObjectMetadataImpl; import org.kuali.rice.krad.data.metadata.impl.DataObjectRelationshipImpl; import org.kuali.rice.krad.data.metadata.impl.MetadataChildBase; import javax.persistence.metamodel.Attribute.PersistentAttributeType; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.SingularAttribute; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; /** * Provides an EclipseLink-specific implementation for the {@link JpaMetadataProviderImpl}. */ public class EclipseLinkJpaMetadataProviderImpl extends JpaMetadataProviderImpl { private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger .getLogger(EclipseLinkJpaMetadataProviderImpl.class); /** * {@inheritDoc} */ @Override protected void populateImplementationSpecificEntityLevelMetadata(DataObjectMetadataImpl metadata, EntityType<?> entityType) { if (entityType instanceof EntityTypeImpl) { metadata.setBackingObjectName(((EntityTypeImpl<?>) entityType).getDescriptor().getTableName()); } } /** * {@inheritDoc} */ @Override protected void populateImplementationSpecificAttributeLevelMetadata(DataObjectAttributeImpl attribute, SingularAttribute<?, ?> attr) { if (attr instanceof SingularAttributeImpl) { DatabaseMapping mapping = ((SingularAttributeImpl<?, ?>) attr).getMapping(); if (mapping != null && mapping.getField() != null) { attribute.setReadOnly(mapping.isReadOnly()); attribute.setBackingObjectName(mapping.getField().getName()); if (mapping.getField().getLength() != 0) { attribute.setMaxLength((long) mapping.getField().getLength()); } // Special check on the converters to attempt to default secure attributes from being shown on the UI // We check for a converter which has "encrypt" in its name and auto-set the attribute security // to mask the attribute. if (mapping instanceof DirectToFieldMapping) { Converter converter = ((DirectToFieldMapping) mapping).getConverter(); // ConverterClass is the internal wrapper EclipseLink uses to wrap the JPA AttributeConverter // classes // and make them conform to the EclipseLink internal API if (converter != null && converter instanceof ConverterClass) { // Unfortunately, there is no access to the actual converter class, so we have to hack it try { Field f = ConverterClass.class.getDeclaredField("attributeConverterClassName"); f.setAccessible(true); String attributeConverterClassName = (String) f.get(converter); if (StringUtils.containsIgnoreCase(attributeConverterClassName, "encrypt")) { attribute.setSensitive(true); } } catch (Exception e) { LOG.warn("Unable to access the converter name for attribute: " + attribute.getOwningType().getName() + "." + attribute.getName() + " Skipping attempt to detect converter."); } } } } } } /** * {@inheritDoc} */ @Override protected void populateImplementationSpecificCollectionLevelMetadata(DataObjectCollectionImpl collection, PluralAttribute<?, ?, ?> cd) { // OJB stores the related class object name. We need to go into the repository and grab the table name. Class<?> collectionElementClass = cd.getElementType().getJavaType(); EntityType<?> elementEntityType = entityManager.getMetamodel().entity(collectionElementClass); // get table name behind element if (elementEntityType instanceof EntityTypeImpl) { collection.setBackingObjectName(((EntityTypeImpl<?>) elementEntityType).getDescriptor().getTableName()); } // Set to read only if store (save) operations should not be pushed through PersistentAttributeType persistentAttributeType = cd.getPersistentAttributeType(); if (cd instanceof PluralAttributeImpl) { PluralAttributeImpl<?, ?, ?> coll = (PluralAttributeImpl<?, ?, ?>) cd; CollectionMapping collectionMapping = coll.getCollectionMapping(); if (collectionMapping instanceof OneToManyMapping) { OneToManyMapping otm = (OneToManyMapping) collectionMapping; populateInverseRelationship(otm, collection); Map<DatabaseField, DatabaseField> keyMap = otm.getSourceKeysToTargetForeignKeys(); List<DataObjectAttributeRelationship> attributeRelationships = new ArrayList<DataObjectAttributeRelationship>(); for (Map.Entry<DatabaseField, DatabaseField> keyRel : keyMap.entrySet()) { attributeRelationships.add(new DataObjectAttributeRelationshipImpl( getPropertyNameFromDatabaseColumnName(cd.getDeclaringType(), keyRel.getKey().getName()), getPropertyNameFromDatabaseColumnName(elementEntityType, keyRel.getValue().getName()))); } collection.setAttributeRelationships(attributeRelationships); } collection.setReadOnly(collectionMapping.isReadOnly()); collection.setSavedWithParent(collectionMapping.isCascadePersist()); collection.setDeletedWithParent(collectionMapping.isCascadeRemove()); collection .setLoadedAtParentLoadTime(collectionMapping.isCascadeRefresh() && !collectionMapping.isLazy()); collection.setLoadedDynamicallyUponUse( collectionMapping.isCascadeRefresh() && collectionMapping.isLazy()); } else { // get what we can based on JPA values (note that we just set some to have values here) collection.setReadOnly(false); collection.setSavedWithParent(persistentAttributeType == PersistentAttributeType.ONE_TO_MANY); collection.setDeletedWithParent(persistentAttributeType == PersistentAttributeType.ONE_TO_MANY); collection.setLoadedAtParentLoadTime(true); collection.setLoadedDynamicallyUponUse(false); } // We need to detect the case of a intermediate mapping table. These tables are not directly mapped // in OJB, but are referenced by their table and column names. // The attributes referenced are assumed to be in the order of the PK fields of the parent and child objects // as there is no way to identify the attributes/columns on the linked classes. // Extract the default sort order for the collection List<DataObjectCollectionSortAttribute> sortAttributes = new ArrayList<DataObjectCollectionSortAttribute>(); if (cd instanceof PluralAttributeImpl) { PluralAttributeImpl<?, ?, ?> coll = (PluralAttributeImpl<?, ?, ?>) cd; CollectionMapping collectionMapping = coll.getCollectionMapping(); if (collectionMapping.getSelectionQuery() instanceof ObjectLevelReadQuery) { ObjectLevelReadQuery readQuery = (ObjectLevelReadQuery) collectionMapping.getSelectionQuery(); List<Expression> orderByExpressions = readQuery.getOrderByExpressions(); for (Expression expression : orderByExpressions) { if (expression instanceof FunctionExpression) { String attributeName = ((FunctionExpression) expression).getBaseExpression().getName(); SortDirection direction = SortDirection.ASCENDING; if (expression.getOperator().isOrderOperator()) { if (StringUtils.containsIgnoreCase(expression.getOperator().getDatabaseStrings()[0], "DESC")) { direction = SortDirection.DESCENDING; } } sortAttributes.add(new DataObjectCollectionSortAttributeImpl(attributeName, direction)); } } } } collection.setDefaultCollectionOrderingAttributeNames(sortAttributes); } /** * Returns the property name on the given entity type which the given database column is mapped to. * * <p> * If no field on the given type is mapped to this field (which is common in cases of a JPA relationship without an * actual {@link javax.persistence.Column} annotated field to represent the foreign key) then this method will * return null. * </p> * * @param entityType the entity type on which to search for a property that is mapped to the given column * @param databaseColumnName the name of the database column * * @return the name of the property on the given entity type which maps to the given column, or null if no such * mapping exists */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected String getPropertyNameFromDatabaseColumnName(ManagedType entityType, String databaseColumnName) { for (SingularAttributeImpl attr : (Set<SingularAttributeImpl>) entityType.getSingularAttributes()) { if (!attr.isAssociation()) { if (!(attr.getClass().isAssignableFrom(EmbeddableTypeImpl.class)) && !(attr.getMapping().getClass().isAssignableFrom(AggregateObjectMapping.class)) && attr.getMapping().getField().getName().equals(databaseColumnName)) { return attr.getName(); } } } return null; } /** * {@inheritDoc} */ @Override protected void populateImplementationSpecificRelationshipLevelMetadata(DataObjectRelationshipImpl relationship, SingularAttribute<?, ?> rd) { // We need to go into the repository and grab the table name. Class<?> referencedClass = rd.getBindableJavaType(); EntityType<?> referencedEntityType = entityManager.getMetamodel().entity(referencedClass); if (referencedEntityType instanceof EntityTypeImpl) { relationship.setBackingObjectName( ((EntityTypeImpl<?>) referencedEntityType).getDescriptor().getTableName()); } // Set to read only if store (save) operations should not be pushed through PersistentAttributeType persistentAttributeType = rd.getPersistentAttributeType(); if (rd instanceof SingularAttributeImpl) { SingularAttributeImpl<?, ?> rel = (SingularAttributeImpl<?, ?>) rd; OneToOneMapping relationshipMapping = (OneToOneMapping) rel.getMapping(); relationship.setReadOnly(relationshipMapping.isReadOnly()); relationship.setSavedWithParent(relationshipMapping.isCascadePersist()); relationship.setDeletedWithParent(relationshipMapping.isCascadeRemove()); relationship.setLoadedAtParentLoadTime( relationshipMapping.isCascadeRefresh() && !relationshipMapping.isLazy()); relationship.setLoadedDynamicallyUponUse( relationshipMapping.isCascadeRefresh() && relationshipMapping.isLazy()); List<DataObjectAttributeRelationship> attributeRelationships = new ArrayList<DataObjectAttributeRelationship>(); for (DatabaseField parentField : relationshipMapping.getForeignKeyFields()) { String parentFieldName = getPropertyNameFromDatabaseColumnName(rd.getDeclaringType(), parentField.getName()); if (parentFieldName != null) { DatabaseField childField = relationshipMapping.getSourceToTargetKeyFields().get(parentField); if (childField != null) { // the target field is always done by column name. So, we need to get into the target entity and // find the associated field :-( // If the lookup fails, we will at least have the column name String childFieldName = getPropertyNameFromDatabaseColumnName(referencedEntityType, childField.getName()); if (childFieldName != null) { attributeRelationships .add(new DataObjectAttributeRelationshipImpl(parentFieldName, childFieldName)); } } else { LOG.warn("Unable to find child field reference. There may be a JPA mapping problem on " + rd.getDeclaringType().getJavaType() + ": " + relationship); } } } relationship.setAttributeRelationships(attributeRelationships); populateInverseRelationship(relationshipMapping, relationship); } else { // get what we can based on JPA values (note that we just set some to have values here) relationship.setReadOnly(persistentAttributeType == PersistentAttributeType.MANY_TO_ONE); relationship.setSavedWithParent(persistentAttributeType == PersistentAttributeType.ONE_TO_ONE); relationship.setDeletedWithParent(persistentAttributeType == PersistentAttributeType.ONE_TO_ONE); relationship.setLoadedAtParentLoadTime(true); relationship.setLoadedDynamicallyUponUse(false); } } /** * Populates the inverse relationship for a given relationship. * * @param mapping the {@link DatabaseMapping} that defines the relationship. * @param relationship the relationship of which to populate the other side. */ protected void populateInverseRelationship(DatabaseMapping mapping, MetadataChildBase relationship) { DatabaseMapping relationshipPartner = findRelationshipPartner(mapping); if (relationshipPartner != null) { Class<?> partnerType = relationshipPartner.getDescriptor().getJavaClass(); DataObjectMetadata partnerMetadata = masterMetadataMap.get(partnerType); // if the target metadata is not null, it means that entity has already been processed, // so we can go ahead and establish the inverse relationship if (partnerMetadata != null) { // first check if it's a relationship MetadataChildBase relationshipPartnerMetadata = (MetadataChildBase) partnerMetadata .getRelationship(relationshipPartner.getAttributeName()); if (relationshipPartnerMetadata == null) { relationshipPartnerMetadata = (MetadataChildBase) partnerMetadata .getCollection(relationshipPartner.getAttributeName()); } if (relationshipPartnerMetadata != null) { relationshipPartnerMetadata.setInverseRelationship(relationship); relationship.setInverseRelationship(relationshipPartnerMetadata); } } } } /** * Gets the inverse mapping of the given {@link DatabaseMapping}. * * @param databaseMapping the {@link DatabaseMapping} of which to get the inverse. * @return the inverse mapping of the given {@link DatabaseMapping}. */ protected DatabaseMapping findRelationshipPartner(DatabaseMapping databaseMapping) { if (databaseMapping instanceof OneToManyMapping) { OneToManyMapping mapping = (OneToManyMapping) databaseMapping; if (mapping.getMappedBy() != null) { Class<?> referenceClass = mapping.getReferenceClass(); ClassDescriptor referenceClassDescriptor = getClassDescriptor(referenceClass); return referenceClassDescriptor.getMappingForAttributeName(mapping.getMappedBy()); } } else if (databaseMapping instanceof ManyToOneMapping) { // one odd thing just to note here, for ManyToOne mappings with an inverse OneToMany, for some reason the // getMappedBy method still returns the mappedBy from the OneToMany side, so we can't use nullness of // mappedBy to infer which side of the relationship we are on, oddly enough, that's not the way it works // for OneToOne mappings (see below)...go figure // // I have to assume this is some sort of bug in EclipseLink metadata ManyToOneMapping mapping = (ManyToOneMapping) databaseMapping; Class<?> referenceClass = mapping.getReferenceClass(); ClassDescriptor referenceClassDescriptor = getClassDescriptor(referenceClass); // find the OneToMany mapping which points back to this ManyToOne for (DatabaseMapping referenceMapping : referenceClassDescriptor.getMappings()) { if (referenceMapping instanceof OneToManyMapping) { OneToManyMapping oneToManyMapping = (OneToManyMapping) referenceMapping; if (mapping.getAttributeName().equals(oneToManyMapping.getMappedBy())) { return oneToManyMapping; } } } } else if (databaseMapping instanceof OneToOneMapping) { OneToOneMapping mapping = (OneToOneMapping) databaseMapping; // well for reasons I can't quite fathom, mappedBy is always null on OneToOne relationships, // thankfully it's OneToOne so it's pretty easy to figure out the inverse ClassDescriptor referenceClassDescriptor = getClassDescriptor(mapping.getReferenceClass()); // let's check if theres a OneToOne pointing back to us for (DatabaseMapping referenceMapping : referenceClassDescriptor.getMappings()) { if (referenceMapping instanceof OneToOneMapping) { OneToOneMapping oneToOneMapping = (OneToOneMapping) referenceMapping; if (oneToOneMapping.getReferenceClass().equals(mapping.getDescriptor().getJavaClass())) { return oneToOneMapping; } } } } // TODO need to implement for bi-directional OneToOne and ManyToMany return null; } /** * {@inheritDoc} */ @Override public DataObjectRelationship addExtensionRelationship(Class<?> entityClass, String extensionPropertyName, Class<?> extensionEntityClass) { ClassDescriptor entityDescriptor = getClassDescriptor(entityClass); ClassDescriptor extensionEntityDescriptor = getClassDescriptor(extensionEntityClass); if (LOG.isDebugEnabled()) { LOG.debug("About to attempt to inject a 1:1 relationship on PKs between " + entityDescriptor + " and " + extensionEntityDescriptor); } OneToOneMapping dm = (OneToOneMapping) entityDescriptor.newOneToOneMapping(); dm.setAttributeName(extensionPropertyName); dm.setReferenceClass(extensionEntityClass); dm.setDescriptor(entityDescriptor); dm.setIsPrivateOwned(true); dm.setJoinFetch(ForeignReferenceMapping.OUTER_JOIN); dm.setCascadeAll(true); dm.setIsLazy(false); dm.dontUseIndirection(); dm.setIsOneToOneRelationship(true); dm.setRequiresTransientWeavedFields(false); OneToOneMapping inverse = findExtensionInverse(extensionEntityDescriptor, entityClass); dm.setMappedBy(inverse.getAttributeName()); for (DatabaseField sourceField : inverse.getSourceToTargetKeyFields().keySet()) { DatabaseField targetField = inverse.getSourceToTargetKeyFields().get(sourceField); // reverse them, pass the source from the inverse as our target and the target from the inverse as our source dm.addTargetForeignKeyField(sourceField, targetField); } dm.preInitialize(getEclipseLinkEntityManager().getDatabaseSession()); dm.initialize(getEclipseLinkEntityManager().getDatabaseSession()); entityDescriptor.addMapping(dm); entityDescriptor.getObjectBuilder().initialize(getEclipseLinkEntityManager().getDatabaseSession()); // build the data object relationship ManagedTypeImpl<?> managedType = (ManagedTypeImpl<?>) getEntityManager().getMetamodel() .managedType(entityClass); SingularAttributeImpl<?, ?> singularAttribute = new SingularAttributeLocal(managedType, dm); return getRelationshipMetadata(singularAttribute); } /** * Provides a local implementation of {@link SingularAttributeImpl}. */ class SingularAttributeLocal extends SingularAttributeImpl { /** * Creates a local implementation of {@link SingularAttributeImpl}. * * @param managedType the {@link ManagedType}. * @param mapping the {@link DatabaseMapping}. */ SingularAttributeLocal(ManagedTypeImpl managedType, DatabaseMapping mapping) { super(managedType, mapping); } } /** * Gets the inverse extension of the given {@link ClassDescriptor}. * * @param extensionEntityDescriptor the {@link ClassDescriptor} of which to get the inverse. * @param entityType the type of the entity. * @return the inverse extension of the given {@link ClassDescriptor}. */ protected OneToOneMapping findExtensionInverse(ClassDescriptor extensionEntityDescriptor, Class<?> entityType) { Collection<DatabaseMapping> derivedIdMappings = extensionEntityDescriptor.getDerivesIdMappinps(); String extensionInfo = "(" + extensionEntityDescriptor.getJavaClass().getName() + " -> " + entityType.getName() + ")"; if (derivedIdMappings == null || derivedIdMappings.isEmpty()) { throw new MetadataConfigurationException("Attempting to use extension framework, but extension " + extensionInfo + " does not have a valid inverse OneToOne Id mapping back to the extended data " + "object. Please ensure it is annotated property for use of the extension framework with JPA."); } else if (derivedIdMappings.size() > 1) { throw new MetadataConfigurationException( "When attempting to determine the inverse relationship for use " + "with extension framework " + extensionInfo + " encountered more than one 'derived id' mapping, " + "there should be only one!"); } DatabaseMapping inverseMapping = derivedIdMappings.iterator().next(); if (!(inverseMapping instanceof OneToOneMapping)) { throw new MetadataConfigurationException("Identified an inverse derived id mapping for extension " + "relationship " + extensionInfo + " but it was not a one-to-one mapping: " + inverseMapping); } return (OneToOneMapping) inverseMapping; } /** * Gets the descriptor for the entity type. * * @param entityClass the type of the enty. * @return the descriptor for the entity type. */ protected ClassDescriptor getClassDescriptor(Class<?> entityClass) { return getEclipseLinkEntityManager().getDatabaseSession().getDescriptor(entityClass); } /** * The entity manager for interacting with the database. * @return the entity manager for interacting with the database. */ protected JpaEntityManager getEclipseLinkEntityManager() { return (JpaEntityManager) entityManager; } }