/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * * Portions copyright 2011-2015 ForgeRock AS. */ package org.forgerock.openidm.sync.impl; import static org.forgerock.json.resource.QueryRequest.FIELD_QUERY_FILTER; // Java Standard Edition import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.forgerock.services.context.Context; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; // SLF4J import org.forgerock.json.resource.ConnectionFactory; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; import org.forgerock.json.resource.Requests; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.openidm.util.RequestUtil; import org.forgerock.util.query.QueryFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // TODO: Extend from something like FieldMap to handle the Java ↔ JSON translations. // SLF4J // JSON Fluent library // OpenIDM /** * Uni-directional view of a link. * * Link Types and Links in the repository are bi-directional. * * This view represents one direction of that Link to match the direction of the * current mapping context (source/target object set). * */ class Link { private final static Logger LOGGER = LoggerFactory.getLogger(Link.class); // The mapping associated with this link view. // This link view is specific to the direction of this mapping context private final ObjectMapping mapping; // The unique identifier of the link public String _id; // The MVCC revision of the link public String _rev; // The id linked in the source object set of the mapping. // This link view is specific to the direction of the mapping context public String sourceId; // The id linked in the target object set of the mapping. // This link view is specific to the direction of the mapping context public String targetId; // Whether this link representation has been initialized. // Once initialized is true, _id == null can be interpreted as a link that doesn't exist in our repository yet public boolean initialized = false; /** * Default link qualifier if no link qualifier was specified. */ public static final String DEFAULT_LINK_QUALIFIER = "default"; /** * Link qualifier. */ public String linkQualifier; /** * TODO: Description. * * @param mapping TODO. */ public Link(ObjectMapping mapping) { this.mapping = mapping; } /** * Sets the linkQualifier. * @param linkQualifier return linkQualifier */ void setLinkQualifier(String linkQualifier) { this.linkQualifier = linkQualifier; } /** * For a local link identifier this creates an identifier for the link stored in the repository. * @param id the local (unqualified) link identifier * @return the qualified id, qualified to the repository */ private static String linkId(String id) { //StringBuilder sb = new StringBuilder("repo/link/").append(mapping.getLinkType().getName()); StringBuilder sb = new StringBuilder("repo/link"); if (id != null) { sb.append('/').append(id); } return sb.toString(); } /** * Queries a single link and populates the object with its settings * * @param The query parameters * @throws SynchronizationException if getting and initializing the link details fail */ private void getLink(JsonValue query) throws SynchronizationException { JsonValue results = linkQuery(mapping.getService().getContext(), mapping.getService().getConnectionFactory(), query); if (results.size() == 1) { fromJsonValue(results.get(0)); } else if (results.size() > 1) { // shouldn't happen if index is unique throw new SynchronizationException("More than one link found"); } } /** * Issues a query on link(s) * * @param context the Context chain for the request * @param connectionFactory the connection factory * @param query the query parameters * @return The query results * @throws SynchronizationException if getting and initializing the link details fail */ private static JsonValue linkQuery(Context context, ConnectionFactory connectionFactory, JsonValue query) throws SynchronizationException { JsonValue results = null; try { final Collection<Map<String, Object>> result = new ArrayList<Map<String, Object>>(); QueryRequest request = RequestUtil.buildQueryRequestFromParameterMap(linkId(null), query.asMap()); connectionFactory.getConnection().query(context, request, new QueryResourceHandler() { @Override public boolean handleResource(ResourceResponse resource) { result.add(resource.getContent().asMap()); return true; } }); results = new JsonValue(result).required().expect(List.class); } catch (JsonValueException jve) { throw new SynchronizationException("Malformed link query response", jve); } catch (ResourceException ose) { throw new SynchronizationException("Link query failed", ose); } return results; } /** * TODO: Description. * * @param value TODO. * @throws org.forgerock.json.fluent.JsonValueException */ private void fromJsonValue(JsonValue jv) throws JsonValueException { _id = jv.get("_id").required().asString(); _rev = jv.get("_rev").asString(); // optional if (mapping.getLinkType().useReverse()) { sourceId = jv.get("secondId").required().asString(); targetId = jv.get("firstId").required().asString(); } else { sourceId = jv.get("firstId").required().asString(); targetId = jv.get("secondId").required().asString(); } linkQualifier = jv.get("linkQualifier").asString(); sourceId = mapping.getLinkType().normalizeSourceId(sourceId); targetId = mapping.getLinkType().normalizeTargetId(targetId); initialized = true; } /** * Transforms the current instance of this class into a JsonValue object. * * @return JsonValue object of this current instance. */ private JsonValue toJsonValue() { JsonValue jv = new JsonValue(new HashMap<String, Object>()); sourceId = mapping.getLinkType().normalizeSourceId(sourceId); targetId = mapping.getLinkType().normalizeTargetId(targetId); jv.put("linkType", mapping.getLinkType().getName()); jv.put("linkQualifier", linkQualifier); if (mapping.getLinkType().useReverse()) { jv.put("secondId", sourceId); jv.put("firstId", targetId); } else { jv.put("firstId", sourceId); jv.put("secondId", targetId); } return jv; } /** * Clear link. */ void clear() { this._id = null; this._rev = null; this.sourceId = null; this.targetId = null; } /** * Gets the link for a given object mapping source * * @param aSourceId the object mapping source system identifier * @throws SynchronizationException if the query could not be performed. */ void getLinkForSource(String aSourceId) throws SynchronizationException { aSourceId = mapping.getLinkType().normalizeSourceId(aSourceId); if (mapping.getLinkType().useReverse()) { getLinkFromSecond(aSourceId); } else { getLinkFromFirst(aSourceId); } } /** * Queries the links for a match on the first system (links can be bi-directional) * <p> * This method expects a {@code "links-for-sourceId"} defined with a parameter of * {@code "sourceId"}. * * @param id The ID to look up the links * @throws SynchronizationException if the query could not be performed. */ private void getLinkFromFirst(String id) throws SynchronizationException { clear(); if (id != null) { JsonValue query = new JsonValue(new HashMap<String, Object>()); query.put(FIELD_QUERY_FILTER, QueryFilter.and(Arrays.asList( QueryFilter.equalTo("/linkType", mapping.getLinkType().getName()), QueryFilter.equalTo("/linkQualifier", linkQualifier), QueryFilter.equalTo("/firstId", id))) .toString()); getLink(query); } } /** * Gets the link for a given object mapping source * * @param targetId the object mapping target system identifier * @throws SynchronizationException if the query could not be performed. */ void getLinkForTarget(String aTargetId) throws SynchronizationException { aTargetId = mapping.getLinkType().normalizeTargetId(aTargetId); if (mapping.getLinkType().useReverse()) { getLinkFromFirst(aTargetId); } else { getLinkFromSecond(aTargetId); } } /** * Queries the links for a match on the second system (links can be bi-directional) * <p> * This method expects a {@code "links-for-targetId"} defined with a parameter of * {@code "targetId"}. * * @param targetId TODO. * @throws SynchronizationException TODO. */ void getLinkFromSecond(String id) throws SynchronizationException { clear(); if (id != null) { JsonValue query = new JsonValue(new HashMap<String, Object>()); query.put(FIELD_QUERY_FILTER, QueryFilter.and(Arrays.asList( QueryFilter.equalTo("/linkType", mapping.getLinkType().getName()), QueryFilter.equalTo("/linkQualifier", linkQualifier), QueryFilter.equalTo("/secondId", id))) .toString()); getLink(query); } } /** * Queries all the links for a given mapping, indexed by the source identifier * <p> * This method expects a {@code "links-for-linkType"} defined with a parameter of * {@code "linkType"}. * * @param mapping the mapping to look up the links for * @throws SynchronizationException if the query could not be performed. * @return the mapping from source identifier to the link object for it */ public static Map<String, Link> getLinksForMapping(ObjectMapping mapping, String linkQualifier) throws SynchronizationException { Map<String, Link> sourceIdToLink = new ConcurrentHashMap<String, Link>(); if (mapping != null) { JsonValue query = new JsonValue(new HashMap<String, Object>()); query.put(FIELD_QUERY_FILTER, QueryFilter.and(Arrays.asList( QueryFilter.equalTo("/linkType", mapping.getLinkType().getName()), QueryFilter.equalTo("/linkQualifier", linkQualifier))) .toString()); JsonValue queryResults = linkQuery(mapping.getService().getContext(), mapping.getService().getConnectionFactory(), query); for (JsonValue entry : queryResults) { Link link = new Link(mapping); link.fromJsonValue(entry); sourceIdToLink.put(link.sourceId, link); } } return sourceIdToLink; } /** Compares the given Id to the current targetId, * taking into account the settings for case sensitivity * @param compareTargetId The target id to compare * @return true if the given Id is considered equivalent to the current target id */ public boolean targetEquals(String compareTargetId) { String normalizedCompId = mapping.getLinkType().normalizeTargetId(compareTargetId); String normalizedTargetId = mapping.getLinkType().normalizeTargetId(targetId); if (normalizedTargetId != null) { return normalizedTargetId.equals(normalizedCompId); } else { return normalizedTargetId == normalizedCompId; } } /** * TODO: Description. * * @throws SynchronizationException TODO. */ void create() throws SynchronizationException { _id = UUID.randomUUID().toString(); // client-assigned identifier JsonValue jv = toJsonValue(); try { CreateRequest r = Requests.newCreateRequest(linkId(null), _id, jv); ResourceResponse resource = mapping.getService().getConnectionFactory().getConnection().create(mapping.getService().getContext(), r); this._id = resource.getId(); this._rev = resource.getRevision(); this.initialized = true; } catch (ResourceException ose) { LOGGER.debug("Failed to create link", ose); throw new SynchronizationException(ose); } } /** * TODO: Description. * * @throws SynchronizationException TODO. */ void delete() throws SynchronizationException { if (_id != null) { // forgiving delete try { DeleteRequest r = Requests.newDeleteRequest(linkId(_id)); r.setRevision(_rev); mapping.getService().getConnectionFactory().getConnection().delete(mapping.getService().getContext(),r); } catch (ResourceException ose) { LOGGER.warn("Failed to delete link", ose); throw new SynchronizationException(ose); } clear(); } } /** * Update a link. * * @throws SynchronizationException if updating link fails */ void update() throws SynchronizationException { if (_id == null) { throw new SynchronizationException("Attempt to update non-existent link"); } JsonValue jv = toJsonValue(); try { UpdateRequest r = Requests.newUpdateRequest(linkId(_id), jv); r.setRevision(_rev); ResourceResponse resource = mapping.getService().getConnectionFactory().getConnection().update(mapping.getService().getContext(),r); _rev = resource.getRevision(); } catch (ResourceException ose) { LOGGER.warn("Failed to update link", ose); throw new SynchronizationException(ose); } } }