/*****************************************************************
* 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.access;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.access.DataDomainSyncBucket.PropagatedValueFactory;
import org.apache.cayenne.access.util.DefaultOperationObserver;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.dba.QuotingStrategy;
import org.apache.cayenne.dba.TypesMapping;
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.ObjRelationship;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.SQLTemplate;
import org.apache.cayenne.util.HashCodeBuilder;
import org.apache.cayenne.util.Util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A holder of flattened relationship modification data.
*
* @since 1.2
*/
final class FlattenedArcKey {
ObjRelationship relationship;
DbArcId id1;
DbArcId id2;
FlattenedArcKey(ObjectId sourceId, ObjectId destinationId, ObjRelationship relationship) {
this.relationship = relationship;
List<DbRelationship> dbRelationships = relationship.getDbRelationships();
if (dbRelationships.size() != 2) {
throw new CayenneRuntimeException(
"Only single-step flattened relationships are supported in this operation, whereas the relationship '%s' has %s",
relationship, dbRelationships.size());
}
DbRelationship r1 = dbRelationships.get(0);
DbRelationship r2 = dbRelationships.get(1).getReverseRelationship();
if (r2 == null) {
throw new IllegalStateException("No reverse relationship for DbRelationship " + dbRelationships.get(1));
}
id1 = new DbArcId(sourceId, r1);
id2 = new DbArcId(destinationId, r2);
}
/**
* Returns a join DbEntity for the single-step flattened relationship.
*/
DbEntity getJoinEntity() {
return id1.getEntity();
}
/**
* Returns a snapshot for join record for the single-step flattened
* relationship, generating value for the primary key column if it is not
* propagated via the relationships.
*/
Map<String, Object> buildJoinSnapshotForInsert(DataNode node) {
Map<String, Object> snapshot = lazyJoinSnapshot();
boolean autoPkDone = false;
DbEntity joinEntity = getJoinEntity();
for (DbAttribute dbAttr : joinEntity.getPrimaryKeys()) {
String dbAttrName = dbAttr.getName();
if (snapshot.containsKey(dbAttrName)) {
continue;
}
DbAdapter adapter = node.getAdapter();
// skip db-generated... looks like we don't care about the actual PK
// value here, so no need to retrieve db-generated pk back to Java.
if (adapter.supportsGeneratedKeys() && dbAttr.isGenerated()) {
continue;
}
if (autoPkDone) {
throw new CayenneRuntimeException("Primary Key autogeneration only works for a single attribute.");
}
// finally, use database generation mechanism
try {
Object pkValue = adapter.getPkGenerator().generatePk(node, dbAttr);
snapshot.put(dbAttrName, pkValue);
autoPkDone = true;
} catch (Exception ex) {
throw new CayenneRuntimeException("Error generating PK: %s", ex, ex.getMessage());
}
}
return snapshot;
}
/**
* Returns pk snapshots for join records for the single-step flattened
* relationship. Multiple joins between the same pair of objects are
* theoretically possible, so the return value is a list.
*/
List buildJoinSnapshotsForDelete(DataNode node) {
Map<String, Object> snapshot = eagerJoinSnapshot();
DbEntity joinEntity = getJoinEntity();
boolean fetchKey = false;
for (DbAttribute dbAttr : joinEntity.getPrimaryKeys()) {
String dbAttrName = dbAttr.getName();
if (!snapshot.containsKey(dbAttrName)) {
fetchKey = true;
break;
}
}
if (!fetchKey) {
return Collections.singletonList(snapshot);
}
// ok, the key is not included in snapshot, must do the fetch...
// TODO: this should be optimized in the future, but now
// DeleteBatchQuery
// expects a PK snapshot, so we must provide it.
QuotingStrategy quoter = node.getAdapter().getQuotingStrategy();
StringBuilder sql = new StringBuilder("SELECT ");
Collection<DbAttribute> pk = joinEntity.getPrimaryKeys();
final List<DbAttribute> pkList = pk instanceof List ? (List<DbAttribute>) pk : new ArrayList<>(pk);
for (int i = 0; i < pkList.size(); i++) {
if (i > 0) {
sql.append(", ");
}
DbAttribute attribute = pkList.get(i);
sql.append("#result('");
sql.append(quoter.quotedName(attribute));
// since the name of the column can potentially be quoted and
// use reserved keywords as name, let's specify generated column
// name parameters to ensure the query doesn't explode
sql.append("' '").append(TypesMapping.getJavaBySqlType(attribute.getType()));
sql.append("' '").append("pk").append(i);
sql.append("')");
}
sql.append(" FROM ").append(quoter.quotedFullyQualifiedName(joinEntity)).append(" WHERE ");
int i = snapshot.size();
for (Object key : snapshot.keySet()) {
sql.append(quoter.quotedIdentifier(joinEntity, String.valueOf(key))).append(" #bindEqual($").append(key)
.append(")");
if (--i > 0) {
sql.append(" AND ");
}
}
SQLTemplate query = new SQLTemplate(joinEntity.getDataMap(), sql.toString(), true);
query.setParams(snapshot);
final List[] result = new List[1];
node.performQueries(Collections.singleton((Query) query), new DefaultOperationObserver() {
@Override
public void nextRows(Query query, List<?> dataRows) {
if (!dataRows.isEmpty()) {
// decode results...
List<DataRow> fixedRows = new ArrayList<>(dataRows.size());
for (Object o : dataRows) {
DataRow row = (DataRow) o;
DataRow fixedRow = new DataRow(2);
for (int i = 0; i < pkList.size(); i++) {
DbAttribute attribute = pkList.get(i);
fixedRow.put(attribute.getName(), row.get("pk" + i));
}
fixedRows.add(fixedRow);
}
dataRows = fixedRows;
}
result[0] = dataRows;
}
@Override
public void nextQueryException(Query query, Exception ex) {
throw new CayenneRuntimeException("Raising from query exception.", Util.unwindException(ex));
}
@Override
public void nextGlobalException(Exception ex) {
throw new CayenneRuntimeException("Raising from underlyingQueryEngine exception.", Util
.unwindException(ex));
}
});
return result[0];
}
@Override
public int hashCode() {
// order ids in array for hashcode consistency purposes. The actual
// order direction is not important, as long as it
// is consistent across invocations
int compare = id1.getSourceId().getEntityName().compareTo(id2.getSourceId().getEntityName());
if (compare == 0) {
compare = id1.getIncominArc().getName().compareTo(id2.getIncominArc().getName());
if (compare == 0) {
// since ordering is mostly important for detecting equivalent
// FlattenedArc keys coming from 2 opposite directions, the name
// of ObjRelationship can be a good criteria
ObjRelationship or2 = relationship.getReverseRelationship();
compare = or2 != null ? relationship.getName().compareTo(or2.getName()) : 1;
// TODO: if(compare == 0) ??
}
}
DbArcId[] ordered;
if (compare < 0) {
ordered = new DbArcId[] { id1, id2 };
} else {
ordered = new DbArcId[] { id2, id1 };
}
return new HashCodeBuilder().append(ordered).toHashCode();
}
/**
* Defines equal based on whether the relationship is bidirectional.
*/
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof FlattenedArcKey)) {
return false;
}
FlattenedArcKey key = (FlattenedArcKey) object;
// ignore id order in comparison
if (id1.equals(key.id1)) {
return id2.equals(key.id2);
} else if (id1.equals(key.id2)) {
return id2.equals(key.id1);
}
return false;
}
private Map<String, Object> eagerJoinSnapshot() {
List<DbRelationship> relList = relationship.getDbRelationships();
if (relList.size() != 2) {
throw new CayenneRuntimeException(
"Only single-step flattened relationships are supported in this operation: %s", relationship);
}
DbRelationship firstDbRel = relList.get(0);
DbRelationship secondDbRel = relList.get(1);
// here ordering of ids is determined by 'relationship', so use id1, id2
// instead of orderedIds
Map<String, ?> sourceId = id1.getSourceId().getIdSnapshot();
Map<String, ?> destinationId = id2.getSourceId().getIdSnapshot();
Map<String, Object> snapshot = new HashMap<>(sourceId.size() + destinationId.size(), 1);
for (DbJoin join : firstDbRel.getJoins()) {
snapshot.put(join.getTargetName(), sourceId.get(join.getSourceName()));
}
for (DbJoin join : secondDbRel.getJoins()) {
snapshot.put(join.getSourceName(), destinationId.get(join.getTargetName()));
}
return snapshot;
}
private Map<String, Object> lazyJoinSnapshot() {
List<DbRelationship> relList = relationship.getDbRelationships();
if (relList.size() != 2) {
throw new CayenneRuntimeException(
"Only single-step flattened relationships are supported in this operation: %s", relationship);
}
DbRelationship firstDbRel = relList.get(0);
DbRelationship secondDbRel = relList.get(1);
List<DbJoin> fromSourceJoins = firstDbRel.getJoins();
List<DbJoin> toTargetJoins = secondDbRel.getJoins();
Map<String, Object> snapshot = new HashMap<>(fromSourceJoins.size() + toTargetJoins.size(), 1);
// here ordering of ids is determined by 'relationship', so use id1, id2
// instead of orderedIds
for (DbJoin join : fromSourceJoins) {
Object value = new PropagatedValueFactory(id1.getSourceId(), join.getSourceName());
snapshot.put(join.getTargetName(), value);
}
for (DbJoin join : toTargetJoins) {
Object value = new PropagatedValueFactory(id2.getSourceId(), join.getTargetName());
snapshot.put(join.getSourceName(), value);
}
return snapshot;
}
}