/* * 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.PrismObject; import com.evolveum.midpoint.prism.delta.ContainerDelta; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.path.*; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.prism.polystring.PrismDefaultPolyStringNormalizer; import com.evolveum.midpoint.prism.query.ObjectPaging; import com.evolveum.midpoint.repo.sql.data.common.RLookupTable; import com.evolveum.midpoint.repo.sql.data.common.RObject; import com.evolveum.midpoint.repo.sql.data.common.id.RContainerId; import com.evolveum.midpoint.repo.sql.data.common.other.RLookupTableRow; import com.evolveum.midpoint.repo.sql.util.RUtil; import com.evolveum.midpoint.schema.GetOperationOptions; import com.evolveum.midpoint.schema.ObjectSelector; import com.evolveum.midpoint.schema.RelationalValueSearchQuery; import com.evolveum.midpoint.schema.SelectorOptions; import com.evolveum.midpoint.util.QNameUtil; 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.LookupTableRowType; 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.Session; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.xml.namespace.QName; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Contains methods specific to handle LookupTable rows. * (As these rows are stored outside main LookupTable 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 lazyman, mederly */ @Component public class LookupTableHelper { private static final Trace LOGGER = TraceManager.getTrace(LookupTableHelper.class); @Autowired private GeneralHelper generalHelper; public void addLookupTableRows(Session session, RObject object, boolean deleteBeforeAdd) { if (!(object instanceof RLookupTable)) { return; } RLookupTable table = (RLookupTable) object; if (deleteBeforeAdd) { deleteLookupTableRows(session, table.getOid()); } if (table.getRows() != null) { for (RLookupTableRow row : table.getRows()) { session.save(row); } } } private void addLookupTableRows(Session session, String tableOid, Collection<PrismContainerValue> values, int currentId, boolean deleteBeforeAdd) throws SchemaException { for (PrismContainerValue value : values) { LookupTableRowType rowType = new LookupTableRowType(); rowType.setupContainerValue(value); if (deleteBeforeAdd) { deleteRowByKey(session, tableOid, rowType.getKey()); } RLookupTableRow row = RLookupTableRow.toRepo(tableOid, rowType); row.setId(currentId); currentId++; session.save(row); } } /** * This method removes all lookup table rows for object defined by oid * @param session * @param oid */ public void deleteLookupTableRows(Session session, String oid) { Query query = session.getNamedQuery("delete.lookupTableData"); query.setParameter("oid", oid); query.executeUpdate(); } public void updateLookupTableData(Session session, String tableOid, Collection<? extends ItemDelta> modifications) throws SchemaException { if (modifications.isEmpty()) { return; } for (ItemDelta delta : modifications) { if (delta.getPath().size() == 1) { // whole row add/delete/replace if (!(delta instanceof ContainerDelta)) { throw new IllegalStateException("Wrong table delta sneaked into updateLookupTableData: class=" + delta.getClass() + ", path=" + delta.getPath()); } // one "table" container modification ContainerDelta containerDelta = (ContainerDelta) delta; if (containerDelta.getValuesToDelete() != null) { // todo do 'bulk' delete like delete from ... where oid=? and id in (...) for (PrismContainerValue<LookupTableRowType> value : (Collection<PrismContainerValue>) containerDelta.getValuesToDelete()) { if (value.getId() != null) { deleteRowById(session, tableOid, value.getId()); } else if (value.asContainerable().getKey() != null) { deleteRowByKey(session, tableOid, value.asContainerable().getKey()); } else { // ignore (or throw an exception?) } } } if (containerDelta.getValuesToAdd() != null) { int currentId = generalHelper.findLastIdInRepo(session, tableOid, "get.lookupTableLastId") + 1; addLookupTableRows(session, tableOid, containerDelta.getValuesToAdd(), currentId, true); } if (containerDelta.getValuesToReplace() != null) { deleteLookupTableRows(session, tableOid); addLookupTableRows(session, tableOid, containerDelta.getValuesToReplace(), 1, false); } } else if (delta.getPath().size() == 3) { // row segment modification (structure is already checked) List<ItemPathSegment> segments = delta.getPath().getSegments(); Long rowId = ((IdItemPathSegment) segments.get(1)).getId(); QName name = ((NameItemPathSegment) segments.get(2)).getName(); RLookupTableRow row = (RLookupTableRow) session.get(RLookupTableRow.class, new RContainerId(RUtil.toInteger(rowId), tableOid)); LookupTableRowType rowType = row.toJAXB(); delta.setParentPath(ItemPath.EMPTY_PATH); delta.applyTo(rowType.asPrismContainerValue()); if (!QNameUtil.match(name, LookupTableRowType.F_LAST_CHANGE_TIMESTAMP)) { rowType.setLastChangeTimestamp(null); // in order to get filled in via toRepo call below } RLookupTableRow rowUpdated = RLookupTableRow.toRepo(tableOid, rowType); session.merge(rowUpdated); } } } private void deleteRowById(Session session, String tableOid, Long id) { Query query = session.getNamedQuery("delete.lookupTableDataRow"); query.setString("oid", tableOid); query.setInteger("id", RUtil.toInteger(id)); query.executeUpdate(); } private void deleteRowByKey(Session session, String tableOid, String key) { Query query = session.getNamedQuery("delete.lookupTableDataRowByKey"); query.setString("oid", tableOid); query.setString("key", key); query.executeUpdate(); } public GetOperationOptions findLookupTableGetOption(Collection<SelectorOptions<GetOperationOptions>> options) { final ItemPath tablePath = new ItemPath(LookupTableType.F_ROW); Collection<SelectorOptions<GetOperationOptions>> filtered = SelectorOptions.filterRetrieveOptions(options); for (SelectorOptions<GetOperationOptions> option : filtered) { ObjectSelector selector = option.getSelector(); if (selector == null) { // Ignore this. These are top-level options. There will not // apply to lookup table continue; } ItemPath selected = selector.getPath(); if (tablePath.equivalent(selected)) { return option.getOptions(); } } return null; } public <T extends ObjectType> void updateLoadedLookupTable(PrismObject<T> object, Collection<SelectorOptions<GetOperationOptions>> options, Session session) throws SchemaException { if (!SelectorOptions.hasToLoadPath(LookupTableType.F_ROW, options)) { return; } LOGGER.debug("Loading lookup table data."); GetOperationOptions getOption = findLookupTableGetOption(options); RelationalValueSearchQuery queryDef = getOption == null ? null : getOption.getRelationalValueSearchQuery(); Criteria criteria = setupLookupTableRowsQuery(session, queryDef, object.getOid()); if (queryDef != null && queryDef.getPaging() != null) { ObjectPaging paging = queryDef.getPaging(); if (paging.getOffset() != null) { criteria.setFirstResult(paging.getOffset()); } if (paging.getMaxSize() != null) { criteria.setMaxResults(paging.getMaxSize()); } ItemPath orderByPath = paging.getOrderBy(); if (paging.getDirection() != null && orderByPath != null && !orderByPath.isEmpty()) { if (orderByPath.size() > 1 || !(orderByPath.first() instanceof NameItemPathSegment) && !(orderByPath.first() instanceof IdentifierPathSegment)) { throw new SchemaException("OrderBy has to consist of just one naming or identifier segment"); } ItemPathSegment first = orderByPath.first(); String orderBy = first instanceof NameItemPathSegment ? ((NameItemPathSegment) first).getName().getLocalPart() : RLookupTableRow.ID_COLUMN_NAME; switch (paging.getDirection()) { case ASCENDING: criteria.addOrder(Order.asc(orderBy)); break; case DESCENDING: criteria.addOrder(Order.desc(orderBy)); break; } } } List<RLookupTableRow> rows = criteria.list(); if (rows == null || rows.isEmpty()) { return; } LookupTableType lookup = (LookupTableType) object.asObjectable(); List<LookupTableRowType> jaxbRows = lookup.getRow(); for (RLookupTableRow row : rows) { LookupTableRowType jaxbRow = row.toJAXB(); jaxbRows.add(jaxbRow); } } private Criteria setupLookupTableRowsQuery(Session session, RelationalValueSearchQuery queryDef, String oid) { Criteria criteria = session.createCriteria(RLookupTableRow.class); criteria.add(Restrictions.eq("ownerOid", oid)); if (queryDef != null && queryDef.getColumn() != null && queryDef.getSearchType() != null && StringUtils.isNotEmpty(queryDef.getSearchValue())) { String param = queryDef.getColumn().getLocalPart(); String value = queryDef.getSearchValue(); if (LookupTableRowType.F_LABEL.equals(queryDef.getColumn())) { param = "label.norm"; PolyString poly = new PolyString(value); poly.recompute(new PrismDefaultPolyStringNormalizer()); value = poly.getNorm(); } switch (queryDef.getSearchType()) { case EXACT: criteria.add(Restrictions.eq(param, value)); break; case STARTS_WITH: criteria.add(Restrictions.like(param, value + "%")); break; case SUBSTRING: criteria.add(Restrictions.like(param, "%" + value + "%")); } } return criteria; } public <T extends ObjectType> Collection<? extends ItemDelta> filterLookupTableModifications(Class<T> type, Collection<? extends ItemDelta> modifications) { Collection<ItemDelta> tableDelta = new ArrayList<>(); if (!LookupTableType.class.equals(type)) { return tableDelta; } ItemPath rowPath = new ItemPath(LookupTableType.F_ROW); for (ItemDelta delta : modifications) { ItemPath path = delta.getPath(); if (path.isEmpty()) { throw new UnsupportedOperationException("Lookup table cannot be modified via empty-path modification"); } else if (path.equivalent(rowPath)) { tableDelta.add(delta); } else if (path.isSuperPath(rowPath)) { // should be row[id]/xxx where xxx=key|value|label? List<ItemPathSegment> segments = path.getSegments(); if (segments.size() != 3 || !(segments.get(1) instanceof IdItemPathSegment) || !(segments.get(2) instanceof NameItemPathSegment)) { throw new UnsupportedOperationException("Unsupported modification path for lookup tables: " + path); } tableDelta.add(delta); } } modifications.removeAll(tableDelta); return tableDelta; } }