/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright © 2011 ForgeRock AS. All rights reserved. * * 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 * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" */ package org.forgerock.openidm.repo.orientdb.impl.query; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.forgerock.guava.common.base.Function; import org.forgerock.guava.common.collect.FluentIterable; import org.forgerock.json.resource.BadRequestException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // TODO: replace use of this class with TokenHandler in org.forgerock.openidm.repo.util public class TokenHandler { public static final String PREFIX_UNQUOTED = "unquoted"; public static final String PREFIX_DOTNOTATION = "dotnotation"; public static final String PREFIX_LIST = "list"; final static Logger logger = LoggerFactory.getLogger(TokenHandler.class); // The OpenIDM query token is of format ${token-name} private static final Pattern tokenPattern = Pattern.compile("\\$\\{(.+?)\\}"); /** {@link Function} to trim leading dot from orient object name */ private static final Function<String, String> TRIM_LEADING_DOT = new Function<String, String>() { @Override public String apply(String object) { return object.startsWith(".") ? object.substring(1) : object; } }; /** {@link Function} to convert json-pointer path-element to orient field */ private static final Function<String, String> JSON_POINTER_PATH_ELEMENT_TO_ORIENT_FIELD = new Function<String, String>() { @Override public String apply(String path) { // numeric path elements are actually array-indices - add brackets return (path.matches("[0-9]+")) ? "[" + path + "]" : path; } }; /** {@link Function} to convert JsonPointer to dot-notation orient object name */ private static final Function<String, String> JSON_POINTER_TO_DOT_NOTATION = new Function<String, String>() { @Override public String apply(String jsonPointer) { return TRIM_LEADING_DOT.apply( StringUtils.join( FluentIterable.from(Arrays.asList(jsonPointer.split("/"))) .transform(JSON_POINTER_PATH_ELEMENT_TO_ORIENT_FIELD), ".")); } }; /** * Replaces a query string with tokens of format ${token-name} with the values from the * passed in map, where the token-name must be the key in the map * * @param queryString the query with tokens * @param params the parameters to replace the tokens. Values can be String or List. * @return the query with all tokens replace with their found values * @throws BadRequestException if token in the query is not in the passed parameters */ String replaceTokensWithValues(String queryString, Map<String, String> params) throws BadRequestException { java.util.regex.Matcher matcher = tokenPattern.matcher(queryString); StringBuffer buffer = new StringBuffer(); while (matcher.find()) { String fullTokenKey = matcher.group(1); String tokenKey = fullTokenKey; String tokenPrefix = null; String[] tokenKeyParts = tokenKey.split(":", 2); // if prefix found if (tokenKeyParts.length == 2) { tokenPrefix = tokenKeyParts[0]; tokenKey = tokenKeyParts[1]; } if (!params.containsKey(tokenKey)) { // fail with an exception if token not found throw new BadRequestException("Missing entry in params passed to query for token " + tokenKey); } else { Object replacement = params.get(tokenKey); if (PREFIX_LIST.equals(tokenPrefix)) { // escape quotes, quote each element, and split on , replacement = Arrays.asList(("'" + replacement.toString().replaceAll("'", "\\\\'").replaceAll(",", "','") + "'").split(",")); } if (replacement instanceof List) { StringBuffer commaSeparated = new StringBuffer(); boolean first = true; for (Object entry : ((List) replacement)) { if (!first) { commaSeparated.append(","); } else { first = false; } commaSeparated.append(entry.toString()); } replacement = commaSeparated.toString(); } if (replacement == null) { replacement = ""; } // Optional control of representation via prefix if (tokenPrefix != null) { if (tokenPrefix.equals(PREFIX_UNQUOTED)) { // Leave replacement unquoted } else if (tokenPrefix.equals(PREFIX_DOTNOTATION)) { // Convert Json Pointer to OrientDB dot notation replacement = JSON_POINTER_TO_DOT_NOTATION.apply(replacement.toString()); } } else { // Default is single quoted string replacement (escaping single quotes in replacement) replacement = "'" + replacement.toString().replaceAll("'", "\\\\'") + "'"; } matcher.appendReplacement(buffer, ""); buffer.append(replacement); } } matcher.appendTail(buffer); return buffer.toString(); } /** * Replaces a query string with tokens of format ${token-name} with the * token format in OrientDB, which is of the form :token-name. * * OrientDB tokens has some limitations, e.g. they can currently only be used * in the where clause, and hence the returned string is not guaranteed to be * valid for use in a prepared statement. If the parsing fails the system may * have to fall back onto non-prepared statements and manual token replacement. * * @param queryString the query with OpenIDM format tokens ${token} * @return the query with all tokens replaced with the OrientDB style tokens :token * @throws PrepareNotSupported if this method knows a given statement can not be converted into a prepared statement. * That a statement was not rejected here though does not mean it could not fail during the parsing phase later. */ String replaceTokensWithOrientToken(String queryString) throws PrepareNotSupported { Matcher matcher = tokenPattern.matcher(queryString); StringBuffer buf = new StringBuffer(); while (matcher.find()) { String origToken = matcher.group(1); String tokenKey = origToken; String tokenPrefix = null; String[] tokenKeyParts = tokenKey.split(":", 2); // if prefix found if (tokenKeyParts.length == 2) { tokenPrefix = tokenKeyParts[0]; tokenKey = tokenKeyParts[1]; } matcher.appendReplacement(buf, ""); if (tokenPrefix != null && tokenPrefix.equals(PREFIX_DOTNOTATION)) { buf.append(JSON_POINTER_TO_DOT_NOTATION.apply(tokenKey)); } else if (tokenKey != null && tokenKey.length() > 0) { // OrientDB token is of format :token-name String newToken = ":" + tokenKey; buf.append(newToken); } } matcher.appendTail(buf); return buf.toString(); } }