/*****************************************************************
* 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.dbsync.merge.context;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.dbsync.filter.NameFilter;
import org.apache.cayenne.dbsync.naming.NameBuilder;
import org.apache.cayenne.dbsync.naming.ObjectNameGenerator;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.Entity;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.map.Relationship;
import org.apache.cayenne.util.DeleteRuleUpdater;
import org.apache.cayenne.util.EntityMergeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Implements methods for entity merging.
*/
public class EntityMergeSupport {
private static final Logger LOGGER = LoggerFactory.getLogger(EntityMergeSupport.class);
private static final Map<String, String> CLASS_TO_PRIMITIVE = new HashMap<>();
/**
* Type conversion to Java 8 types (now it's only java.time.* types)
*/
private static final Map<Integer, String> SQL_TYPE_TO_JAVA8_TYPE = new HashMap<>();
static {
CLASS_TO_PRIMITIVE.put(Byte.class.getName(), "byte");
CLASS_TO_PRIMITIVE.put(Long.class.getName(), "long");
CLASS_TO_PRIMITIVE.put(Double.class.getName(), "double");
CLASS_TO_PRIMITIVE.put(Boolean.class.getName(), "boolean");
CLASS_TO_PRIMITIVE.put(Float.class.getName(), "float");
CLASS_TO_PRIMITIVE.put(Short.class.getName(), "short");
CLASS_TO_PRIMITIVE.put(Integer.class.getName(), "int");
SQL_TYPE_TO_JAVA8_TYPE.put(Types.DATE, "java.time.LocalDate");
SQL_TYPE_TO_JAVA8_TYPE.put(Types.TIME, "java.time.LocalTime");
SQL_TYPE_TO_JAVA8_TYPE.put(Types.TIMESTAMP, "java.time.LocalDateTime");
}
private ObjectNameGenerator nameGenerator;
private final List<EntityMergeListener> listeners;
private final boolean removingMeaningfulFKs;
private final NameFilter meaningfulPKsFilter;
private final boolean usingPrimitives;
private final boolean usingJava7Types;
public EntityMergeSupport(ObjectNameGenerator nameGenerator,
NameFilter meaningfulPKsFilter,
boolean removingMeaningfulFKs,
boolean usingPrimitives,
boolean usingJava7Types) {
this.listeners = new ArrayList<>();
this.nameGenerator = nameGenerator;
this.removingMeaningfulFKs = removingMeaningfulFKs;
this.meaningfulPKsFilter = meaningfulPKsFilter;
this.usingPrimitives = usingPrimitives;
this.usingJava7Types = usingJava7Types;
// will ensure that all created ObjRelationships would have
// default delete rule
addEntityMergeListener(DeleteRuleUpdater.getEntityMergeListener());
}
public boolean isRemovingMeaningfulFKs() {
return removingMeaningfulFKs;
}
/**
* Updates each one of the collection of ObjEntities, adding attributes and
* relationships based on the current state of its DbEntity.
*
* @return true if any ObjEntity has changed as a result of synchronization.
*/
public boolean synchronizeWithDbEntities(Iterable<ObjEntity> objEntities) {
boolean changed = false;
for (ObjEntity nextEntity : objEntities) {
if (synchronizeWithDbEntity(nextEntity)) {
changed = true;
}
}
return changed;
}
/**
* Updates ObjEntity attributes and relationships based on the current state
* of its DbEntity.
*
* @return true if the ObjEntity has changed as a result of synchronization.
*/
public boolean synchronizeWithDbEntity(ObjEntity entity) {
if (entity == null) {
return false;
}
DbEntity dbEntity = entity.getDbEntity();
if (dbEntity == null) {
return false;
}
boolean changed = false;
if (removingMeaningfulFKs) {
changed = getRidOfAttributesThatAreNowSrcAttributesForRelationships(entity);
}
changed |= addMissingAttributes(entity);
changed |= addMissingRelationships(entity);
return changed;
}
/**
* @since 4.0
*/
public boolean synchronizeOnDbAttributeAdded(ObjEntity entity, DbAttribute dbAttribute) {
Collection<DbRelationship> incomingRels = getIncomingRelationships(dbAttribute.getEntity());
if (shouldAddToObjEntity(entity, dbAttribute, incomingRels)) {
addMissingAttribute(entity, dbAttribute);
return true;
}
return false;
}
/**
* @since 4.0
*/
public boolean synchronizeOnDbRelationshipAdded(ObjEntity entity, DbRelationship dbRelationship) {
if (shouldAddToObjEntity(entity, dbRelationship)) {
addMissingRelationship(entity, dbRelationship);
if (removingMeaningfulFKs) {
getRidOfAttributesThatAreNowSrcAttributesForRelationships(entity);
}
}
return true;
}
private boolean addMissingRelationships(ObjEntity entity) {
List<DbRelationship> relationshipsToAdd = getRelationshipsToAdd(entity);
if (relationshipsToAdd.isEmpty()) {
return false;
}
for (DbRelationship dr : relationshipsToAdd) {
addMissingRelationship(entity, dr);
}
return true;
}
private boolean createObjRelationship(ObjEntity entity, DbRelationship dr, String targetEntityName) {
ObjRelationship or = new ObjRelationship();
or.setName(NameBuilder.builder(or, entity)
.baseName(nameGenerator.relationshipName(dr))
.name());
or.addDbRelationship(dr);
Map<String, ObjEntity> objEntities = entity.getDataMap().getSubclassesForObjEntity(entity);
boolean hasFlattingAttributes = false;
boolean needGeneratedEntity = true;
if (objEntities.containsKey(targetEntityName)) {
needGeneratedEntity = false;
}
for (ObjEntity subObjEntity : objEntities.values()) {
for (ObjAttribute objAttribute : subObjEntity.getAttributes()) {
String path = objAttribute.getDbAttributePath();
if (path != null) {
if (path.startsWith(or.getDbRelationshipPath())) {
hasFlattingAttributes = true;
break;
}
}
}
}
if (!hasFlattingAttributes) {
if (needGeneratedEntity) {
or.setTargetEntityName(targetEntityName);
or.setSourceEntity(entity);
}
entity.addRelationship(or);
fireRelationshipAdded(or);
}
return needGeneratedEntity;
}
private boolean addMissingAttributes(ObjEntity entity) {
boolean changed = false;
for (DbAttribute da : getAttributesToAdd(entity)) {
addMissingAttribute(entity, da);
changed = true;
}
return changed;
}
private void addMissingRelationship(ObjEntity entity, DbRelationship dbRelationship) {
// getting DataMap from DbRelationship's source entity. This is the only object in our arguments that
// is guaranteed to be a part of the map....
DataMap dataMap = dbRelationship.getSourceEntity().getDataMap();
DbEntity targetEntity = dbRelationship.getTargetEntity();
Collection<ObjEntity> mappedObjEntities = dataMap.getMappedEntities(targetEntity);
if (mappedObjEntities.isEmpty()) {
if (targetEntity == null) {
targetEntity = new DbEntity(dbRelationship.getTargetEntityName());
}
if (dbRelationship.getTargetEntityName() != null) {
boolean needGeneratedEntity = createObjRelationship(entity, dbRelationship, nameGenerator.objEntityName(targetEntity));
if (needGeneratedEntity) {
LOGGER.warn("Can't find ObjEntity for " + dbRelationship.getTargetEntityName());
LOGGER.warn("Db Relationship (" + dbRelationship + ") will have GUESSED Obj Relationship reflection. ");
}
}
} else {
for (Entity mappedTarget : mappedObjEntities) {
createObjRelationship(entity, dbRelationship, mappedTarget.getName());
}
}
}
private void addMissingAttribute(ObjEntity entity, DbAttribute da) {
ObjAttribute oa = new ObjAttribute();
oa.setName(NameBuilder.builder(oa, entity).baseName(nameGenerator.objAttributeName(da)).name());
oa.setEntity(entity);
oa.setType(getTypeForObjAttribute(da));
oa.setDbAttributePath(da.getName());
entity.addAttribute(oa);
fireAttributeAdded(oa);
}
private String getTypeForObjAttribute(DbAttribute dbAttribute) {
String java8Type;
if(!usingJava7Types && (java8Type = SQL_TYPE_TO_JAVA8_TYPE.get(dbAttribute.getType())) != null) {
return java8Type;
}
String type = TypesMapping.getJavaBySqlType(dbAttribute.getType());
String primitiveType;
if (usingPrimitives && (primitiveType = CLASS_TO_PRIMITIVE.get(type)) != null) {
return primitiveType;
}
return type;
}
private boolean getRidOfAttributesThatAreNowSrcAttributesForRelationships(ObjEntity entity) {
boolean changed = false;
for (DbAttribute da : getMeaningfulFKs(entity)) {
ObjAttribute oa = entity.getAttributeForDbAttribute(da);
while (oa != null) {
String attrName = oa.getName();
entity.removeAttribute(attrName);
changed = true;
oa = entity.getAttributeForDbAttribute(da);
}
}
return changed;
}
/**
* Returns a list of DbAttributes that are mapped to foreign keys.
*
* @since 1.2
*/
public Collection<DbAttribute> getMeaningfulFKs(ObjEntity objEntity) {
List<DbAttribute> fks = new ArrayList<>(2);
for (ObjAttribute property : objEntity.getAttributes()) {
DbAttribute column = property.getDbAttribute();
// check if adding it makes sense at all
if (column != null && column.isForeignKey()) {
fks.add(column);
}
}
return fks;
}
/**
* Returns a list of attributes that exist in the DbEntity, but are missing
* from the ObjEntity.
*/
private List<DbAttribute> getAttributesToAdd(ObjEntity objEntity) {
DbEntity dbEntity = objEntity.getDbEntity();
List<DbAttribute> missing = new ArrayList<>();
Collection<DbRelationship> incomingRels = getIncomingRelationships(dbEntity);
for (DbAttribute dba : dbEntity.getAttributes()) {
if (shouldAddToObjEntity(objEntity, dba, incomingRels)) {
missing.add(dba);
}
}
return missing;
}
private boolean shouldAddToObjEntity(ObjEntity entity, DbAttribute dbAttribute, Collection<DbRelationship> incomingRels) {
if (dbAttribute.getName() == null || entity.getAttributeForDbAttribute(dbAttribute) != null) {
return false;
}
boolean addMeaningfulPK = meaningfulPKsFilter.isIncluded(entity.getDbEntityName());
if (dbAttribute.isPrimaryKey()) {
return addMeaningfulPK;
}
// check FK's
if(isFK(dbAttribute, dbAttribute.getEntity().getRelationships(), true)) {
return false;
}
// check incoming relationships
return !isFK(dbAttribute, incomingRels, false);
}
private boolean isFK(DbAttribute dbAttribute, Collection<DbRelationship> collection, boolean source) {
for (DbRelationship rel : collection) {
for (DbJoin join : rel.getJoins()) {
DbAttribute joinAttribute = source ? join.getSource() : join.getTarget();
if (joinAttribute == dbAttribute) {
return true;
}
}
}
return false;
}
private boolean shouldAddToObjEntity(ObjEntity entity, DbRelationship dbRelationship) {
if(dbRelationship.getName() == null) {
return false;
}
for(Relationship relationship : entity.getRelationships()) {
ObjRelationship objRelationship = (ObjRelationship)relationship;
if(objRelationshipHasDbRelationship(objRelationship, dbRelationship)) {
return false;
}
}
return true;
}
/**
* @return true if objRelationship includes given dbRelationship
*/
private boolean objRelationshipHasDbRelationship(ObjRelationship objRelationship, DbRelationship dbRelationship) {
for(DbRelationship relationship : objRelationship.getDbRelationships()) {
if(relationship.getSourceEntityName().equals(dbRelationship.getSourceEntityName())
&& relationship.getTargetEntityName().equals(dbRelationship.getTargetEntityName())
&& isSameAttributes(relationship.getSourceAttributes(), dbRelationship.getSourceAttributes())
&& isSameAttributes(relationship.getTargetAttributes(), dbRelationship.getTargetAttributes())) {
return true;
}
}
return false;
}
/**
* @param collection1 first collection to compare
* @param collection2 second collection to compare
* @return true if collections have same size and attributes in them have same names
*/
private boolean isSameAttributes(Collection<DbAttribute> collection1, Collection<DbAttribute> collection2) {
if(collection1.size() != collection2.size()) {
return false;
}
if(collection1.isEmpty()) {
return true;
}
Iterator<DbAttribute> iterator1 = collection1.iterator();
Iterator<DbAttribute> iterator2 = collection2.iterator();
for(int i=0; i<collection1.size(); i++) {
DbAttribute attr1 = iterator1.next();
DbAttribute attr2 = iterator2.next();
if(!attr1.getName().equals(attr2.getName())) {
return false;
}
}
return true;
}
private Collection<DbRelationship> getIncomingRelationships(DbEntity entity) {
Collection<DbRelationship> incoming = new ArrayList<>();
for (DbEntity nextEntity : entity.getDataMap().getDbEntities()) {
for (DbRelationship relationship : nextEntity.getRelationships()) {
// TODO: PERFORMANCE 'getTargetEntity' is generally slow, called
// in this iterator it is showing (e.g. in YourKit profiles)..
// perhaps use cheaper 'getTargetEntityName()' or even better -
// pre-cache all relationships by target entity to avoid O(n)
// search ?
// (need to profile to prove the difference)
if (entity == relationship.getTargetEntity()) {
incoming.add(relationship);
}
}
}
return incoming;
}
protected List<DbRelationship> getRelationshipsToAdd(ObjEntity objEntity) {
List<DbRelationship> missing = new ArrayList<>();
for (DbRelationship dbRel : objEntity.getDbEntity().getRelationships()) {
if (shouldAddToObjEntity(objEntity, dbRel)) {
missing.add(dbRel);
}
}
return missing;
}
/**
* Registers new EntityMergeListener
*/
public void addEntityMergeListener(EntityMergeListener listener) {
listeners.add(listener);
}
/**
* Unregisters an EntityMergeListener
*/
public void removeEntityMergeListener(EntityMergeListener listener) {
listeners.remove(listener);
}
/**
* Notifies all listeners that an ObjAttribute was added
*/
private void fireAttributeAdded(ObjAttribute attr) {
for (EntityMergeListener listener : listeners) {
listener.objAttributeAdded(attr);
}
}
/**
* Notifies all listeners that an ObjRelationship was added
*/
private void fireRelationshipAdded(ObjRelationship rel) {
for (EntityMergeListener listener : listeners) {
listener.objRelationshipAdded(rel);
}
}
public void setNameGenerator(ObjectNameGenerator nameGenerator) {
this.nameGenerator = nameGenerator;
}
}