/*****************************************************************
* 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.cayenne.jpa.conf;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Date;
import javax.persistence.EnumType;
import javax.persistence.TemporalType;
import org.apache.cayenne.jpa.JpaProviderException;
import org.apache.cayenne.jpa.map.AccessType;
import org.apache.cayenne.jpa.map.JpaAbstractEntity;
import org.apache.cayenne.jpa.map.JpaAttribute;
import org.apache.cayenne.jpa.map.JpaAttributes;
import org.apache.cayenne.jpa.map.JpaBasic;
import org.apache.cayenne.jpa.map.JpaClassDescriptor;
import org.apache.cayenne.jpa.map.JpaColumn;
import org.apache.cayenne.jpa.map.JpaEntity;
import org.apache.cayenne.jpa.map.JpaEntityMap;
import org.apache.cayenne.jpa.map.JpaId;
import org.apache.cayenne.jpa.map.JpaJoinColumn;
import org.apache.cayenne.jpa.map.JpaManagedClass;
import org.apache.cayenne.jpa.map.JpaManyToMany;
import org.apache.cayenne.jpa.map.JpaManyToOne;
import org.apache.cayenne.jpa.map.JpaMappedSuperclass;
import org.apache.cayenne.jpa.map.JpaOneToMany;
import org.apache.cayenne.jpa.map.JpaOneToOne;
import org.apache.cayenne.jpa.map.JpaPropertyDescriptor;
import org.apache.cayenne.jpa.map.JpaRelationship;
import org.apache.cayenne.jpa.map.JpaTable;
import org.apache.cayenne.jpa.map.JpaVersion;
import org.apache.cayenne.project.ProjectPath;
import org.apache.cayenne.util.BaseTreeVisitor;
import org.apache.cayenne.util.HierarchicalTreeVisitor;
import org.apache.cayenne.util.TraversalUtil;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.validation.SimpleValidationFailure;
/**
* Initializes JPA specification compatible mapping defaults.
*
* @author Andrus Adamchik
*/
public class EntityMapDefaultsProcessor {
protected HierarchicalTreeVisitor visitor;
protected transient EntityMapLoaderContext context;
public void applyDefaults(EntityMapLoaderContext context) throws JpaProviderException {
this.context = context;
if (visitor == null) {
visitor = createVisitor();
}
TraversalUtil.traverse(context.getEntityMap(), visitor);
}
/**
* Creates a stateless instance of the JpaEntityMap traversal visitor. This method is
* lazily invoked and cached by this object.
*/
protected HierarchicalTreeVisitor createVisitor() {
return new EntityMapVisitor();
}
abstract class AbstractEntityVisitor extends BaseTreeVisitor {
AbstractEntityVisitor() {
BaseTreeVisitor attributeVisitor = new BaseTreeVisitor();
attributeVisitor.addChildVisitor(JpaId.class, new IdVisitor());
attributeVisitor.addChildVisitor(JpaBasic.class, new BasicVisitor());
attributeVisitor.addChildVisitor(JpaVersion.class, new VersionVisitor());
attributeVisitor.addChildVisitor(JpaManyToOne.class, new ManyToOneVisitor());
attributeVisitor.addChildVisitor(JpaOneToOne.class, new OneToOneVisitor());
attributeVisitor.addChildVisitor(
JpaOneToMany.class,
new RelationshipVisitor());
attributeVisitor.addChildVisitor(
JpaManyToMany.class,
new RelationshipVisitor());
addChildVisitor(JpaAttributes.class, attributeVisitor);
addChildVisitor(JpaId.class, new IdVisitor());
}
@Override
public boolean onStartNode(ProjectPath path) {
JpaAbstractEntity abstractEntity = (JpaAbstractEntity) path.getObject();
// * entity name
if (abstractEntity.getClassName() == null) {
return false;
}
if (abstractEntity instanceof JpaEntity) {
JpaEntity entity = (JpaEntity) abstractEntity;
if (entity.getName() == null) {
// use unqualified class name
String fqName = abstractEntity.getClassName();
int split = fqName.lastIndexOf('.');
entity.setName(split > 0 ? fqName.substring(split + 1) : fqName);
}
// * default table (see @Table annotation defaults, JPA spec 9.1.1)
if (entity.getTable() == null) {
JpaTable table = new JpaTable(AnnotationPrototypes.getTable());
// unclear whether we need to apply any other name transformations ...
// or even if we need to uppercase the name. Per default examples
// looks
// like we need. table.setName(entity.getName().toUpperCase());
table.setName(entity.getName());
entity.setTable(table);
}
}
if (abstractEntity.getAttributes() == null) {
abstractEntity.setAttributes(new JpaAttributes());
}
// * default persistent fields
JpaClassDescriptor descriptor = abstractEntity.getClassDescriptor();
AccessType access = abstractEntity.getAccess();
if (access == null) {
access = ((JpaEntityMap) path.getRoot()).getAccess();
abstractEntity.setAccess(access);
}
if (access == AccessType.PROPERTY) {
for (JpaPropertyDescriptor candidate : descriptor
.getPropertyDescriptors()) {
processProperty(abstractEntity, descriptor, candidate);
}
}
// field is default...
else {
for (JpaPropertyDescriptor candidate : descriptor.getFieldDescriptors()) {
processProperty(abstractEntity, descriptor, candidate);
}
}
return true;
}
void processProperty(
JpaAbstractEntity entity,
JpaClassDescriptor descriptor,
JpaPropertyDescriptor property) {
JpaAttributes attributes = entity.getAttributes();
if (attributes.getAttribute(property.getName()) != null) {
return;
}
if (property.isDefaultNonRelationalType()) {
JpaBasic attribute = new JpaBasic();
attribute.setPropertyDescriptor(property);
attribute.setName(property.getName());
attributes.getBasicAttributes().add(attribute);
}
else {
String path = descriptor.getManagedClass().getName()
+ "."
+ property.getName();
context.recordConflict(new SimpleValidationFailure(
property.getMember(),
"Undefined property persistence status: " + path));
}
}
}
class BasicVisitor extends BaseTreeVisitor {
BasicVisitor() {
addChildVisitor(JpaColumn.class, new ColumnVisitor());
}
@Override
public boolean onStartNode(ProjectPath path) {
JpaBasic jpaBasic = (JpaBasic) path.getObject();
if (jpaBasic.getColumn() == null) {
JpaColumn column = new JpaColumn(AnnotationPrototypes.getColumn());
column.setName(jpaBasic.getName());
column.setNullable(jpaBasic.isOptional());
jpaBasic.setColumn(column);
}
JpaAbstractEntity entity = (JpaAbstractEntity) path
.firstInstanceOf(JpaAbstractEntity.class);
// process temporal type defaults
if (jpaBasic.getTemporal() == null && jpaBasic.getEnumerated() == null) {
JpaClassDescriptor descriptor = entity.getClassDescriptor();
JpaPropertyDescriptor property = descriptor.getProperty(jpaBasic
.getName());
// sanity check
if (property == null) {
throw new IllegalStateException("No class property found for name: "
+ jpaBasic.getName());
}
if (java.sql.Date.class.isAssignableFrom(property.getType())) {
jpaBasic.setTemporal(TemporalType.DATE);
}
else if (Time.class.isAssignableFrom(property.getType())) {
jpaBasic.setTemporal(TemporalType.TIME);
}
else if (Timestamp.class.isAssignableFrom(property.getType())) {
jpaBasic.setTemporal(TemporalType.TIMESTAMP);
}
else if (Date.class.isAssignableFrom(property.getType())) {
jpaBasic.setTemporal(TemporalType.TIMESTAMP);
}
else if (property.getType().isEnum()) {
jpaBasic.setEnumerated(EnumType.ORDINAL);
}
}
return true;
}
}
class VersionVisitor extends BasicVisitor {
@Override
public boolean onStartNode(ProjectPath path) {
JpaVersion jpaBasic = (JpaVersion) path.getObject();
if (jpaBasic.getColumn() == null) {
JpaColumn column = new JpaColumn(AnnotationPrototypes.getColumn());
column.setName(jpaBasic.getName());
jpaBasic.setColumn(column);
}
if (jpaBasic.getTemporal() == null) {
JpaEntity entity = (JpaEntity) path.firstInstanceOf(JpaEntity.class);
JpaClassDescriptor descriptor = entity.getClassDescriptor();
JpaPropertyDescriptor property = descriptor.getProperty(jpaBasic
.getName());
if (Date.class.equals(property.getType())) {
jpaBasic.setTemporal(TemporalType.TIMESTAMP);
}
}
return true;
}
}
final class ColumnVisitor extends BaseTreeVisitor {
@Override
public boolean onStartNode(ProjectPath path) {
JpaColumn column = (JpaColumn) path.getObject();
JpaAttribute parent = (JpaAttribute) path.firstInstanceOf(JpaAttribute.class);
if (column.getName() == null) {
column.setName(parent.getName());
}
if (column.getTable() == null) {
JpaEntity entity = (JpaEntity) path.firstInstanceOf(JpaEntity.class);
// parent can be a mapped superclass
if (entity != null) {
column.setTable(entity.getTable().getName());
}
}
if (parent.getPropertyDescriptor().isStringType()) {
if (column.getLength() <= 0) {
column.setLength(JpaColumn.DEFAULT_LENGTH);
}
}
else {
// length for non-string types should be ignored...
column.setLength(-1);
}
return true;
}
}
final class EntityMapVisitor extends BaseTreeVisitor {
EntityMapVisitor() {
addChildVisitor(JpaEntity.class, new EntityVisitor());
addChildVisitor(JpaMappedSuperclass.class, new MappedSuperclassVisitor());
}
@Override
public boolean onStartNode(ProjectPath path) {
JpaEntityMap entityMap = (JpaEntityMap) path.getObject();
// TODO: andrus, 4/28/2006 - actually we need to analyze preloaded classes and
// see how they were annotated to choose the right access type...
entityMap.setAccess(AccessType.FIELD);
return true;
}
}
final class EntityVisitor extends AbstractEntityVisitor {
}
final class IdVisitor extends BaseTreeVisitor {
IdVisitor() {
addChildVisitor(JpaColumn.class, new ColumnVisitor());
}
@Override
public boolean onStartNode(ProjectPath path) {
JpaId id = (JpaId) path.getObject();
if (id.getColumn() == null) {
JpaColumn column = new JpaColumn(AnnotationPrototypes.getColumn());
column.setName(id.getName());
JpaEntity entity = (JpaEntity) path.firstInstanceOf(JpaEntity.class);
column.setTable(entity.getTable().getName());
id.setColumn(column);
}
return true;
}
}
final class JoinColumnVisitor extends BaseTreeVisitor {
@Override
public boolean onStartNode(ProjectPath path) {
JpaRelationship relationship = (JpaRelationship) path.getObjectParent();
JpaJoinColumn column = (JpaJoinColumn) path.getObject();
if (column.getTable() == null) {
JpaEntity entity = (JpaEntity) path.firstInstanceOf(JpaEntity.class);
column.setTable(entity.getTable().getName());
}
// JPA Spec, 2.1.8.2 (same for all relationship owners):
// The following mapping defaults apply: [...]
// Table A contains a foreign key to table B. The foreign key column
// name is formed as the concatenation of the following: the name of
// the relationship property or field of entityA; "_" ; the name of
// the primary key column in table B. The foreign key column has the
// same type as the primary key of table B.
JpaEntityMap map = (JpaEntityMap) path.firstInstanceOf(JpaEntityMap.class);
JpaEntity target = map.entityForClass(relationship.getTargetEntityName());
if (target == null) {
context.recordConflict(new SimpleValidationFailure(
relationship,
"Invalid relationship target "
+ relationship.getTargetEntityName()));
}
else if (target.getAttributes() == null
|| target.getAttributes().getIds().isEmpty()) {
context.recordConflict(new SimpleValidationFailure(
target,
"Relationship target has no PK defined: "
+ relationship.getTargetEntityName()));
}
else if (target.getAttributes().getIds().size() > 1) {
// TODO: andrus, 4/30/2006 implement this; note that instead of
// checking for "attribute.getJoinColumns().isEmpty()" above,
// we'll have to match individual columns
context.recordConflict(new SimpleValidationFailure(
relationship,
"Defaults for compound FK are not implemented."));
}
else {
JpaId id = target.getAttributes().getIds().iterator().next();
String pkName = id.getColumn() != null ? id.getColumn().getName() : id
.getName();
column.setName(relationship.getName() + '_' + pkName);
column.setReferencedColumnName(id.getColumn() != null ? id
.getColumn()
.getName() : id.getName());
}
return true;
}
}
final class MappedSuperclassVisitor extends AbstractEntityVisitor {
}
final class ManyToOneVisitor extends RelationshipVisitor {
ManyToOneVisitor() {
addChildVisitor(JpaJoinColumn.class, new JoinColumnVisitor());
}
@Override
public boolean onStartNode(ProjectPath path) {
if (!super.onStartNode(path)) {
return false;
}
JpaManyToOne manyToOne = (JpaManyToOne) path.getObject();
Collection<JpaJoinColumn> joinColumns = manyToOne.getJoinColumns();
if (joinColumns.isEmpty()) {
joinColumns.add(new JpaJoinColumn(AnnotationPrototypes.getJoinColumn()));
}
return true;
}
}
final class OneToOneVisitor extends RelationshipVisitor {
OneToOneVisitor() {
addChildVisitor(JpaJoinColumn.class, new JoinColumnVisitor());
}
@Override
public boolean onStartNode(ProjectPath path) {
if (!super.onStartNode(path)) {
return false;
}
JpaOneToOne oneToOne = (JpaOneToOne) path.getObject();
Collection<JpaJoinColumn> joinColumns = oneToOne.getJoinColumns();
String mappedBy = oneToOne.getMappedBy();
if (joinColumns.isEmpty() && mappedBy == null) {
joinColumns.add(new JpaJoinColumn(AnnotationPrototypes.getJoinColumn()));
}
return true;
}
}
class RelationshipVisitor extends BaseTreeVisitor {
@Override
public boolean onStartNode(ProjectPath path) {
JpaRelationship relationship = (JpaRelationship) path.getObject();
if (Util.isEmptyString(relationship.getTargetEntityName())) {
JpaManagedClass relationshipOwner = (JpaManagedClass) path
.firstInstanceOf(JpaManagedClass.class);
String name = relationship.getName();
JpaClassDescriptor srcDescriptor = relationshipOwner.getClassDescriptor();
JpaPropertyDescriptor property = srcDescriptor.getProperty(name);
Class targetEntityType = property.getTargetEntityType();
if (targetEntityType == null) {
context.recordConflict(new SimpleValidationFailure(property
.getMember(), "Undefined target entity type: " + name));
return false;
}
else {
relationship.setTargetEntityName(targetEntityType.getName());
}
}
return true;
}
}
}