1 /* 2 * Copyright 2016, Google Inc. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 package com.google.api.pathtemplate; 32 33 import com.google.common.base.Preconditions; 34 import com.google.common.collect.ImmutableMap; 35 import com.google.common.collect.Sets; 36 import java.util.Collection; 37 import java.util.Map; 38 import java.util.Objects; 39 import java.util.Set; 40 import javax.annotation.Nullable; 41 42 /** 43 * Class for representing and working with resource names. 44 * 45 * <p>A resource name is represented by {@link PathTemplate}, an assignment to variables in the 46 * template, and an optional endpoint. The {@code ResourceName} class implements the map interface 47 * (unmodifiable) to work with the variable assignments, and has methods to reproduce the string 48 * representation of the name, to construct new names, and to dereference names into resources. 49 * 50 * <p>As a resource name essentially represents a match of a path template against a string, it can 51 * be also used for other purposes than naming resources. However, not all provided methods may make 52 * sense in all applications. 53 * 54 * <p>Usage examples: 55 * 56 * <pre>{@code 57 * PathTemplate template = PathTemplate.create("shelves/*/books/*"); 58 * TemplatedResourceName resourceName = TemplatedResourceName.create(template, "shelves/s1/books/b1"); 59 * assert resourceName.get("$1").equals("b1"); 60 * assert resourceName.parentName().toString().equals("shelves/s1/books"); 61 * }</pre> 62 */ 63 public class TemplatedResourceName implements Map<String, String> { 64 65 // ResourceName Resolver 66 // ===================== 67 68 /** Represents a resource name resolver which can be registered with this class. */ 69 public interface Resolver { 70 /** Resolves the resource name into a resource by calling the underlying API. */ resolve(Class<T> resourceType, TemplatedResourceName name, @Nullable String version)71 <T> T resolve(Class<T> resourceType, TemplatedResourceName name, @Nullable String version); 72 } 73 74 // The registered resource name resolver. 75 // TODO(wrwg): its a bit spooky to have this static global. Think of ways to 76 // configure this from the outside instead if programmatically (e.g. java properties). 77 private static volatile Resolver resourceNameResolver = 78 new Resolver() { 79 @Override 80 public <T> T resolve(Class<T> resourceType, TemplatedResourceName name, String version) { 81 throw new IllegalStateException( 82 "No resource name resolver is registered in ResourceName class."); 83 } 84 }; 85 86 /** 87 * Sets the resource name resolver which is used by the {@link #resolve(Class, String)} method. By 88 * default, no resolver is registered. 89 */ registerResourceNameResolver(Resolver resolver)90 public static void registerResourceNameResolver(Resolver resolver) { 91 resourceNameResolver = resolver; 92 } 93 94 // ResourceName 95 // ============ 96 97 /** 98 * Creates a new resource name based on given template and path. The path must match the template, 99 * otherwise null is returned. 100 * 101 * @throws ValidationException if the path does not match the template. 102 */ create(PathTemplate template, String path)103 public static TemplatedResourceName create(PathTemplate template, String path) { 104 Map<String, String> values = template.match(path); 105 if (values == null) { 106 throw new ValidationException("path '%s' does not match template '%s'", path, template); 107 } 108 return new TemplatedResourceName(template, values, null); 109 } 110 111 /** 112 * Creates a new resource name from a template and a value assignment for variables. 113 * 114 * @throws ValidationException if not all variables in the template are bound. 115 */ create(PathTemplate template, Map<String, String> values)116 public static TemplatedResourceName create(PathTemplate template, Map<String, String> values) { 117 if (!values.keySet().containsAll(template.vars())) { 118 Set<String> unbound = Sets.newLinkedHashSet(template.vars()); 119 unbound.removeAll(values.keySet()); 120 throw new ValidationException("unbound variables: %s", unbound); 121 } 122 return new TemplatedResourceName(template, values, null); 123 } 124 125 /** 126 * Creates a new resource name based on given template and path, where the path contains an 127 * endpoint. If the path does not match, null is returned. 128 */ 129 @Nullable createFromFullName(PathTemplate template, String path)130 public static TemplatedResourceName createFromFullName(PathTemplate template, String path) { 131 Map<String, String> values = template.matchFromFullName(path); 132 if (values == null) { 133 return null; 134 } 135 return new TemplatedResourceName(template, values, null); 136 } 137 138 private final PathTemplate template; 139 private final ImmutableMap<String, String> values; 140 private final String endpoint; 141 142 private volatile String stringRepr; 143 TemplatedResourceName( PathTemplate template, Map<String, String> values, String endpoint)144 private TemplatedResourceName( 145 PathTemplate template, Map<String, String> values, String endpoint) { 146 this.template = template; 147 this.values = ImmutableMap.copyOf(values); 148 this.endpoint = endpoint; 149 } 150 151 @Override toString()152 public String toString() { 153 if (stringRepr == null) { 154 stringRepr = template.instantiate(values); 155 } 156 return stringRepr; 157 } 158 159 @Override equals(Object obj)160 public boolean equals(Object obj) { 161 if (!(obj instanceof TemplatedResourceName)) { 162 return false; 163 } 164 TemplatedResourceName other = (TemplatedResourceName) obj; 165 return Objects.equals(template, other.template) 166 && Objects.equals(endpoint, other.endpoint) 167 && Objects.equals(values, other.values); 168 } 169 170 @Override hashCode()171 public int hashCode() { 172 return Objects.hash(template, endpoint, values); 173 } 174 175 /** Gets the template associated with this resource name. */ template()176 public PathTemplate template() { 177 return template; 178 } 179 180 /** Checks whether the resource name has an endpoint. */ hasEndpoint()181 public boolean hasEndpoint() { 182 return endpoint != null; 183 } 184 185 /** Returns the endpoint of this resource name, or null if none is defined. */ 186 @Nullable endpoint()187 public String endpoint() { 188 return endpoint; 189 } 190 191 /** Returns a resource name with specified endpoint. */ withEndpoint(String endpoint)192 public TemplatedResourceName withEndpoint(String endpoint) { 193 return new TemplatedResourceName(template, values, Preconditions.checkNotNull(endpoint)); 194 } 195 196 /** 197 * Returns the parent resource name. For example, if the name is {@code shelves/s1/books/b1}, the 198 * parent is {@code shelves/s1/books}. 199 */ parentName()200 public TemplatedResourceName parentName() { 201 PathTemplate parentTemplate = template.parentTemplate(); 202 return new TemplatedResourceName(parentTemplate, values, endpoint); 203 } 204 205 /** 206 * Returns true of the resource name starts with the parent resource name, i.e. is a child of the 207 * parent. 208 */ startsWith(TemplatedResourceName parentName)209 public boolean startsWith(TemplatedResourceName parentName) { 210 // TODO: more efficient implementation. 211 return toString().startsWith(parentName.toString()); 212 } 213 214 /** 215 * Attempts to resolve a resource name into a resource, by calling the associated API. The 216 * resource name must have an endpoint. An optional version can be specified to determine in which 217 * version of the API to call. 218 */ resolve(Class<T> resourceType, @Nullable String version)219 public <T> T resolve(Class<T> resourceType, @Nullable String version) { 220 Preconditions.checkArgument(hasEndpoint(), "Resource name must have an endpoint."); 221 return resourceNameResolver.resolve(resourceType, this, version); 222 } 223 224 // Map Interface 225 // ============= 226 227 @Override size()228 public int size() { 229 return values.size(); 230 } 231 232 @Override isEmpty()233 public boolean isEmpty() { 234 return values.isEmpty(); 235 } 236 237 @Override containsKey(Object key)238 public boolean containsKey(Object key) { 239 return values.containsKey(key); 240 } 241 242 @Override containsValue(Object value)243 public boolean containsValue(Object value) { 244 return values.containsValue(value); 245 } 246 247 @Override get(Object key)248 public String get(Object key) { 249 return values.get(key); 250 } 251 252 @Override 253 @Deprecated put(String key, String value)254 public String put(String key, String value) { 255 return values.put(key, value); 256 } 257 258 @Override 259 @Deprecated remove(Object key)260 public String remove(Object key) { 261 return values.remove(key); 262 } 263 264 @Override 265 @Deprecated putAll(Map<? extends String, ? extends String> m)266 public void putAll(Map<? extends String, ? extends String> m) { 267 values.putAll(m); 268 } 269 270 @Override 271 @Deprecated clear()272 public void clear() { 273 values.clear(); 274 } 275 276 @Override keySet()277 public Set<String> keySet() { 278 return values.keySet(); 279 } 280 281 @Override values()282 public Collection<String> values() { 283 return values.values(); 284 } 285 286 @Override entrySet()287 public Set<Entry<String, String>> entrySet() { 288 return values.entrySet(); 289 } 290 } 291