/* * Copyright (c) 2010-2014 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.PrismObject; import com.evolveum.midpoint.prism.PrismReferenceValue; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ReferenceDelta; import com.evolveum.midpoint.prism.query.ObjectQuery; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.repo.sql.SqlRepositoryConfiguration; import com.evolveum.midpoint.repo.sql.data.common.ROrgClosure; import com.evolveum.midpoint.repo.sql.data.common.other.RObjectType; import com.evolveum.midpoint.schema.ResultHandler; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.result.OperationResultStatus; import com.evolveum.midpoint.util.Holder; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SystemException; 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.ObjectReferenceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.OrgType; import org.apache.commons.lang.Validate; import org.apache.commons.lang.mutable.MutableInt; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.internal.SessionFactoryImpl; import org.hibernate.jdbc.Work; import org.hibernate.tool.hbm2ddl.SchemaUpdate; import org.hibernate.type.IntegerType; import org.hibernate.type.StringType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.*; /** * This class and its subclasses provides org. closure table handling. * * Data structures used are: * * (1) Repo object graph G = (V, E) where V is a set of vertices (repo objects) and E is a set of edges (parentRef relations). * There is an edge e = (V1, V2) in E [i.e. edge from V1 to V2] if and only if V1.parentRef contains V2 [i.e. V2 is a parent of V1]. * * (2) OrgClosure table. OrgClosure(D, A, N) iff there are exactly N paths in object graph from D (descendant) to A (ancestor). * It is transitive reflexive closure, i.e. OrgClosure(V, V, 1) items are there as well. * * Algorithms taken from "SQL Design Patterns" book by Vadim Tropashko (http://vadimtropashko.wordpress.com/) * namely from Chapter 6 (http://vadimtropashko.files.wordpress.com/2014/01/book_sql_chap6_v1.pdf). * SQL queries were then optimized by hand for various database engines. * * @author lazyman * @author mederly */ @Component @DependsOn("repositoryService") public class OrgClosureManager { private static final Trace LOGGER = TraceManager.getTrace(OrgClosureManager.class); @Autowired @Qualifier("repositoryService") private RepositoryService repositoryService; @Autowired private BaseHelper baseHelper; private static boolean DUMP_TABLES = false; private static final boolean COUNT_CLOSURE_RECORDS = false; static final String CLOSURE_TABLE_NAME = "m_org_closure"; public static final String TEMP_DELTA_TABLE_NAME_FOR_ORACLE = "m_org_closure_temp_delta"; // only for single-thread performance testing long lastOperationDuration; //region Public interface /** * Main method called from SQL repository service to update the closure table during an operation. * @param originalObject Original state of the object - before applying modification in the repository. * It is used only in case of MODIFY (note that "overwriting ADD" is present here as MODIFY!) * @param modifications Collection of modifications to be applied to the object. * @param session Database session to use. * @param oid OID of the object. * @param type Type of the object. * @param operation Operation that is carried out. * @param closureContext */ public <T extends ObjectType> void updateOrgClosure(PrismObject<? extends ObjectType> originalObject, Collection<? extends ItemDelta> modifications, Session session, String oid, Class<T> type, Operation operation, Context closureContext) { if (!isEnabled()) { return; } if (!OrgType.class.isAssignableFrom(type)) { return; } session.flush(); session.clear(); long time = System.currentTimeMillis(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("################# Starting {} for org. closure for {} oid={}.", new Object[]{operation, type.getSimpleName(), oid}); } List<ReferenceDelta> deltas = filterParentRefDeltas(modifications); switch (operation) { case ADD: handleAdd(oid, deltas, closureContext, session); break; case DELETE: handleDelete(oid, closureContext, session); break; case MODIFY: handleModify(oid, deltas, originalObject, closureContext, session); } long duration = System.currentTimeMillis() - time; LOGGER.debug("################# Org. closure update finished in {} ms.", duration); lastOperationDuration = duration; } /* * Note: the interface for onBeginTransactionXYZ is a bit different from the one used for updateOrgClosure. * For example, "overwriting ADD" is indicated as onBeginTransactionAdd(..., overwrite:=true) but as * updateOrgClosure(..., operation:=MODIFY). Also, onBeginTransactionXYZ gets raw data, while * updateOrgClosure gets data after some pre-processing (e.g. exctraction of parentOrgRef reference values). * * This will be perhaps unified in the future. */ public <T extends ObjectType> Context onBeginTransactionAdd(Session session, PrismObject<T> object, boolean overwrite) { if (!isEnabled() || !(OrgType.class.isAssignableFrom(object.getCompileTimeClass()))) { return null; } // we have to be ready for closure-related operation even if there are no known parents (because there may be orphans pointing to this org!) return onBeginTransaction(session); } public <T extends ObjectType> Context onBeginTransactionModify(Session session, Class<T> type, String oid, Collection<? extends ItemDelta> modifications) { if (!isEnabled()) { return null; } if (!(OrgType.class.isAssignableFrom(type))) { return null; } if (filterParentRefDeltas(modifications).isEmpty()) { return null; } return onBeginTransaction(session); } public <T extends ObjectType> Context onBeginTransactionDelete(Session session, Class<T> type, String oid) { if (!isEnabled() || !(OrgType.class.isAssignableFrom(type))) { return null; } return onBeginTransaction(session); } private Context onBeginTransaction(Session session) { // table locking if (isH2() || isOracle() || isSQLServer()) { lockClosureTable(session); } // other Context ctx = new Context(); if (isH2()) { ctx.temporaryTableName = generateDeltaTempTableName(); String createTableQueryText = "create temporary table " + ctx.temporaryTableName + " (\n" + " descendant_oid VARCHAR(36) NOT NULL,\n" + " ancestor_oid VARCHAR(36) NOT NULL,\n" + " val INTEGER NOT NULL,\n" + " PRIMARY KEY (descendant_oid, ancestor_oid)\n" + ")"; long start = System.currentTimeMillis(); Query q = session.createSQLQuery(createTableQueryText); q.executeUpdate(); LOGGER.trace("Temporary table {} created in {} ms", ctx.temporaryTableName, System.currentTimeMillis()-start); } return ctx; } // may cause implicit commit!!! (in H2) public void cleanUpAfterOperation(Context closureContext, Session session) { if (closureContext == null) { return; } if (closureContext.temporaryTableName == null) { return; } if (isH2()) { // beware, this does implicit commit! Query dropQuery = session.createSQLQuery("drop table if exists " + closureContext.temporaryTableName); dropQuery.executeUpdate(); closureContext.temporaryTableName = null; } } @PostConstruct public void initialize() { OperationResult result = new OperationResult(OrgClosureManager.class.getName() + ".initialize"); if (!isEnabled()) { return; } SqlRepositoryConfiguration repoConfiguration = baseHelper.getConfiguration(); if (isOracle()) { initializeOracleTemporaryTable(); } if (autoUpdateClosureTableStructure()) { // need to rebuild the content of the closure table after re-creating it anew checkAndOrRebuild(false, true, repoConfiguration.isStopOnOrgClosureStartupFailure(), true, result); } else { boolean check, rebuild; switch (repoConfiguration.getOrgClosureStartupAction()) { case NONE: return; case CHECK: check = true; rebuild = false; break; case REBUILD_IF_NEEDED: check = true; rebuild = true; break; case ALWAYS_REBUILD: check = false; rebuild = true; break; default: throw new IllegalArgumentException("Invalid value: " + repoConfiguration.getOrgClosureStartupAction()); } checkAndOrRebuild(check, rebuild, repoConfiguration.isStopOnOrgClosureStartupFailure(), true, result); } } // TEMPORARY and quite BRUTAL HACK // Originally in midPoint 3.0, m_org_closure has 5 columns, a non-null ID among them. // In 3.1, it has 3, and no ID. Unfortunately, hibernate's hbm2ddl tool does not automatically remove ID column, // so people can expect quite hard-to-understand error messages when running midPoint after upgrade. // // This code removes and re-creates the m_org_closure table if hbm2ddl is set to "update". // // returns true if the table was re-created private boolean autoUpdateClosureTableStructure() { if (baseHelper.getConfiguration().isSkipOrgClosureStructureCheck()) { LOGGER.debug("Skipping org closure structure check."); return false; } SessionFactory sf = baseHelper.getSessionFactory(); if (sf instanceof SessionFactoryImpl) { SessionFactoryImpl sfi = ((SessionFactoryImpl) sf); LOGGER.debug("SessionFactoryImpl.getSettings() = {}; auto update schema = {}", sfi.getSettings(), sfi.getSettings() != null ? sfi.getSettings().isAutoUpdateSchema() : null); if (sfi.getSettings() != null && sfi.getSettings().isAutoUpdateSchema()) { LOGGER.info("Checking the closure table structure."); final Session session = baseHelper.getSessionFactory().openSession(); final Holder<Boolean> wrongNumberOfColumns = new Holder<>(false); session.doWork(new Work() { @Override public void execute(Connection connection) throws SQLException { DatabaseMetaData meta = connection.getMetaData(); if (meta == null) { LOGGER.warn("No database metadata found."); } else { ResultSet rsColumns = meta.getColumns(null, null, CLOSURE_TABLE_NAME, null); int columns = 0; while (rsColumns.next()) { LOGGER.debug("Column: {} {}", rsColumns.getString("TYPE_NAME"), rsColumns.getString("COLUMN_NAME")); columns++; } if (columns > 0) { LOGGER.debug("There are {} columns in {} (obtained via DatabaseMetaData)", columns, CLOSURE_TABLE_NAME); if (columns != 3) { wrongNumberOfColumns.setValue(true); } return; } // perhaps some problem here... let's try another way out try { Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("select * from " + CLOSURE_TABLE_NAME); int cols = rs.getMetaData().getColumnCount(); if (cols > 0) { LOGGER.debug("There are {} columns in {} (obtained via resultSet.getMetaData())", cols, CLOSURE_TABLE_NAME); if (cols != 3) { wrongNumberOfColumns.setValue(true); } } else { LOGGER.warn("Couldn't determine the number of columns in {}. In case of problems, please fix your database structure manually using DB scripts in 'config' folder.", CLOSURE_TABLE_NAME); } rs.close(); // don't care about closing them in case of failure stmt.close(); } catch (RuntimeException e) { LoggingUtils.logException(LOGGER, "Couldn't obtain the number of columns in {}. In case of problems running midPoint, please fix your database structure manually using DB scripts in 'config' folder.", e, CLOSURE_TABLE_NAME); } } } }); if (wrongNumberOfColumns.getValue()) { session.getTransaction().begin(); LOGGER.info("Wrong number of columns detected; dropping table " + CLOSURE_TABLE_NAME); Query q = session.createSQLQuery("drop table " + CLOSURE_TABLE_NAME); q.executeUpdate(); session.getTransaction().commit(); LOGGER.info("Calling hibernate hbm2ddl SchemaUpdate tool to create the table in the necessary form."); new SchemaUpdate(sfi.getServiceRegistry(), baseHelper.getSessionFactoryBean().getConfiguration()).execute(false, true); LOGGER.info("Done, table was (hopefully) created. If not, please fix your database structure manually using DB scripts in 'config' folder."); return true; } } else { // auto schema update is disabled } } else { LOGGER.warn("SessionFactory is not of type SessionFactoryImpl; it is {}", sf != null ? sf.getClass() : "null"); } return false; } public boolean isEnabled() { return !baseHelper.getConfiguration().isIgnoreOrgClosure(); } /** * Does a consistency check (either quick or thorough one) and rebuilds the closure table if necessary. * * Quick check is conducted by comparing whether are there any orgs without entries in closure * (the reverse direction is ensured via foreign keys in m_org_closure). * * Thorough check is conducted by recomputing the closure table. */ public void checkAndOrRebuild(boolean check, boolean rebuild, boolean stopOnFailure, boolean quickCheckOnly, OperationResult result) { LOGGER.debug("Org closure check/rebuild request: check={}, rebuild={}", check?(quickCheckOnly?"quick":"thorough"):"none", rebuild); if (!isEnabled()) { result.recordWarning("Organizational closure processing is disabled."); return; } if (!check && !rebuild) { result.recordWarning("Neither 'check' nor 'rebuild' option was requested."); return; // nothing to do here } Session session = baseHelper.getSessionFactory().openSession(); Context context = null; boolean rebuilt = false; try { session.getTransaction().begin(); if (rebuild || (check && !quickCheckOnly)) { // thorough check requires the temporary table as well context = onBeginTransaction(session); } if (quickCheckOnly) { boolean ok = false; if (check) { int problems = quickCheck(session); if (problems != 0) { LOGGER.warn("Content of M_ORG_CLOSURE table is not consistent with the content of M_ORG one. Missing OIDs: {}", problems); if (!rebuild && stopOnFailure) { throw new IllegalStateException("Content of M_ORG_CLOSURE table is not consistent with the content of M_ORG one. Missing OIDs: " + problems); } } else { LOGGER.debug("Org closure quick test passed."); ok = true; } } if (!ok && rebuild) { rebuild(false, true, stopOnFailure, context, session, result); rebuilt = true; } } else { // if the check has to be thorough rebuild(check, rebuild, stopOnFailure, context, session, result); rebuilt = rebuild; // if we are here this means the CL was rebuilt if it was to be rebuilt if (stopOnFailure && result.isError()) { throw new IllegalStateException(result.getMessage()); } } if (rebuilt) { session.getTransaction().commit(); LOGGER.info("Recomputed org closure table was successfully committed into database."); } else { // if !rebuilt, we either didn't do any modifications (in quick check mode) // or we did, but we want them to disappear (although this wish is a bit strange...) session.getTransaction().rollback(); } } catch (SchemaException|RuntimeException e) { LoggingUtils.logException(LOGGER, "Exception during check and/or recomputation of closure table", e); session.getTransaction().rollback(); if (stopOnFailure) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new IllegalStateException("Unexpected exception during check and/or recomputation of closure table: " + e.getMessage(), e); } } } finally { cleanUpAfterOperation(context, session); // commits in case of H2! session.close(); } } //endregion //region Rebuilding or checking org closure // we are already in the context of a transaction (and the org struct table is locked if possible) // "check" here means "thorough check" (i.e. comparing with recomputed closure) private void rebuild(boolean check, boolean rebuild, boolean stopOnFailure, final Context context, final Session session, OperationResult result) throws SchemaException { List existingEntries = null; if (check) { LOGGER.info("Reading from existing org closure table"); Query selectQuery = session.createSQLQuery("SELECT descendant_oid, ancestor_oid, val from " + CLOSURE_TABLE_NAME) .addScalar("descendant_oid", StringType.INSTANCE) .addScalar("ancestor_oid", StringType.INSTANCE) .addScalar("val", IntegerType.INSTANCE); existingEntries = selectQuery.list(); LOGGER.info("{} entries read", existingEntries.size()); } LOGGER.info("Computing org closure table from scratch"); Query deleteQuery = session.createSQLQuery("delete from " + CLOSURE_TABLE_NAME); deleteQuery.executeUpdate(); LOGGER.trace("Closure table content deleted"); final int orgsTotal = repositoryService.countObjects(OrgType.class, new ObjectQuery(), result); final MutableInt orgsProcessed = new MutableInt(0); ResultHandler<OrgType> handler = new ResultHandler<OrgType>() { @Override public boolean handle(PrismObject<OrgType> object, OperationResult parentResult) { LOGGER.trace("Processing {}", object); handleAdd(object.getOid(), getParentOidsFromObject(object), context, session); orgsProcessed.add(1); int currentState = orgsProcessed.intValue(); if (currentState % 100 == 0) { LOGGER.info("{} organizations processed (out of {})", currentState, orgsTotal); } return true; } }; repositoryService.searchObjectsIterative(OrgType.class, new ObjectQuery(), handler, null, false, result); LOGGER.info("Org closure table was successfully recomputed (not committed yet); all {} organizations processed", orgsTotal); if (check) { LOGGER.info("Reading from recomputed org closure table"); Query selectQuery = session.createSQLQuery("SELECT descendant_oid, ancestor_oid, val from " + CLOSURE_TABLE_NAME) .addScalar("descendant_oid", StringType.INSTANCE) .addScalar("ancestor_oid", StringType.INSTANCE) .addScalar("val", IntegerType.INSTANCE); List recomputedEntries = selectQuery.list(); LOGGER.info("{} entries read", recomputedEntries.size()); compareOrgClosureTables(existingEntries, recomputedEntries, rebuild, result); } else { result.recordSuccess(); } } private void compareOrgClosureTables(List existingEntries, List recomputedEntries, boolean rebuild, OperationResult result) { Set<List> existing = convertEntries(existingEntries); Set<List> recomputed = convertEntries(recomputedEntries); if (!existing.equals(recomputed)) { String addendum; OperationResultStatus status; if (rebuild) { status = OperationResultStatus.HANDLED_ERROR; addendum = " The table has been recomputed and now it is OK."; } else { status = OperationResultStatus.FATAL_ERROR; addendum = " Please recompute the table as soon as possible."; } String m = "Closure table is not consistent with the repository. Expected size: " + recomputed.size() + " actual size: " + existing.size() + "." + addendum; result.recordStatus(status, m); LOGGER.info(m); } else { String m = "Closure table is OK (" + existing.size() + " entries)"; result.recordStatus(OperationResultStatus.SUCCESS, m); LOGGER.info(m); } } private Set<List> convertEntries(List<Object[]> entries) { Set<List> rv = new HashSet<>(); for (Object[] entry : entries) { rv.add(Arrays.asList(entry)); } return rv; } private int quickCheck(Session session) { Query q = session.createSQLQuery( "select count(m_org.oid) as problems from m_org left join m_org_closure cl " + "on cl.descendant_oid = m_org.oid and cl.ancestor_oid = m_org.oid " + "where cl.descendant_oid is null").addScalar("problems", IntegerType.INSTANCE); List problemsList = q.list(); if (problemsList == null || problemsList.size() != 1) { throw new IllegalStateException("Unexpected return value from the closure check query: " + problemsList + " (a 1-item list of Integer expected)"); } return (int) problemsList.get(0); } //endregion //region Handling ADD operation // we can safely expect that the object didn't exist before (because the "overwriting add" is sent to us as MODIFY operation) private void handleAdd(String oid, List<ReferenceDelta> deltas, Context context, Session session) { handleAdd(oid, getParentOidsToAdd(deltas, null), context, session); } // parents may be non-existent at this point private void handleAdd(String oid, Set<String> parents, Context context, Session session) { // adding self-record session.save(new ROrgClosure(oid, oid, 1)); session.flush(); session.clear(); List<String> livingChildren = getChildren(oid, session); // no need to check existence of these oids, as owner is a FK pointing to RObject in RParentRef LOGGER.trace("Living children = {}", livingChildren); addChildrenEdges(oid, livingChildren, context, session); // all parents are "new", so we should just select which do really exist at this moment Collection<String> livingParents = retainExistingOids(parents, session); LOGGER.trace("Living parents = {} (parents = {})", livingParents, parents); if (livingParents.size() <= 1 && (livingChildren == null || livingChildren.isEmpty())) { String parent; if (livingParents.isEmpty()) { parent = null; } else { parent = livingParents.iterator().next(); } addEdgeSimple(oid, parent, session); } else { addParentEdges(oid, livingParents, context, session); } } // we expect that all livingChildren do exist and // that none of the links child->oid does exist yet private void addChildrenEdges(String oid, List<String> livingChildren, Context context, Session session) { List<Edge> edges = childrenToEdges(oid, livingChildren); addIndependentEdges(edges, context, session); } private List<Edge> childrenToEdges(String oid, List<String> livingChildren) { List<Edge> edges = new ArrayList<>(); for (String child : livingChildren) { edges.add(new Edge(child, oid)); } return edges; } // we expect that all livingParents do exist and // that none of the links oid->parent does exist yet private void addParentEdges(String oid, Collection<String> livingParents, Context context, Session session) { List<Edge> edges = parentsToEdges(oid, livingParents); addIndependentEdges(edges, context, session); } private List<Edge> parentsToEdges(String oid, Collection<String> parents) { List<Edge> edges = new ArrayList<>(); for (String parent : parents) { edges.add(new Edge(oid, parent)); } return edges; } // we expect that the link oid->parent does not exist yet and the parent exists private void addEdgeSimple(String oid, String parent, Session session) { if (parent != null) { long start = System.currentTimeMillis(); Query addToClosureQuery = session.createSQLQuery( "insert into "+ CLOSURE_TABLE_NAME +" (descendant_oid, ancestor_oid, val) " + "select :oid as descendant_oid, CL.ancestor_oid as ancestor_oid, CL.val as val " + "from "+ CLOSURE_TABLE_NAME +" CL " + "where CL.descendant_oid = :parent"); addToClosureQuery.setString("oid", oid); addToClosureQuery.setParameter("parent", parent); int count = addToClosureQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("addEdges simplified: Added {} records to closure table ({} ms).", count, System.currentTimeMillis() - start); } session.flush(); session.clear(); } // IMPORTANT PRECONDITIONS: // - all edges are "real", i.e. their tails and heads (as objects) do exist in repository // - none of the edges does exist yet private void addIndependentEdges(List<Edge> edges, Context context, Session session) { long start = System.currentTimeMillis(); LOGGER.trace("===================== ADD INDEPENDENT EDGES: {} ================", edges); if (!edges.isEmpty()) { // for unknown reason, queries in the form of // select t1.descendant_oid as descendant_oid, t2.ancestor_oid as ancestor_oid, sum(t1.val*t2.val) as val // from m_org_closure t1, m_org_closure t2 where // (t1.ancestor_oid = 'o21101..-....-....-....-............' and t2.descendant_oid = 'o1101...-....-....-....-............') // or (t1.ancestor_oid = 'o21101..-....-....-....-............' and t2.descendant_oid = 'o2130...-....-....-....-............') // group by t1.descendant_oid, t2.ancestor_oid // // take radically longer in H2 than queries without the disjunction (i.e. having only one "ancestor=X and descendant=Y" item) // So, in H2, we insert the edges one-by-one if (isH2()) { for (Edge edge : edges) { addIndependentEdgesInternal(Arrays.asList(edge), context, session); } } else { addIndependentEdgesInternal(edges, context, session); } } session.flush(); session.clear(); LOGGER.trace("--------------------- DONE ADD EDGES: {} ({} ms) ----------------", edges, System.currentTimeMillis() - start); } private void addIndependentEdgesInternal(List<Edge> edges, Context context, Session session) { checkForCycles(edges, session); String deltaTempTableName = computeDeltaTable(edges, context, session); try { int count; if (isMySQL() || isMariaDb() || isOracle() || isSQLServer()) { long startUpsert = System.currentTimeMillis(); String upsertQueryText; if (isMySQL() || isMariaDb()) { upsertQueryText = "insert into " + CLOSURE_TABLE_NAME + " (descendant_oid, ancestor_oid, val) " + "select descendant_oid, ancestor_oid, val from " + deltaTempTableName + " delta " + "on duplicate key update " + CLOSURE_TABLE_NAME + ".val = " + CLOSURE_TABLE_NAME + ".val + values(val)"; } else if (isSQLServer()) { // TODO try if this one (without prefixes in INSERT clause does not work for Oracle) upsertQueryText = "merge into " + CLOSURE_TABLE_NAME + " closure " + "using (select descendant_oid, ancestor_oid, val from " + deltaTempTableName + ") delta " + "on (closure.descendant_oid = delta.descendant_oid and closure.ancestor_oid = delta.ancestor_oid) " + "when matched then update set closure.val = closure.val + delta.val " + "when not matched then insert (descendant_oid, ancestor_oid, val) " + "values (delta.descendant_oid, delta.ancestor_oid, delta.val);"; } else { // Oracle upsertQueryText = "merge into " + CLOSURE_TABLE_NAME + " closure " + "using (select descendant_oid, ancestor_oid, val from " + deltaTempTableName + ") delta " + "on (closure.descendant_oid = delta.descendant_oid and closure.ancestor_oid = delta.ancestor_oid) " + "when matched then update set closure.val = closure.val + delta.val " + "when not matched then insert (closure.descendant_oid, closure.ancestor_oid, closure.val) " + "values (delta.descendant_oid, delta.ancestor_oid, delta.val)"; } Query upsertQuery = session.createSQLQuery(upsertQueryText); int countUpsert = upsertQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("Added/updated {} records to closure table ({} ms)", countUpsert, System.currentTimeMillis() - startUpsert); if (DUMP_TABLES) dumpOrgClosureTypeTable(session, CLOSURE_TABLE_NAME); } else { // separate update and insert long startUpdate = System.currentTimeMillis(); String updateInClosureQueryText; if (isH2()) { updateInClosureQueryText = "update " + CLOSURE_TABLE_NAME + " " + "set val = val + (select val from " + deltaTempTableName + " td " + "where td.descendant_oid=" + CLOSURE_TABLE_NAME + ".descendant_oid and td.ancestor_oid=" + CLOSURE_TABLE_NAME + ".ancestor_oid) " + "where (descendant_oid, ancestor_oid) in (select (descendant_oid, ancestor_oid) from " + deltaTempTableName + ")"; } else if (isPostgreSQL()) { updateInClosureQueryText = "update " + CLOSURE_TABLE_NAME + " " + "set val = val + (select val from " + deltaTempTableName + " td " + "where td.descendant_oid=" + CLOSURE_TABLE_NAME + ".descendant_oid and td.ancestor_oid=" + CLOSURE_TABLE_NAME + ".ancestor_oid) " + "where (descendant_oid, ancestor_oid) in (select descendant_oid, ancestor_oid from " + deltaTempTableName + ")"; } else { throw new UnsupportedOperationException("Org. closure manager - unsupported database operation"); } Query updateInClosureQuery = session.createSQLQuery(updateInClosureQueryText); int countUpdate = updateInClosureQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("Updated {} records to closure table ({} ms)", countUpdate, System.currentTimeMillis() - startUpdate); if (DUMP_TABLES) dumpOrgClosureTypeTable(session, CLOSURE_TABLE_NAME); long startAdd = System.currentTimeMillis(); String addQuery = "insert into " + CLOSURE_TABLE_NAME + " (descendant_oid, ancestor_oid, val) " + "select descendant_oid, ancestor_oid, val from " + deltaTempTableName + " delta "; if (countUpdate > 0) { if (isH2()) { addQuery += " where (descendant_oid, ancestor_oid) not in (select (descendant_oid, ancestor_oid) from " + CLOSURE_TABLE_NAME + ")"; } else if (isPostgreSQL()) { addQuery += " where not exists (select 1 from " + CLOSURE_TABLE_NAME + " cl where cl.descendant_oid=delta.descendant_oid and cl.ancestor_oid=delta.ancestor_oid)"; } else { throw new UnsupportedOperationException("Org. closure manager - unsupported database operation"); } } Query addToClosureQuery = session.createSQLQuery(addQuery); count = addToClosureQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("Added {} records to closure table ({} ms)", count, System.currentTimeMillis() - startAdd); if (DUMP_TABLES) dumpOrgClosureTypeTable(session, CLOSURE_TABLE_NAME); } } finally { dropDeltaTableIfNecessary(session, deltaTempTableName); } } // Checks that there is no edge=(D,A) such that A->D exists in the transitive closure // (this would yield a cycle D->A->D in the graph) private void checkForCycles(List<Edge> edges, Session session) { String queryText = "select descendant_oid, ancestor_oid from " + CLOSURE_TABLE_NAME + " where " + getWhereClauseForCycleCheck(edges); Query query = session.createSQLQuery(queryText) .addScalar("descendant_oid", StringType.INSTANCE) .addScalar("ancestor_oid", StringType.INSTANCE); long start = System.currentTimeMillis(); List list = query.list(); LOGGER.trace("Cycles checked in {} ms, {} conflicts found", System.currentTimeMillis()-start, list.size()); if (!list.isEmpty()) { throw new IllegalArgumentException("Modification couldn't be executed, because a cycle in org structure graph would be created. Cycle-creating edges being added: " + formatList(list)); } } private String formatList(List list) { StringBuilder sb = new StringBuilder(); int count = 0; for (Object o : list) { Object[] pair = (Object[]) o; String descendant = (String) pair[0]; String ancestor = (String) pair[1]; if (count++ > 0) { sb.append(", "); } sb.append(ancestor).append(" -> ").append(descendant); // results contains "existing" paths, so the newly added ones are in reverse if (count == 10) { sb.append("... (and ").append(list.size()-10).append(" more)"); break; } } return sb.toString(); } private String getWhereClauseForCycleCheck(List<Edge> edges) { StringBuilder whereClause = new StringBuilder(); boolean first = true; for (Edge edge : edges) { if (first) { first = false; } else { whereClause.append(" or "); } checkOidSyntax(edge.getAncestor()); checkOidSyntax(edge.getDescendant()); whereClause.append("(ancestor_oid = '").append(edge.getDescendant()).append("'"); whereClause.append(" and descendant_oid = '").append(edge.getAncestor()).append("')"); } return whereClause.toString(); } // temporary solution, until we find out how to send a list of pairs to the query private void checkOidSyntax(String oid) { for (int i = 0; i < oid.length(); i++) { char c = oid.charAt(i); if (!Character.isLetterOrDigit(c) && c != '-' && c != ':' && c != '.') { // strictly speaking, : and . are not legal either, but we use them in tests throw new IllegalArgumentException("Illegal character(s) in OID " + oid); } } } private void dropDeltaTableIfNecessary(Session session, String deltaTempTableName) { // postgresql deletes the table automatically on commit // in H2 we delete the table after whole closure operation (after commit) if (isMySQL() || isMariaDb()) { Query dropQuery = session.createSQLQuery("drop temporary table " + deltaTempTableName); dropQuery.executeUpdate(); } else if (isSQLServer()) { // TODO drop temporary if using SQL Server Query dropQuery = session.createSQLQuery( "if (exists (" + "select * " + "from sys.tables " + "where name like '"+deltaTempTableName+"%'))\n" + "drop table " + deltaTempTableName + ";"); dropQuery.executeUpdate(); } } //endregion //region Handling DELETE operation private void handleDelete(String oid, Context context, Session session) { List<String> livingChildren = getChildren(oid, session); if (livingChildren.isEmpty()) { handleDeleteLeaf(oid, session); return; } // delete all edges "<child> -> OID" from the closure removeChildrenEdges(oid, livingChildren, context, session); if (LOGGER.isTraceEnabled()) LOGGER.trace("Deleted {} 'child' links.", livingChildren.size()); // delete all edges "OID -> <parent>" from the closure List<String> livingParents = retainExistingOids(getParents(oid, session), session); removeParentEdges(oid, livingParents, context, session); if (LOGGER.isTraceEnabled()) LOGGER.trace("Deleted {} 'parent' links.", livingParents.size()); // delete (OID, OID) record Query deleteSelfQuery = session.createSQLQuery("delete from "+ CLOSURE_TABLE_NAME +" " + "where descendant_oid=:oid and ancestor_oid=:oid"); deleteSelfQuery.setString("oid", oid); int count = deleteSelfQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("Removed {} self-record from closure table.", count); } private void handleDeleteLeaf(String oid, Session session) { Query removeFromClosureQuery = session.createSQLQuery( "delete from " + CLOSURE_TABLE_NAME + " " + "where descendant_oid = :oid"); removeFromClosureQuery.setString("oid", oid); int count = removeFromClosureQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("DeleteLeaf: Removed {} records from closure table.", count); } private void removeChildrenEdges(String oid, List<String> livingChildren, Context context, Session session) { List<Edge> edges = childrenToEdges(oid, livingChildren); removeIndependentEdges(edges, context, session); } private void removeParentEdges(String oid, Collection<String> parents, Context context, Session session) { List<Edge> edges = parentsToEdges(oid, parents); removeIndependentEdges(edges, context, session); } private void removeIndependentEdges(List<Edge> edges, Context context, Session session) { long start = System.currentTimeMillis(); LOGGER.trace("===================== REMOVE INDEPENDENT EDGES: {} ================", edges); if (!edges.isEmpty()) { // for the reason for this decomposition, see addIndependentEdges if (isH2()) { for (Edge edge : edges) { removeIndependentEdgesInternal(Arrays.asList(edge), context, session); } } else { removeIndependentEdgesInternal(edges, context, session); } } session.flush(); session.clear(); LOGGER.trace("--------------------- DONE REMOVE EDGES: {} ({} ms) ----------------", edges, System.currentTimeMillis()-start); } private void removeIndependentEdgesInternal(List<Edge> edges, Context context, Session session) { String deltaTempTableName = computeDeltaTable(edges, context, session); try { int count; String deleteFromClosureQueryText, updateInClosureQueryText; if (isH2()) { // delete with join is not supported by H2 // and the "postgresql/oracle version" does not work for some reasons deleteFromClosureQueryText = "delete from " + CLOSURE_TABLE_NAME + " cl " + "where exists (" + "select 0 from " + deltaTempTableName + " delta " + "where cl.descendant_oid = delta.descendant_oid and cl.ancestor_oid = delta.ancestor_oid and cl.val = delta.val)"; updateInClosureQueryText = "update " + CLOSURE_TABLE_NAME + " " + "set val = val - (select val from " + deltaTempTableName + " td " + "where td.descendant_oid=" + CLOSURE_TABLE_NAME + ".descendant_oid and td.ancestor_oid=" + CLOSURE_TABLE_NAME + ".ancestor_oid) " + "where (descendant_oid, ancestor_oid) in (select (descendant_oid, ancestor_oid) from " + deltaTempTableName + ")"; } else if (isPostgreSQL() || isOracle()) { deleteFromClosureQueryText = "delete from " + CLOSURE_TABLE_NAME + " " + "where (descendant_oid, ancestor_oid, val) in " + "(select descendant_oid, ancestor_oid, val from " + deltaTempTableName + ")"; updateInClosureQueryText = "update " + CLOSURE_TABLE_NAME + " " + "set val = val - (select val from " + deltaTempTableName + " td " + "where td.descendant_oid=" + CLOSURE_TABLE_NAME + ".descendant_oid and td.ancestor_oid=" + CLOSURE_TABLE_NAME + ".ancestor_oid) " + "where (descendant_oid, ancestor_oid) in (select descendant_oid, ancestor_oid from " + deltaTempTableName + ")"; } else if (isSQLServer()) { // delete is the same as for MySQL deleteFromClosureQueryText = "delete " + CLOSURE_TABLE_NAME + " from " + CLOSURE_TABLE_NAME + " " + "inner join " + deltaTempTableName + " td on " + "td.descendant_oid = "+ CLOSURE_TABLE_NAME +".descendant_oid and td.ancestor_oid = "+ CLOSURE_TABLE_NAME +".ancestor_oid and "+ "td.val = "+ CLOSURE_TABLE_NAME +".val"; // update is also done via inner join (as in MySQL), but using slightly different syntax updateInClosureQueryText = "update " + CLOSURE_TABLE_NAME + " " + "set "+ CLOSURE_TABLE_NAME +".val = "+ CLOSURE_TABLE_NAME +".val - td.val " + "from "+ CLOSURE_TABLE_NAME + " " + "inner join " + deltaTempTableName + " td " + "on td.descendant_oid=" + CLOSURE_TABLE_NAME + ".descendant_oid and " + "td.ancestor_oid=" + CLOSURE_TABLE_NAME + ".ancestor_oid"; } else if (isMySQL() || isMariaDb()) { // http://stackoverflow.com/questions/652770/delete-with-join-in-mysql // TODO consider this for other databases as well deleteFromClosureQueryText = "delete " + CLOSURE_TABLE_NAME + " from " + CLOSURE_TABLE_NAME + " " + "inner join " + deltaTempTableName + " td on " + "td.descendant_oid = "+ CLOSURE_TABLE_NAME +".descendant_oid and td.ancestor_oid = "+ CLOSURE_TABLE_NAME +".ancestor_oid and "+ "td.val = "+ CLOSURE_TABLE_NAME +".val"; // it is not possible to use temporary table twice in a query // TODO consider using this in postgresql as well... updateInClosureQueryText = "update " + CLOSURE_TABLE_NAME + " join " + deltaTempTableName + " td " + "on td.descendant_oid=" + CLOSURE_TABLE_NAME + ".descendant_oid and td.ancestor_oid=" + CLOSURE_TABLE_NAME + ".ancestor_oid " + "set "+ CLOSURE_TABLE_NAME +".val = "+ CLOSURE_TABLE_NAME +".val - td.val"; } else { throw new UnsupportedOperationException("Org. closure manager - unsupported database operation"); } long startDelete = System.currentTimeMillis(); Query deleteFromClosureQuery = session.createSQLQuery(deleteFromClosureQueryText); count = deleteFromClosureQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("Deleted {} records from closure table in {} ms", count, System.currentTimeMillis() - startDelete); if (DUMP_TABLES) dumpOrgClosureTypeTable(session, CLOSURE_TABLE_NAME); long startUpdate = System.currentTimeMillis(); Query updateInClosureQuery = session.createSQLQuery(updateInClosureQueryText); count = updateInClosureQuery.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("Updated {} records in closure table in {} ms", count, System.currentTimeMillis() - startUpdate); if (DUMP_TABLES) dumpOrgClosureTypeTable(session, CLOSURE_TABLE_NAME); } finally { dropDeltaTableIfNecessary(session, deltaTempTableName); } } //endregion //region Handling MODIFY private void handleModify(String oid, Collection<? extends ItemDelta> modifications, PrismObject<? extends ObjectType> originalObject, Context context, Session session) { if (modifications.isEmpty()) { return; } Set<String> parentsToDelete = getParentOidsToDelete(modifications, originalObject); Set<String> parentsToAdd = getParentOidsToAdd(modifications, originalObject); Collection<String> livingParentsToDelete = retainExistingOids(parentsToDelete, session); Collection<String> livingParentsToAdd = retainExistingOids(parentsToAdd, session); parentsToDelete.removeAll(parentsToAdd); // if something is deleted and the re-added we can skip this operation removeParentEdges(oid, livingParentsToDelete, context, session); addParentEdges(oid, livingParentsToAdd, context, session); } //endregion //region Misc private void lockClosureTable(Session session) { long start = System.currentTimeMillis(); LOGGER.trace("Locking closure table"); if (isH2()) { Query q = session.createSQLQuery("SELECT * FROM " + CLOSURE_TABLE_NAME + " WHERE 1=0 FOR UPDATE"); q.list(); } else if (isOracle()) { Query q = session.createSQLQuery("LOCK TABLE " + CLOSURE_TABLE_NAME + " IN EXCLUSIVE MODE"); q.executeUpdate(); } else if (isPostgreSQL()) { // currently not used Query q = session.createSQLQuery("LOCK TABLE " + CLOSURE_TABLE_NAME + " IN EXCLUSIVE MODE"); q.executeUpdate(); } else if (isSQLServer()) { Query q = session.createSQLQuery("SELECT count(*) FROM " + CLOSURE_TABLE_NAME + " WITH (TABLOCK, XLOCK)"); q.list(); } LOGGER.trace("...locked in {} ms", System.currentTimeMillis()-start); } // returns table name private String computeDeltaTable(List<Edge> edges, Context context, Session session) { if (edges.isEmpty()) { throw new IllegalArgumentException("No edges to add/remove"); } String deltaTempTableName; if (context.temporaryTableName != null) { deltaTempTableName = context.temporaryTableName; // table was created on the beginning of trasaction } else if (isOracle()) { deltaTempTableName = TEMP_DELTA_TABLE_NAME_FOR_ORACLE; // table definition is global } else { deltaTempTableName = generateDeltaTempTableName(); // table will be created now } if (COUNT_CLOSURE_RECORDS && LOGGER.isTraceEnabled()) { Query q = session.createSQLQuery("select count(*) from " + CLOSURE_TABLE_NAME); List list = q.list(); LOGGER.trace("OrgClosure has {} rows", list.toString()); } long start; int count; String selectClause = "select t1.descendant_oid as descendant_oid, t2.ancestor_oid as ancestor_oid, " + "sum(t1.val*t2.val) as val " + "from " + CLOSURE_TABLE_NAME + " t1, " + CLOSURE_TABLE_NAME + " t2 " + "where " + getWhereClause(edges) + " " + "group by t1.descendant_oid, t2.ancestor_oid"; if (isSQLServer()) { // we create the table manually, because we want to have an index on it, and // with serializable transactions it is not possible to create index within the transaction (after inserting data) start = System.currentTimeMillis(); final String createTableSql = "create table " + deltaTempTableName + " (" + "descendant_oid NVARCHAR(36) COLLATE database_default, " + "ancestor_oid NVARCHAR(36) COLLATE database_default, " + "val INT, " + "PRIMARY KEY (descendant_oid, ancestor_oid))"; // Query createTableQuery = session.createSQLQuery(createTableSql); // createTableQuery.executeUpdate(); <--- this does not work because the temporary table gets deleted when the command terminates (preparedStatement issue - maybe something like this: https://support.microsoft.com/en-us/kb/280134 ?) session.doWork(new Work() { @Override public void execute(Connection connection) throws SQLException { connection.createStatement().execute(createTableSql); } }); if (LOGGER.isTraceEnabled()) LOGGER.trace("Empty delta table created in {} ms", System.currentTimeMillis() - start); Query insertQuery = session.createSQLQuery("insert into " + deltaTempTableName + " " + selectClause); start = System.currentTimeMillis(); count = insertQuery.executeUpdate(); } else { String createTablePrefix; if (isPostgreSQL()) { createTablePrefix = "create local temporary table " + deltaTempTableName + " on commit drop as "; } else if (isH2()) { // todo skip if this is first in this transaction Query q = session.createSQLQuery("delete from " + deltaTempTableName); int c = q.executeUpdate(); LOGGER.trace("Deleted {} rows from temporary table {}", c, deltaTempTableName); createTablePrefix = "insert into " + deltaTempTableName + " "; } else if (isMySQL() || isMariaDb()) { createTablePrefix = "create temporary table " + deltaTempTableName + " engine=memory as "; // engine=memory is questionable because of missing tansactionality (but the transactionality is needed in the main table, not the delta table...) } else if (isOracle()) { // todo skip if this is first in this transaction Query q = session.createSQLQuery("delete from " + deltaTempTableName); int c = q.executeUpdate(); LOGGER.trace("Deleted {} rows from temporary table {}", c, deltaTempTableName); createTablePrefix = "insert into " + deltaTempTableName + " "; } else { throw new UnsupportedOperationException("Org. closure manager - unsupported database operation"); } Query query1 = session.createSQLQuery(createTablePrefix + selectClause); start = System.currentTimeMillis(); count = query1.executeUpdate(); } if (LOGGER.isTraceEnabled()) LOGGER.trace("Added {} records to temporary delta table {} ({} ms).", new Object[] {count, deltaTempTableName, System.currentTimeMillis()-start}); if (isPostgreSQL()) { start = System.currentTimeMillis(); Query qIndex = session.createSQLQuery("CREATE INDEX " + deltaTempTableName + "_idx " + " ON " + deltaTempTableName + " USING btree " + " (descendant_oid, ancestor_oid)"); qIndex.executeUpdate(); if (LOGGER.isTraceEnabled()) LOGGER.trace("Index created in {} ms", System.currentTimeMillis()-start); } // TODO index for MySQL !!! if (DUMP_TABLES) dumpOrgClosureTypeTable(session, CLOSURE_TABLE_NAME); if (DUMP_TABLES) dumpOrgClosureTypeTable(session, deltaTempTableName); // TODO drop delta table in case of exception return deltaTempTableName; } private String generateDeltaTempTableName() { String deltaTempTableName; deltaTempTableName = (isSQLServer()?"#":"") + "m_org_closure_delta_" + System.currentTimeMillis() + "_" + ((int) (Math.random() * 10000000.0)); return deltaTempTableName; } private String getWhereClause(List<Edge> edges) { StringBuilder whereClause = new StringBuilder(); boolean first = true; for (Edge edge : edges) { if (first) { first = false; } else { whereClause.append(" or "); } checkOidSyntax(edge.getDescendant()); checkOidSyntax(edge.getAncestor()); whereClause.append("(t1.ancestor_oid = '").append(edge.getTail()).append("'"); whereClause.append(" and t2.descendant_oid = '").append(edge.getHead()).append("')"); } return whereClause.toString(); } private void dumpOrgClosureTypeTable(Session session, String tableName) { Query q = session.createSQLQuery("select descendant_oid, ancestor_oid, val from " + tableName) .addScalar("descendant_oid", StringType.INSTANCE) .addScalar("ancestor_oid", StringType.INSTANCE) .addScalar("val", IntegerType.INSTANCE); List<Object[]> list = q.list(); LOGGER.trace("{} ({} rows):", tableName, list.size()); for (Object[] row : list) { LOGGER.trace(" - [d={}, a={}, val={}]", row); } } private void initializeOracleTemporaryTable() { Session session = baseHelper.getSessionFactory().openSession(); Query qCheck = session.createSQLQuery("select table_name from user_tables where table_name = upper('" + TEMP_DELTA_TABLE_NAME_FOR_ORACLE + "')"); if (qCheck.list().isEmpty()) { LOGGER.info("Creating temporary table {}", TEMP_DELTA_TABLE_NAME_FOR_ORACLE); session.beginTransaction(); Query qCreate = session.createSQLQuery("CREATE GLOBAL TEMPORARY TABLE " + TEMP_DELTA_TABLE_NAME_FOR_ORACLE + " (descendant_oid VARCHAR2(36 CHAR), " + " ancestor_oid VARCHAR2(36 CHAR), " + " val NUMBER (10, 0), " + " PRIMARY KEY (descendant_oid, ancestor_oid)) " + " ON COMMIT DELETE ROWS"); try { qCreate.executeUpdate(); session.getTransaction().commit(); } catch (RuntimeException e) { String m = "Couldn't create temporary table " + TEMP_DELTA_TABLE_NAME_FOR_ORACLE + ". Please create the table manually."; LoggingUtils.logException(LOGGER, m, e); throw new SystemException(m, e); } } session.close(); } private List<ReferenceDelta> filterParentRefDeltas(Collection<? extends ItemDelta> modifications) { boolean containsAdd = false, containsDelete = false, containsReplace = false; List<ReferenceDelta> deltas = new ArrayList<>(); if (modifications == null) { return deltas; } for (ItemDelta delta : modifications) { if (!QNameUtil.match(ObjectType.F_PARENT_ORG_REF, delta.getElementName())) { // TODO is this OK? continue; } if (delta.isAdd()) { containsAdd = true; } if (delta.isDelete()) { containsDelete = true; } if (delta.isReplace()) { if (containsReplace) { throw new IllegalStateException("Unsupported combination of parentOrgRef operations: more REPLACE ItemDeltas"); } containsReplace = true; } deltas.add((ReferenceDelta) delta); } if (containsReplace && (containsAdd || containsDelete)) { throw new IllegalStateException("Unsupported combination of parentOrgRef operations: REPLACE with either ADD or DELETE"); } return deltas; } private Set<String> getParentOidsToDelete(Collection<? extends ItemDelta> modifications, PrismObject<? extends ObjectType> originalObject) { Validate.notNull(originalObject); Set<String> oids = new HashSet<>(); Set<String> existingOids = getParentOidsFromObject(originalObject); for (ItemDelta delta : modifications) { if (delta.getValuesToDelete() != null) { for (PrismReferenceValue val : (Collection<PrismReferenceValue>) delta.getValuesToDelete()) { String oid = val.getOid(); if (existingOids.contains(oid)) { // if it's not there, we do not want to delete it! oids.add(oid); } } } if (delta.getValuesToReplace() != null) { // at this point we can assume this is not mixed with DELETE or ADD deltas // we mark all existing values not present in the new set as being removed! oids = new HashSet<>(); // and we do this for the latest REPLACE ItemDelta for (String existingOid : existingOids) { boolean found = false; for (PrismReferenceValue newVal : (Collection<PrismReferenceValue>) delta.getValuesToReplace()) { if (existingOid.equals(newVal.getOid())) { found = true; break; } } if (!found) { oids.add(existingOid); // if existing OID was not found in values to REPLACE, it should be deleted } } } } return oids; } // filters out those OIDs that are already present in originalObject (beware, it may be null) private Set<String> getParentOidsToAdd(Collection<? extends ItemDelta> modifications, PrismObject<? extends ObjectType> originalObject) { Set<String> oids = new HashSet<>(); Set<String> existingOids = getParentOidsFromObject(originalObject); for (ItemDelta delta : modifications) { if (delta.getValuesToAdd() != null) { for (PrismReferenceValue val : (Collection<PrismReferenceValue>) delta.getValuesToAdd()) { String oid = val.getOid(); if (!existingOids.contains(oid)) { // if it's already there, we don't want to add it oids.add(oid); } } } if (delta.getValuesToReplace() != null) { // at this point we can assume this is not mixed with DELETE or ADD deltas // we mark all 'new' values in REPLACE delta not present in existing object as being added oids = new HashSet<>(); // and we do this for the latest REPLACE ItemDelta for (PrismReferenceValue val : (Collection<PrismReferenceValue>) delta.getValuesToReplace()) { String oid = val.getOid(); if (!existingOids.contains(oid)) { // if it's already there, we don't want to add it oids.add(oid); } } } } return oids; } private Set<String> getParentOidsFromObject(PrismObject<? extends ObjectType> originalObject) { Set<String> retval = new HashSet<>(); if (originalObject != null) { for (ObjectReferenceType ort : originalObject.asObjectable().getParentOrgRef()) { retval.add(ort.getOid()); } // // is this really necessary? (no, and actually, it is harmful) // for (OrgType org : originalObject.asObjectable().getParentOrg()) { // retval.add(org.getOid()); // } } return retval; } // only for performance testing (doesn't account for multithreading!) public long getLastOperationDuration() { return lastOperationDuration; } private <T extends ObjectType> boolean isTypeNonLeaf(Class<T> type) { return OrgType.class.equals(type); } private List<String> getParents(String oid, Session session) { Query parentsQuery = session.createQuery("select distinct targetOid from RObjectReference where ownerOid=:oid and referenceType=0"); parentsQuery.setString("oid", oid); return parentsQuery.list(); } private List<String> getChildren(String oid, Session session) { Query childrenQuery = session.createQuery("select distinct parentRef.ownerOid from RObjectReference as parentRef" + " join parentRef.owner as owner where parentRef.targetOid=:oid and parentRef.referenceType=0" + " and owner.objectTypeClass = :orgType"); childrenQuery.setParameter("orgType", RObjectType.ORG); // TODO eliminate use of parameter here childrenQuery.setString("oid", oid); return childrenQuery.list(); } private List<String> retainExistingOids(Collection<String> oids, Session session) { if (!oids.isEmpty()) { Query query = session.createQuery("select o.oid from RObject o where o.oid in (:oids)"); query.setParameterList("oids", oids); return query.list(); } else { return new ArrayList<String>(); } } private boolean isMariaDb() { return baseHelper.getConfiguration().isUsingMariaDB(); } private boolean isMySQL() { return baseHelper.getConfiguration().isUsingMySQL(); } private boolean isOracle() { return baseHelper.getConfiguration().isUsingOracle(); } private boolean isSQLServer() { return baseHelper.getConfiguration().isUsingSQLServer(); } private boolean isH2() { return baseHelper.getConfiguration().isUsingH2(); } private boolean isPostgreSQL() { return baseHelper.getConfiguration().isUsingPostgreSQL(); } //endregion //region Helper classes public static class Edge { private String descendant; // i.e. child, or technically, edge tail private String ancestor; // i.e. parent, or technically, edge head public Edge(String descendant, String ancestor) { this.descendant = descendant; this.ancestor = ancestor; } public String getDescendant() { return descendant; } public String getAncestor() { return ancestor; } public String getTail() { return descendant; } public String getHead() { return ancestor; } public String toString() { return descendant + "->" + ancestor; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Edge edge = (Edge) o; if (!ancestor.equals(edge.ancestor)) return false; if (!descendant.equals(edge.descendant)) return false; return true; } @Override public int hashCode() { int result = descendant.hashCode(); result = 31 * result + ancestor.hashCode(); return result; } } public static enum Operation {ADD, DELETE, MODIFY} public static enum StartupAction { NONE("none"), CHECK("check"), REBUILD_IF_NEEDED("rebuildIfNeeded"), ALWAYS_REBUILD("alwaysRebuild"); private String value; StartupAction(String value) { this.value = value; } @Override public String toString() { return value; } public static StartupAction fromValue(String v) { for (StartupAction a: StartupAction.values()) { if (a.value.equals(v)) { return a; } } throw new IllegalArgumentException(v); } } public static class Context { String temporaryTableName; } //endregion // private void lockClosureTableIfNeeded(Session session) { // if (!getConfiguration().isUsingH2()) { // return; // } // // long start = System.currentTimeMillis(); // LOGGER.info("Locking closure table"); // if (getConfiguration().isUsingH2()) { // //Query q = session.createSQLQuery("SELECT * FROM " + OrgClosureManager.CLOSURE_TABLE_NAME + " WHERE 1=0 FOR UPDATE"); // Query q = session.createSQLQuery("SELECT * FROM m_connector_host WHERE 1=0 FOR UPDATE"); // q.list(); // } // LOGGER.info("...locked in {} ms", System.currentTimeMillis()-start); // // } }