/*
* 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.helpers;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.PrismObjectDefinition;
import com.evolveum.midpoint.prism.PrismReference;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.prism.delta.ReferenceDelta;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.util.CloneUtil;
import com.evolveum.midpoint.repo.api.RepoAddOptions;
import com.evolveum.midpoint.repo.api.RepoModifyOptions;
import com.evolveum.midpoint.repo.api.RepositoryService;
import com.evolveum.midpoint.repo.sql.SerializationRelatedException;
import com.evolveum.midpoint.repo.sql.SqlRepositoryConfiguration;
import com.evolveum.midpoint.repo.sql.SqlRepositoryServiceImpl;
import com.evolveum.midpoint.repo.sql.data.RepositoryContext;
import com.evolveum.midpoint.repo.sql.data.common.RObject;
import com.evolveum.midpoint.repo.sql.util.ClassMapper;
import com.evolveum.midpoint.repo.sql.util.DtoTranslationException;
import com.evolveum.midpoint.repo.sql.util.IdGeneratorResult;
import com.evolveum.midpoint.repo.sql.util.PrismIdentifierGenerator;
import com.evolveum.midpoint.repo.sql.util.RUtil;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.RetrieveOption;
import com.evolveum.midpoint.schema.SelectorOptions;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationCampaignType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.FocusType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.LookupTableType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.*;
/**
* @author lazyman, mederly
*/
@Component
public class ObjectUpdater {
private static final Trace LOGGER = TraceManager.getTrace(ObjectUpdater.class);
private static final Trace LOGGER_PERFORMANCE = TraceManager.getTrace(SqlRepositoryServiceImpl.PERFORMANCE_LOG_NAME);
@Autowired
@Qualifier("repositoryService")
private RepositoryService repositoryService;
@Autowired
private BaseHelper baseHelper;
@Autowired
private ObjectRetriever objectRetriever;
@Autowired
private LookupTableHelper lookupTableHelper;
@Autowired
private CertificationCaseHelper caseHelper;
@Autowired
private OrgClosureManager closureManager;
@Autowired
private PrismContext prismContext;
public <T extends ObjectType> String addObjectAttempt(PrismObject<T> object, RepoAddOptions options,
OperationResult result) throws ObjectAlreadyExistsException, SchemaException {
LOGGER_PERFORMANCE.debug("> add object {}, oid={}, overwrite={}",
object.getCompileTimeClass().getSimpleName(), object.getOid(), options.isOverwrite());
String oid = null;
Session session = null;
OrgClosureManager.Context closureContext = null;
// it is needed to keep the original oid for example for import options. if we do not keep it
// and it was null it can bring some error because the oid is set when the object contains orgRef
// or it is org. and by the import we do not know it so it will be trying to delete non-existing object
String originalOid = object.getOid();
try {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Object\n{}", object.debugDump());
}
ObjectTypeUtil.normalizeAllRelations(object);
LOGGER.trace("Translating JAXB to data type.");
PrismIdentifierGenerator.Operation operation = options.isOverwrite() ?
PrismIdentifierGenerator.Operation.ADD_WITH_OVERWRITE :
PrismIdentifierGenerator.Operation.ADD;
RObject rObject = createDataObjectFromJAXB(object, operation);
session = baseHelper.beginTransaction();
closureContext = closureManager.onBeginTransactionAdd(session, object, options.isOverwrite());
if (options.isOverwrite()) {
oid = overwriteAddObjectAttempt(object, rObject, originalOid, session, closureContext, result);
} else {
oid = nonOverwriteAddObjectAttempt(object, rObject, originalOid, session, closureContext);
}
session.getTransaction().commit();
LOGGER.trace("Saved object '{}' with oid '{}'", object.getCompileTimeClass().getSimpleName(), oid);
object.setOid(oid);
} catch (ConstraintViolationException ex) {
handleConstraintViolationException(session, ex, result);
baseHelper.rollbackTransaction(session, ex, result, true);
LOGGER.debug("Constraint violation occurred (will be rethrown as ObjectAlreadyExistsException).", ex);
// we don't know if it's only name uniqueness violation, or something else,
// therefore we're throwing it always as ObjectAlreadyExistsException revert
// to the original oid and prevent of unexpected behaviour (e.g. by import with overwrite option)
if (StringUtils.isEmpty(originalOid)) {
object.setOid(null);
}
String constraintName = ex.getConstraintName();
// Breaker to avoid long unreadable messages
if (constraintName != null && constraintName.length() > SqlRepositoryServiceImpl.MAX_CONSTRAINT_NAME_LENGTH) {
constraintName = null;
}
throw new ObjectAlreadyExistsException("Conflicting object already exists"
+ (constraintName == null ? "" : " (violated constraint '" + constraintName + "')"), ex);
} catch (ObjectAlreadyExistsException | SchemaException ex) {
baseHelper.rollbackTransaction(session, ex, result, true);
throw ex;
} catch (DtoTranslationException | RuntimeException ex) {
baseHelper.handleGeneralException(ex, session, result);
} finally {
cleanupClosureAndSessionAndResult(closureContext, session, result);
}
return oid;
}
private <T extends ObjectType> String overwriteAddObjectAttempt(PrismObject<T> object, RObject rObject,
String originalOid, Session session, OrgClosureManager.Context closureContext, OperationResult result)
throws ObjectAlreadyExistsException, SchemaException, DtoTranslationException {
PrismObject<T> oldObject = null;
//check if object already exists, find differences and increment version if necessary
Collection<? extends ItemDelta> modifications = null;
if (originalOid != null) {
try {
oldObject = objectRetriever.getObjectInternal(session, object.getCompileTimeClass(), originalOid, null, true, result);
ObjectDelta<T> delta = object.diff(oldObject);
modifications = delta.getModifications();
LOGGER.trace("overwriteAddObjectAttempt: originalOid={}, modifications={}", originalOid, modifications);
//we found existing object which will be overwritten, therefore we increment version
Integer version = RUtil.getIntegerFromString(oldObject.getVersion());
version = (version == null) ? 0 : ++version;
rObject.setVersion(version);
// } catch (QueryException ex) {
// baseHelper.handleGeneralCheckedException(ex, session, null);
} catch (ObjectNotFoundException ex) {
//it's ok that object was not found, therefore we won't be overwriting it
}
}
updateFullObject(rObject, object);
RObject merged = (RObject) session.merge(rObject);
lookupTableHelper.addLookupTableRows(session, rObject, oldObject != null);
caseHelper.addCertificationCampaignCases(session, rObject, oldObject != null);
if (closureManager.isEnabled()) {
OrgClosureManager.Operation operation;
if (modifications == null) {
operation = OrgClosureManager.Operation.ADD;
modifications = createAddParentRefDelta(object);
} else {
operation = OrgClosureManager.Operation.MODIFY;
}
closureManager.updateOrgClosure(oldObject, modifications, session, merged.getOid(), object.getCompileTimeClass(),
operation, closureContext);
}
return merged.getOid();
}
private <T extends ObjectType> List<ReferenceDelta> createAddParentRefDelta(PrismObject<T> object) {
PrismReference parentOrgRef = object.findReference(ObjectType.F_PARENT_ORG_REF);
if (parentOrgRef == null || parentOrgRef.isEmpty()) {
return new ArrayList<>();
}
PrismObjectDefinition def = object.getDefinition();
ReferenceDelta delta = ReferenceDelta.createModificationAdd(new ItemPath(ObjectType.F_PARENT_ORG_REF),
def, parentOrgRef.getClonedValues());
return Arrays.asList(delta);
}
public <T extends ObjectType> void updateFullObject(RObject object, PrismObject<T> savedObject)
throws DtoTranslationException, SchemaException {
LOGGER.debug("Updating full object xml column start.");
savedObject.setVersion(Integer.toString(object.getVersion()));
// Deep cloning for object transformation - we don't want to return object "changed" by save.
// Its' because we're removing some properties during save operation and if save fails,
// overwrite attempt (for example using object importer) might try to delete existing object
// and then try to save this object one more time.
String xml = prismContext.serializeObjectToString(savedObject, PrismContext.LANG_XML);
savedObject = prismContext.parseObject(xml);
if (FocusType.class.isAssignableFrom(savedObject.getCompileTimeClass())) {
savedObject.removeProperty(FocusType.F_JPEG_PHOTO);
} else if (LookupTableType.class.equals(savedObject.getCompileTimeClass())) {
savedObject.removeContainer(LookupTableType.F_ROW);
} else if (AccessCertificationCampaignType.class.equals(savedObject.getCompileTimeClass())) {
savedObject.removeContainer(AccessCertificationCampaignType.F_CASE);
}
xml = prismContext.serializeObjectToString(savedObject, PrismContext.LANG_XML);
byte[] fullObject = RUtil.getByteArrayFromXml(xml, getConfiguration().isUseZip());
LOGGER.trace("Storing full object\n{}", xml);
object.setFullObject(fullObject);
LOGGER.debug("Updating full object xml column finish.");
}
protected SqlRepositoryConfiguration getConfiguration() {
return baseHelper.getConfiguration();
}
private <T extends ObjectType> String nonOverwriteAddObjectAttempt(PrismObject<T> object, RObject rObject,
String originalOid, Session session, OrgClosureManager.Context closureContext)
throws ObjectAlreadyExistsException, SchemaException, DtoTranslationException {
// check name uniqueness (by type)
if (StringUtils.isNotEmpty(originalOid)) {
LOGGER.trace("Checking oid uniqueness.");
//todo improve this table name bullshit
Class hqlType = ClassMapper.getHQLTypeClass(object.getCompileTimeClass());
SQLQuery query = session.createSQLQuery("select count(*) from " + RUtil.getTableName(hqlType)
+ " where oid=:oid");
query.setString("oid", object.getOid());
Number count = (Number) query.uniqueResult();
if (count != null && count.longValue() > 0) {
throw new ObjectAlreadyExistsException("Object '" + object.getCompileTimeClass().getSimpleName()
+ "' with oid '" + object.getOid() + "' already exists.");
}
}
updateFullObject(rObject, object);
LOGGER.trace("Saving object (non overwrite).");
String oid = (String) session.save(rObject);
lookupTableHelper.addLookupTableRows(session, rObject, false);
caseHelper.addCertificationCampaignCases(session, rObject, false);
if (closureManager.isEnabled()) {
Collection<ReferenceDelta> modifications = createAddParentRefDelta(object);
closureManager.updateOrgClosure(null, modifications, session, oid, object.getCompileTimeClass(),
OrgClosureManager.Operation.ADD, closureContext);
}
return oid;
}
public <T extends ObjectType> void deleteObjectAttempt(Class<T> type, String oid, OperationResult result)
throws ObjectNotFoundException {
LOGGER_PERFORMANCE.debug("> delete object {}, oid={}", new Object[]{type.getSimpleName(), oid});
Session session = null;
OrgClosureManager.Context closureContext = null;
try {
session = baseHelper.beginTransaction();
closureContext = closureManager.onBeginTransactionDelete(session, type, oid);
Criteria query = session.createCriteria(ClassMapper.getHQLTypeClass(type));
query.add(Restrictions.eq("oid", oid));
RObject object = (RObject) query.uniqueResult();
if (object == null) {
throw new ObjectNotFoundException("Object of type '" + type.getSimpleName() + "' with oid '" + oid
+ "' was not found.", null, oid);
}
closureManager.updateOrgClosure(null, null, session, oid, type, OrgClosureManager.Operation.DELETE, closureContext);
session.delete(object);
if (LookupTableType.class.equals(type)) {
lookupTableHelper.deleteLookupTableRows(session, oid);
}
if (AccessCertificationCampaignType.class.equals(type)) {
caseHelper.deleteCertificationCampaignCases(session, oid);
}
session.getTransaction().commit();
} catch (ObjectNotFoundException ex) {
baseHelper.rollbackTransaction(session, ex, result, true);
throw ex;
} catch (RuntimeException ex) {
baseHelper.handleGeneralException(ex, session, result);
} finally {
cleanupClosureAndSessionAndResult(closureContext, session, result);
}
}
public <T extends ObjectType> void modifyObjectAttempt(Class<T> type, String oid,
Collection<? extends ItemDelta> modifications,
RepoModifyOptions modifyOptions, OperationResult result) throws ObjectNotFoundException,
SchemaException, ObjectAlreadyExistsException, SerializationRelatedException {
// clone - because some certification and lookup table related methods manipulate this collection and even their constituent deltas
// TODO clone elements only if necessary
modifications = CloneUtil.cloneCollectionMembers(modifications);
//modifications = new ArrayList<>(modifications);
LOGGER.debug("Modifying object '{}' with oid '{}'.", new Object[]{type.getSimpleName(), oid});
LOGGER_PERFORMANCE.debug("> modify object {}, oid={}, modifications={}", type.getSimpleName(), oid, modifications);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Modifications:\n{}", DebugUtil.debugDump(modifications));
}
Session session = null;
OrgClosureManager.Context closureContext = null;
try {
session = baseHelper.beginTransaction();
closureContext = closureManager.onBeginTransactionModify(session, type, oid, modifications);
Collection<? extends ItemDelta> lookupTableModifications = lookupTableHelper.filterLookupTableModifications(type, modifications);
Collection<? extends ItemDelta> campaignCaseModifications = caseHelper.filterCampaignCaseModifications(type, modifications);
if (!modifications.isEmpty() || RepoModifyOptions.isExecuteIfNoChanges(modifyOptions)) {
// JpegPhoto (RFocusPhoto) is a special kind of entity. First of all, it is lazily loaded, because photos are really big.
// Each RFocusPhoto naturally belongs to one RFocus, so it would be appropriate to set orphanRemoval=true for focus-photo
// association. However, this leads to a strange problem when merging in-memory RFocus object with the database state:
// If in-memory RFocus object has no photo associated (because of lazy loading), then the associated RFocusPhoto is deleted.
//
// To prevent this behavior, we've set orphanRemoval to false. Fortunately, the remove operation on RFocus
// seems to be still cascaded to RFocusPhoto. What we have to implement ourselves, however, is removal of RFocusPhoto
// _without_ removing of RFocus. In order to know whether the photo has to be removed, we have to retrieve
// its value, apply the delta (e.g. if the delta is a DELETE VALUE X, we have to know whether X matches current
// value of the photo), and if the resulting value is empty, we have to manually delete the RFocusPhoto instance.
//
// So the first step is to retrieve the current value of photo - we obviously do this only if the modifications
// deal with the jpegPhoto property.
Collection<SelectorOptions<GetOperationOptions>> options;
boolean containsFocusPhotoModification = FocusType.class.isAssignableFrom(type) && containsPhotoModification(modifications);
if (containsFocusPhotoModification) {
options = Collections.singletonList(SelectorOptions.create(FocusType.F_JPEG_PHOTO, GetOperationOptions.createRetrieve(RetrieveOption.INCLUDE)));
} else {
options = null;
}
// get object
PrismObject<T> prismObject = objectRetriever.getObjectInternal(session, type, oid, options, true, result);
// apply diff
LOGGER.trace("OBJECT before:\n{}", prismObject.debugDumpLazily());
PrismObject<T> originalObject = null;
if (closureManager.isEnabled()) {
originalObject = prismObject.clone();
}
ItemDelta.applyTo(modifications, prismObject);
LOGGER.trace("OBJECT after:\n{}", prismObject.debugDumpLazily());
// Continuing the photo treatment: should we remove the (now obsolete) focus photo?
// We have to test prismObject at this place, because updateFullObject (below) removes photo property from the prismObject.
boolean shouldPhotoBeRemoved = containsFocusPhotoModification && ((FocusType) prismObject.asObjectable()).getJpegPhoto() == null;
// merge and update object
LOGGER.trace("Translating JAXB to data type.");
ObjectTypeUtil.normalizeAllRelations(prismObject);
RObject rObject = createDataObjectFromJAXB(prismObject, PrismIdentifierGenerator.Operation.MODIFY);
rObject.setVersion(rObject.getVersion() + 1);
updateFullObject(rObject, prismObject);
LOGGER.trace("Starting merge.");
session.merge(rObject);
if (closureManager.isEnabled()) {
closureManager.updateOrgClosure(originalObject, modifications, session, oid, type, OrgClosureManager.Operation.MODIFY, closureContext);
}
// JpegPhoto cleanup: As said before, if a focus has to have no photo (after modifications are applied),
// we have to remove the photo manually.
if (shouldPhotoBeRemoved) {
Query query = session.createQuery("delete RFocusPhoto where ownerOid = :oid");
query.setParameter("oid", prismObject.getOid());
query.executeUpdate();
LOGGER.trace("Focus photo for {} was deleted", prismObject.getOid());
}
}
if (LookupTableType.class.isAssignableFrom(type)) {
lookupTableHelper.updateLookupTableData(session, oid, lookupTableModifications);
}
if (AccessCertificationCampaignType.class.isAssignableFrom(type)) {
caseHelper.updateCampaignCases(session, oid, campaignCaseModifications, modifyOptions);
}
LOGGER.trace("Before commit...");
session.getTransaction().commit();
LOGGER.trace("Committed!");
} catch (ObjectNotFoundException ex) {
baseHelper.rollbackTransaction(session, ex, result, true);
throw ex;
} catch (ConstraintViolationException ex) {
handleConstraintViolationException(session, ex, result);
baseHelper.rollbackTransaction(session, ex, result, true);
LOGGER.debug("Constraint violation occurred (will be rethrown as ObjectAlreadyExistsException).", ex);
// we don't know if it's only name uniqueness violation, or something else,
// therefore we're throwing it always as ObjectAlreadyExistsException
//todo improve (we support only 5 DB, so we should probably do some hacking in here)
throw new ObjectAlreadyExistsException(ex);
} catch (SchemaException ex) {
baseHelper.rollbackTransaction(session, ex, result, true);
throw ex;
} catch (DtoTranslationException | RuntimeException ex) {
baseHelper.handleGeneralException(ex, session, result);
} finally {
cleanupClosureAndSessionAndResult(closureContext, session, result);
LOGGER.trace("Session cleaned up.");
}
}
private <T extends ObjectType> boolean containsPhotoModification(Collection<? extends ItemDelta> modifications) {
ItemPath photoPath = new ItemPath(FocusType.F_JPEG_PHOTO);
for (ItemDelta delta : modifications) {
ItemPath path = delta.getPath();
if (path.isEmpty()) {
throw new UnsupportedOperationException("Focus cannot be modified via empty-path modification");
} else if (photoPath.isSubPathOrEquivalent(path)) { // actually, "subpath" variant should not occur
return true;
}
}
return false;
}
private void cleanupClosureAndSessionAndResult(final OrgClosureManager.Context closureContext, final Session session, final OperationResult result) {
if (closureContext != null) {
closureManager.cleanUpAfterOperation(closureContext, session);
}
baseHelper.cleanupSessionAndResult(session, result);
}
private void handleConstraintViolationException(Session session, ConstraintViolationException ex, OperationResult result) {
// BRUTAL HACK - in PostgreSQL, concurrent changes in parentRefOrg sometimes cause the following exception
// "duplicate key value violates unique constraint "XXXX". This is *not* an ObjectAlreadyExistsException,
// more likely it is a serialization-related one.
//
// TODO: somewhat generalize this approach - perhaps by retrying all operations not dealing with OID/name uniqueness
SQLException sqlException = baseHelper.findSqlException(ex);
if (sqlException != null) {
SQLException nextException = sqlException.getNextException();
LOGGER.debug("ConstraintViolationException = {}; SQL exception = {}; embedded SQL exception = {}", new Object[]{ex, sqlException, nextException});
String[] ok = new String[]{
"duplicate key value violates unique constraint \"m_org_closure_pkey\"",
"duplicate key value violates unique constraint \"m_reference_pkey\""
};
String msg1;
if (sqlException.getMessage() != null) {
msg1 = sqlException.getMessage();
} else {
msg1 = "";
}
String msg2;
if (nextException != null && nextException.getMessage() != null) {
msg2 = nextException.getMessage();
} else {
msg2 = "";
}
for (int i = 0; i < ok.length; i++) {
if (msg1.contains(ok[i]) || msg2.contains(ok[i])) {
baseHelper.rollbackTransaction(session, ex, result, false);
throw new SerializationRelatedException(ex);
}
}
}
}
public <T extends ObjectType> RObject createDataObjectFromJAXB(PrismObject<T> prismObject,
PrismIdentifierGenerator.Operation operation)
throws SchemaException {
PrismIdentifierGenerator generator = new PrismIdentifierGenerator();
IdGeneratorResult generatorResult = generator.generate(prismObject, operation);
T object = prismObject.asObjectable();
RObject rObject;
Class<? extends RObject> clazz = ClassMapper.getHQLTypeClass(object.getClass());
try {
rObject = clazz.newInstance();
Method method = clazz.getMethod("copyFromJAXB", object.getClass(), clazz,
RepositoryContext.class, IdGeneratorResult.class);
method.invoke(clazz, object, rObject, new RepositoryContext(repositoryService, prismContext), generatorResult);
} catch (Exception ex) {
String message = ex.getMessage();
if (StringUtils.isEmpty(message) && ex.getCause() != null) {
message = ex.getCause().getMessage();
}
throw new SchemaException(message, ex);
}
return rObject;
}
}