package com.temenos.interaction.core.hypermedia; /* * #%L * interaction-springdsl * %% * Copyright (C) 2012 - 2016 Temenos Holdings N.V. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class provides a tree representation of a set of OData path template / http method tuples including handling path wildcards like /{id} and provides a * means of resolving a path to a path template allowing navigation from a path to an object associated with a given path template / http method tuple. * * @author mlambert */ public class PathTree { private static final Pattern PATH_PARAMETER_PATTERN = Pattern.compile("(.*)(?:\\()(.*)(?:\\))$"); private final Logger logger = LoggerFactory.getLogger(PathTree.class); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private class Node { String segment; Map<String, String> value = new HashMap<String,String>(); Map<String, Node> literals = new HashMap<String, Node>(); List<Node> variables = new LinkedList<Node>(); } private Node root; /** * Returns true if there are no OData paths in the tree otherwise false. * * @return true if there are no OData paths in the tree otherwise false. */ public boolean isEmpty() { Lock readLock = readWriteLock.readLock(); readLock.lock(); try { return root == null; } finally { readLock.unlock(); } } /** * Puts an OData path template / http method tuple in the tree, if the tuple already exists then the state name associated with it will be replaced. * * @param path * The OData path template part of the new tuple that will be put in the tree * * @param httpMethod * The http method part of the new tuple that will be put in the tree * * @param stateName * The state name to associate with the new tuple */ public void put(String path, String httpMethod, String stateName) { if("dynamic".equals(stateName)){ // Skip placeholder dynamic state return; } LinkedList<String> segments = new LinkedList<String>(Arrays.asList(path.split("/"))); Lock writeLock = readWriteLock.writeLock(); writeLock.lock(); try { if(root == null) { // All paths will be relative to "/" so this we be the root root = new Node(); String segment = "/"; root.segment = segment; } if(segments.isEmpty()) { root.value.put(httpMethod, stateName); } else { segments.remove(0); put(root, segments, httpMethod, stateName); } } finally { writeLock.unlock(); } } private void put(Node current, LinkedList<String> segments, String httpMethod, String stateName) { String segment = segments.remove(0); Node match = null; boolean variableSegment = false; Matcher m = PATH_PARAMETER_PATTERN.matcher(segment); if (m.find()) { // We are dealing with a path that contains (...) where ... may contain 0 or more characters String tmpSegment = m.group(2); if (!"".equals(tmpSegment)) { /* * TODO The current implementation internally expands /myResource('{id}')/modify to something that can be thought of as equivalent to * /myResource/{id}/modify; in future we may want to modify it to be /myResource/(/{id}/)/modify to reduce the chance of a collision * with another path such as /myResource/{other}/modify */ if (tmpSegment.charAt(0) == '\'' && tmpSegment.charAt(tmpSegment.length() - 1) == '\'') { // There are ' at the start and end of the segment - drop them tmpSegment = tmpSegment.substring(1, tmpSegment.length() - 1); } segments.addFirst(tmpSegment); } // Reduce the segment to non (...) section segment = m.group(1) == null ? "" : m.group(1); } if(segment.charAt(0) == '{' && segment.charAt(segment.length() - 1) == '}') { // The current segment represents a variable variableSegment = true; } if(variableSegment) { // We are dealing with a variable segment - check if this node already has this variable child for(Node variable: current.variables) { if(variable.segment.equals(segment)) { match = variable; break; } } if(match == null) { match = new Node(); match.segment = segment; current.variables.add(match); if(logger.isDebugEnabled()) { logger.debug("Adding " + match + " to " + segment); } } } if(!variableSegment) { // We a dealing with a literal segment - check if this node already has this literal child if(current.literals.containsKey(segment)) { match = current.literals.get(segment); } else { match = new Node(); match.segment = segment; current.literals.put(segment, match); if(logger.isDebugEnabled()) { logger.debug("Adding " + match + " to " + segment); } } } if(segments.isEmpty()) { match.value.put(httpMethod, stateName); } else { put(match, segments, httpMethod, stateName); } } /** * Gets the http method / state name pairs associated with the given OData path * * @param path The OData path to look up * * @return The http method / state name pairs associated with the given OData path */ public Map<String,String> get(String path) { List<String> segments = new LinkedList<String>(Arrays.asList(path.split("/"))); String segment = "/"; Lock readLock = readWriteLock.readLock(); readLock.lock(); try { if(root != null && root.segment != null && root.segment.equals(segment)) { if(!segments.isEmpty()) { segments.remove(0); } if(segments.isEmpty()) { return root.value; } else { return get(root, segments); } } else { return null; } } finally { readLock.unlock(); } } private Map<String,String> get(Node current, List<String> segments) { LinkedList<String> tmpSegments = new LinkedList<String>(); tmpSegments.addAll(segments); String segment = tmpSegments.get(0); tmpSegments.remove(0); Matcher matcher = PATH_PARAMETER_PATTERN.matcher(segment); if(matcher.find()) { // We are dealing with a path that contains (...) where ... may contain 0 or more characters String tmpSegment = matcher.group(2); if(!"".equals(tmpSegment)) { // Drop '(' and ')' characters and add the value as the next segment to process tmpSegments.addFirst(tmpSegment); } // Reduce the segment to non (...) section segment = matcher.group(1) == null ? "" : matcher.group(1); } if(current.literals.containsKey(segment)) { if(tmpSegments.isEmpty()) { return current.literals.get(segment).value; } else { return get(current.literals.get(segment), tmpSegments); } } else { Map<String,String> result = null; if(tmpSegments.isEmpty()) { if(current.variables.size() == 1) { return current.variables.get(0).value; } else { return null; } } else { for(Node variable: current.variables) { result = get(variable, tmpSegments); if(result != null) { break; } } } return result; } } /** * Removes an OData path template / http method tuple from the tree. * * @param path * The OData path template part of the tuple to remove from the tree * * @param httpMethod * The http method part of the tuple to remove from the tree */ public void remove(String path, String httpMethod) { Lock writeLock = readWriteLock.writeLock(); writeLock.lock(); try { Map<String,String> httpMethodToState = get(path); if(httpMethodToState == null) { throw new IllegalArgumentException("Path not found (" + path + ")"); } else { if(httpMethodToState.containsKey(httpMethod)) { // Remove the http method given from the set of http methods associated with the url httpMethodToState.remove(httpMethod); } else { throw new IllegalArgumentException("Method (" + httpMethod + ") not found for path (" + path + ")"); } } } finally { writeLock.unlock(); } } }