/* * 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.PrismContainerValue; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.path.IdItemPathSegment; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.path.ItemPathSegment; import com.evolveum.midpoint.prism.query.ObjectQuery; import com.evolveum.midpoint.prism.query.builder.QueryBuilder; import com.evolveum.midpoint.repo.api.RepoModifyOptions; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.repo.sql.data.RepositoryContext; import com.evolveum.midpoint.repo.sql.data.common.RAccessCertificationCampaign; import com.evolveum.midpoint.repo.sql.data.common.RObject; import com.evolveum.midpoint.repo.sql.data.common.container.RAccessCertificationCase; import com.evolveum.midpoint.repo.sql.data.common.container.RAccessCertificationWorkItem; import com.evolveum.midpoint.repo.sql.data.common.container.RCertWorkItemReference; import com.evolveum.midpoint.repo.sql.query.QueryException; import com.evolveum.midpoint.repo.sql.query.RQuery; import com.evolveum.midpoint.repo.sql.query2.QueryEngine2; import com.evolveum.midpoint.repo.sql.util.*; import com.evolveum.midpoint.schema.GetOperationOptions; import com.evolveum.midpoint.schema.SelectorOptions; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.logging.LoggingUtils; 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.AccessCertificationCaseType; import com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationWorkItemType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import org.hibernate.Criteria; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.criterion.Restrictions; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.*; /** * Contains methods specific to handle certification cases. * (As these cases are stored outside main certification campaign object.) * * It is quite a temporary solution in order to ease SqlRepositoryServiceImpl * from tons of type-specific code. Serious solution would be to implement * subobject-level operations more generically. * * @author mederly */ @Component public class CertificationCaseHelper { private static final Trace LOGGER = TraceManager.getTrace(CertificationCaseHelper.class); @Autowired private PrismContext prismContext; @Autowired private GeneralHelper generalHelper; @Autowired private NameResolutionHelper nameResolutionHelper; @Autowired private ObjectRetriever objectRetriever; @Autowired private RepositoryService repositoryService; public void addCertificationCampaignCases(Session session, RObject object, boolean deleteBeforeAdd) { if (!(object instanceof RAccessCertificationCampaign)) { return; } RAccessCertificationCampaign campaign = (RAccessCertificationCampaign) object; if (deleteBeforeAdd) { LOGGER.trace("Deleting existing cases for {}", campaign.getOid()); deleteCertificationCampaignCases(session, campaign.getOid()); } if (campaign.getCase() != null) { for (RAccessCertificationCase aCase : campaign.getCase()) { session.save(aCase); } } } public void addCertificationCampaignCases(Session session, String campaignOid, Collection<PrismContainerValue> values, int currentId, List<Long> affectedIds) throws DtoTranslationException { for (PrismContainerValue value : values) { AccessCertificationCaseType caseType = new AccessCertificationCaseType(); caseType.setupContainerValue(value); if (caseType.getId() == null) { caseType.setId((long) currentId); currentId++; } // we need to generate IDs but we (currently) do not use that for setting "isTransient" flag PrismIdentifierGenerator generator = new PrismIdentifierGenerator(); generator.generate(caseType, PrismIdentifierGenerator.Operation.MODIFY); RAccessCertificationCase row = RAccessCertificationCase.toRepo(campaignOid, caseType, createRepositoryContext()); row.setId(RUtil.toInteger(caseType.getId())); affectedIds.add(caseType.getId()); session.save(row); } } @NotNull private RepositoryContext createRepositoryContext() { return new RepositoryContext(repositoryService, prismContext); } public void deleteCertificationCampaignCases(Session session, String oid) { // TODO couldn't this cascading be done by hibernate itself? // Query deleteReferences = session.getNamedQuery("delete.campaignCasesReferences"); // deleteReferences.setParameter("oid", oid); // deleteReferences.executeUpdate(); Query deleteWorkItemReferences = session.getNamedQuery("delete.campaignCasesWorkItemReferences"); deleteWorkItemReferences.setParameter("oid", oid); deleteWorkItemReferences.executeUpdate(); Query deleteWorkItems = session.getNamedQuery("delete.campaignCasesWorkItems"); deleteWorkItems.setParameter("oid", oid); deleteWorkItems.executeUpdate(); Query deleteCases = session.getNamedQuery("delete.campaignCases"); deleteCases.setParameter("oid", oid); deleteCases.executeUpdate(); } public <T extends ObjectType> Collection<? extends ItemDelta> filterCampaignCaseModifications(Class<T> type, Collection<? extends ItemDelta> modifications) { Collection<ItemDelta> caseDelta = new ArrayList<>(); if (!AccessCertificationCampaignType.class.equals(type)) { return caseDelta; } ItemPath casePath = new ItemPath(AccessCertificationCampaignType.F_CASE); for (ItemDelta delta : modifications) { ItemPath path = delta.getPath(); if (path.isEmpty()) { throw new UnsupportedOperationException("Certification campaign cannot be modified via empty-path modification"); } else if (path.equivalent(casePath)) { caseDelta.add(delta); } else if (path.isSuperPath(casePath)) { // like case[id]/xxx caseDelta.add(delta); } } modifications.removeAll(caseDelta); return caseDelta; } public <T extends ObjectType> void updateCampaignCases(Session session, String campaignOid, Collection<? extends ItemDelta> modifications, RepoModifyOptions modifyOptions) throws SchemaException, ObjectNotFoundException, DtoTranslationException { if (modifications.isEmpty() && !RepoModifyOptions.isExecuteIfNoChanges(modifyOptions)) { return; } List<Long> casesAddedOrDeleted = addOrDeleteCases(session, campaignOid, modifications); LOGGER.trace("Cases added/deleted (null means REPLACE operation) = {}", casesAddedOrDeleted); updateCasesContent(session, campaignOid, modifications, casesAddedOrDeleted, modifyOptions); } protected List<Long> addOrDeleteCases(Session session, String campaignOid, Collection<? extends ItemDelta> modifications) throws SchemaException, DtoTranslationException { final ItemPath casePath = new ItemPath(AccessCertificationCampaignType.F_CASE); boolean replacePresent = false; List<Long> affectedIds = new ArrayList<>(); for (ItemDelta delta : modifications) { ItemPath deltaPath = delta.getPath(); if (!casePath.isSubPathOrEquivalent(deltaPath)) { throw new IllegalStateException("Wrong campaign delta sneaked into updateCampaignCases: class=" + delta.getClass() + ", path=" + deltaPath); } if (deltaPath.size() == 1) { if (delta.getValuesToDelete() != null) { // todo do 'bulk' delete like delete from ... where oid=? and id in (...) for (PrismContainerValue value : (Collection<PrismContainerValue>) delta.getValuesToDelete()) { Long id = value.getId(); if (id == null) { throw new SchemaException("Couldn't delete certification case with null id"); } affectedIds.add(id); // TODO couldn't this cascading be done by hibernate itself? Integer integerCaseId = RUtil.toInteger(id); // Query deleteCaseReferences = session.createSQLQuery("delete from " + RCertCaseReference.TABLE + // " where owner_owner_oid=:oid and owner_id=:id"); // deleteCaseReferences.setString("oid", campaignOid); // deleteCaseReferences.setInteger("id", integerCaseId); // deleteCaseReferences.executeUpdate(); Query deleteWorkItemReferences = session.createSQLQuery("delete from " + RCertWorkItemReference.TABLE + " where owner_owner_owner_oid=:oid and owner_owner_id=:id"); deleteWorkItemReferences.setString("oid", campaignOid); deleteWorkItemReferences.setInteger("id", integerCaseId); deleteWorkItemReferences.executeUpdate(); Query deleteCaseWorkItems = session.createSQLQuery("delete from " + RAccessCertificationWorkItem.TABLE + " where owner_owner_oid=:oid and owner_id=:id"); deleteCaseWorkItems.setString("oid", campaignOid); deleteCaseWorkItems.setInteger("id", integerCaseId); deleteCaseWorkItems.executeUpdate(); Query deleteCase = session.getNamedQuery("delete.campaignCase"); deleteCase.setString("oid", campaignOid); deleteCase.setInteger("id", integerCaseId); deleteCase.executeUpdate(); } } // TODO generated IDs might conflict with client-provided ones // also, client-provided IDs might conflict with those that are already in the database // So it's safest not to provide any IDs by the client if (delta.getValuesToAdd() != null) { int currentId = generalHelper.findLastIdInRepo(session, campaignOid, "get.campaignCaseLastId") + 1; addCertificationCampaignCases(session, campaignOid, delta.getValuesToAdd(), currentId, affectedIds); } if (delta.getValuesToReplace() != null) { deleteCertificationCampaignCases(session, campaignOid); addCertificationCampaignCases(session, campaignOid, delta.getValuesToReplace(), 1, affectedIds); replacePresent = true; } } } return replacePresent ? null : affectedIds; } private void updateCasesContent(Session session, String campaignOid, Collection<? extends ItemDelta> modifications, List<Long> casesAddedOrDeleted, RepoModifyOptions modifyOptions) throws SchemaException, ObjectNotFoundException, DtoTranslationException { Set<Long> casesModified = new HashSet<>(); for (ItemDelta delta : modifications) { ItemPath deltaPath = delta.getPath(); if (deltaPath.size() > 1) { LOGGER.trace("Updating campaign " + campaignOid + " with delta " + delta); // should start with "case[id]" long id = checkPathSanity(deltaPath, casesAddedOrDeleted); Query query = session.getNamedQuery("get.campaignCase"); query.setString("ownerOid", campaignOid); query.setInteger("id", (int) id); byte[] fullObject = (byte[]) query.uniqueResult(); if (fullObject == null) { throw new ObjectNotFoundException("Couldn't update cert campaign " + campaignOid + " + by delta with path " + deltaPath + " - specified case does not exist"); } AccessCertificationCaseType aCase = RAccessCertificationCase.createJaxb(fullObject, prismContext, false); delta = delta.clone(); // to avoid changing original modifications delta.setParentPath(delta.getParentPath().tail(2)); // remove "case[id]" from the delta path delta.applyTo(aCase.asPrismContainerValue()); // we need to generate IDs but we (currently) do not use that for setting "isTransient" flag PrismIdentifierGenerator generator = new PrismIdentifierGenerator(); generator.generate(aCase, PrismIdentifierGenerator.Operation.MODIFY); RAccessCertificationCase rCase = RAccessCertificationCase.toRepo(campaignOid, aCase, createRepositoryContext()); session.merge(rCase); LOGGER.trace("Access certification case {} merged", rCase); casesModified.add(aCase.getId()); } } // refresh campaign cases, if requested if (RepoModifyOptions.isExecuteIfNoChanges(modifyOptions)) { Query query = session.getNamedQuery("get.campaignCases"); query.setString("ownerOid", campaignOid); List<Object> cases = query.list(); for (Object o : cases) { if (!(o instanceof byte[])) { throw new IllegalStateException("Certification case: expected byte[], got " + o.getClass()); } byte[] fullObject = (byte[]) o; AccessCertificationCaseType aCase = RAccessCertificationCase.createJaxb(fullObject, prismContext, false); Long id = aCase.getId(); if (id != null && casesAddedOrDeleted != null && !casesAddedOrDeleted.contains(id) && !casesModified.contains(id)) { RAccessCertificationCase rCase = RAccessCertificationCase.toRepo(campaignOid, aCase, createRepositoryContext()); session.merge(rCase); LOGGER.trace("Access certification case {} refreshed", rCase); } } } } private long checkPathSanity(ItemPath deltaPath, List<Long> casesAddedOrDeleted) { ItemPathSegment secondSegment = deltaPath.getSegments().get(1); if (!(secondSegment instanceof IdItemPathSegment)) { throw new IllegalStateException("Couldn't update cert campaign by delta with path " + deltaPath + " - should start with case[id]"); } Long id = ((IdItemPathSegment) secondSegment).getId(); if (id == null) { throw new IllegalStateException("Couldn't update cert campaign by delta with path " + deltaPath + " - should start with case[id]"); } if (deltaPath.size() == 2) { // not enough throw new IllegalStateException("Couldn't update cert campaign by delta with path " + deltaPath + " - should start with case[id] and contain additional path"); } if (casesAddedOrDeleted == null || casesAddedOrDeleted.contains(id)) { throw new IllegalArgumentException("Couldn't update certification case that was added/deleted in this operation. Path=" + deltaPath); } return id; } // TODO find a better name public AccessCertificationCaseType updateLoadedCertificationCase(GetContainerableResult result, Map<String, PrismObject<AccessCertificationCampaignType>> ownersMap, Collection<SelectorOptions<GetOperationOptions>> options, Session session, OperationResult operationResult) throws SchemaException { AccessCertificationCaseType aCase = RAccessCertificationCase.createJaxb(result.getFullObject(), prismContext, false); generalHelper.validateContainerable(aCase, AccessCertificationCaseType.class); String ownerOid = result.getOwnerOid(); PrismObject<AccessCertificationCampaignType> campaign = resolveCampaign(ownerOid, ownersMap, session, operationResult); if (campaign != null && !campaign.asObjectable().getCase().contains(aCase)) { campaign.asObjectable().getCase().add(aCase); } return aCase; } public AccessCertificationWorkItemType updateLoadedCertificationWorkItem(GetCertificationWorkItemResult result, Map<String, PrismContainerValue<AccessCertificationCaseType>> casesCache, // key=OID:ID Map<String, PrismObject<AccessCertificationCampaignType>> campaignsCache, // key=OID Collection<SelectorOptions<GetOperationOptions>> options, QueryEngine2 engine, Session session, OperationResult operationResult) throws SchemaException, QueryException { String campaignOid = result.getCampaignOid(); Integer caseId = result.getCaseId(); Integer workItemId = result.getId(); String caseKey = campaignOid + ":" + caseId; PrismContainerValue<AccessCertificationCaseType> casePcv = casesCache.get(caseKey); if (casePcv == null) { ObjectQuery query = QueryBuilder.queryFor(AccessCertificationCaseType.class, prismContext) .ownerId(campaignOid) .and().id(caseId) .build(); RQuery caseQuery = engine.interpret(query, AccessCertificationCaseType.class, null, false, session); List<GetContainerableResult> cases = caseQuery.list(); if (cases.size() > 1) { throw new IllegalStateException( "More than one certification case found for campaign " + campaignOid + ", ID " + caseId); } else if (cases.isEmpty()) { // we need it, because otherwise we have only identifiers for the work item, no data throw new IllegalStateException("No certification case found for campaign " + campaignOid + ", ID " + caseId); } AccessCertificationCaseType _case = updateLoadedCertificationCase(cases.get(0), campaignsCache, null, session, operationResult); casePcv = _case.asPrismContainerValue(); casesCache.put(caseKey, casePcv); } @SuppressWarnings({"raw", "unchecked"}) PrismContainerValue<AccessCertificationWorkItemType> workItemPcv = (PrismContainerValue<AccessCertificationWorkItemType>) casePcv.find(new ItemPath(AccessCertificationCaseType.F_WORK_ITEM, workItemId)); if (workItemPcv == null) { throw new IllegalStateException("No work item " + workItemId + " in " + casePcv); } else { return workItemPcv.asContainerable(); } } private PrismObject<AccessCertificationCampaignType> resolveCampaign(String campaignOid, Map<String, PrismObject<AccessCertificationCampaignType>> campaignsCache, Session session, OperationResult operationResult) { PrismObject<AccessCertificationCampaignType> campaign = campaignsCache.get(campaignOid); if (campaign != null) { return campaign; } try { campaign = objectRetriever.getObjectInternal(session, AccessCertificationCampaignType.class, campaignOid, null, false, operationResult); } catch (ObjectNotFoundException|SchemaException|DtoTranslationException|RuntimeException e) { LoggingUtils.logExceptionOnDebugLevel(LOGGER, "Couldn't get campaign with OID {}", e, campaignOid); return null; } campaignsCache.put(campaignOid, campaign); return campaign; } // adds cases to campaign if requested by options public <T extends ObjectType> void updateLoadedCampaign(PrismObject<T> object, Collection<SelectorOptions<GetOperationOptions>> options, Session session) throws SchemaException { if (!SelectorOptions.hasToLoadPath(AccessCertificationCampaignType.F_CASE, options)) { return; } LOGGER.debug("Loading certification campaign cases."); Criteria criteria = session.createCriteria(RAccessCertificationCase.class); criteria.add(Restrictions.eq("ownerOid", object.getOid())); // TODO fetch only XML representation List<RAccessCertificationCase> cases = criteria.list(); if (cases == null || cases.isEmpty()) { return; } AccessCertificationCampaignType campaign = (AccessCertificationCampaignType) object.asObjectable(); List<AccessCertificationCaseType> jaxbCases = campaign.getCase(); for (RAccessCertificationCase rCase : cases) { AccessCertificationCaseType jaxbCase = rCase.toJAXB(prismContext); jaxbCases.add(jaxbCase); } } }