001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.client; 007 008import java.net.URI; 009import java.util.ArrayList; 010import java.util.HashMap; 011import java.util.List; 012import java.util.Map; 013import java.util.StringTokenizer; 014 015/** 016 * A class representing the value of an HTTP Link header 017 * 018 * @author Aaron Coburn 019 * @author bbpennel 020 */ 021public class FcrepoLink { 022 023 private static final String PARAM_DELIM = ";"; 024 025 private static final String META_REL = "rel"; 026 027 private static final String META_TYPE = "type"; 028 029 private URI uri; 030 031 private Map<String, String> params; 032 033 /** 034 * Create a representation of a Link header. 035 * 036 * @param link the value for a Link header 037 */ 038 public FcrepoLink(final String link) { 039 if (link == null) { 040 throw new IllegalArgumentException("Link header did not contain a URI"); 041 } 042 this.params = new HashMap<>(); 043 parse(link); 044 } 045 046 /** 047 * Construct a representation of a Link header from the given uri and parameters. 048 * 049 * @param uri URI portion of the link header 050 * @param params link parameters 051 */ 052 private FcrepoLink(final URI uri, final Map<String, String> params) { 053 this.uri = uri; 054 this.params = params; 055 } 056 057 /** 058 * Retrieve the URI of the link 059 * 060 * @return the URI portion of a Link header 061 */ 062 public URI getUri() { 063 return uri; 064 } 065 066 /** 067 * Retrieve the REL portion of the link 068 * 069 * @return the "rel" portion of a Link header 070 */ 071 public String getRel() { 072 return getParam(META_REL); 073 } 074 075 /** 076 * Retrieve the type portion of the link 077 * 078 * @return the "type" parameter of the header 079 */ 080 public String getType() { 081 return getParam(META_TYPE); 082 } 083 084 /** 085 * Retrieve a parameter from the link header 086 * 087 * @param name name of the parameter in the link header 088 * @return the value of the parameter or null if not present. 089 */ 090 public String getParam(final String name) { 091 return params.get(name); 092 } 093 094 /** 095 * Retrieve a map of parameters from the link header 096 * 097 * @return map of parameters 098 */ 099 public Map<String, String> getParams() { 100 return params; 101 } 102 103 /** 104 * Parse the value of a link header 105 */ 106 private void parse(final String link) { 107 final int paramIndex = link.indexOf(PARAM_DELIM); 108 if (paramIndex == -1) { 109 uri = getLinkPart(link); 110 } else { 111 uri = getLinkPart(link.substring(0, paramIndex)); 112 // Parse the remainder of the header after the URI as parameters 113 parseParams(link.substring(paramIndex + 1)); 114 } 115 } 116 117 private void parseParams(final String paramString) { 118 final StringTokenizer st = new StringTokenizer(paramString, ";\",", true); 119 while (st.hasMoreTokens()) { 120 // Read one parameter, until an unquoted ; is encountered or no more tokens 121 boolean inQuotes = false; 122 final StringBuilder paramBuilder = new StringBuilder(); 123 while (st.hasMoreTokens()) { 124 final String token = st.nextToken(); 125 if (token.equals("\"")) { 126 inQuotes = !inQuotes; 127 } else if (!inQuotes && token.equals(";")) { 128 break; 129 } else if (!inQuotes && token.equals(",")) { 130 throw new IllegalArgumentException("Cannot parse link, contains unterminated quotes"); 131 } else { 132 paramBuilder.append(token); 133 } 134 } 135 136 if (inQuotes) { 137 throw new IllegalArgumentException("Cannot parse link, contains unterminated quotes"); 138 } 139 140 final String param = paramBuilder.toString(); 141 final String[] components = param.split("=", 2); 142 if (components.length == 2) { 143 final String name = components[0].trim(); 144 final String value = components[1].trim(); 145 params.put(name, value); 146 } else { 147 throw new IllegalArgumentException( 148 "Cannot parse link, improperly structured parameter encountered: " + param); 149 } 150 } 151 } 152 153 private static String stripQuotes(final String value) { 154 if (value.startsWith("\"") && value.endsWith("\"")) { 155 return value.substring(1, value.length() - 1); 156 } 157 return value; 158 } 159 160 @Override 161 public String toString() { 162 final StringBuilder result = new StringBuilder(); 163 result.append('<').append(uri.toString()).append('>'); 164 165 params.forEach((name, value) -> { 166 result.append("; ").append(name).append("=\"").append(value).append('"'); 167 }); 168 return result.toString(); 169 } 170 171 /** 172 * Extract the URI part of the link header 173 */ 174 private static URI getLinkPart(final String uriPart) { 175 final String linkPart = uriPart.trim(); 176 if (!linkPart.startsWith("<") || !linkPart.endsWith(">")) { 177 throw new IllegalArgumentException("Link header did not contain a URI"); 178 } else { 179 return URI.create(linkPart.substring(1, linkPart.length() - 1)); 180 } 181 } 182 183 /** 184 * Create a new builder instance initialized from an existing URI represented as a string. 185 * 186 * @param uri URI which will be used to initialize the builder 187 * @return a new link builder. 188 * @throws IllegalArgumentException if uri is {@code null}. 189 */ 190 public static Builder fromUri(final String uri) { 191 if (uri == null) { 192 throw new IllegalArgumentException("URI is required"); 193 } 194 return new Builder().uri(uri); 195 } 196 197 /** 198 * Create a new builder instance initialized from an existing URI. 199 * 200 * @param uri URI which will be used to initialize the builder 201 * @return a new link builder. 202 * @throws IllegalArgumentException if uri is {@code null}. 203 */ 204 public static Builder fromUri(final URI uri) { 205 if (uri == null) { 206 throw new IllegalArgumentException("URI is required"); 207 } 208 return new Builder().uri(uri); 209 } 210 211 /** 212 * Simple parser to convert a link header containing a single link into an FcrepoLink object. 213 * 214 * @param link link header value. 215 * @return FcrepoLink representing the link 216 */ 217 public static FcrepoLink valueOf(final String link) { 218 return new FcrepoLink(link); 219 } 220 221 /** 222 * Parser which converts a link header containing one or more links into a list of FcrepoLink objects. 223 * 224 * @param headerValue link header value. 225 * @return List containing an FcrepoLink for each link in the header value. 226 */ 227 public static List<FcrepoLink> fromHeader(final String headerValue) { 228 final List<FcrepoLink> links = new ArrayList<>(); 229 230 // States which indicate that a "," is not currently a link delimiter 231 boolean inQuotes = false; 232 boolean inUri = false; 233 234 // Split the header into separate links and create FcrepoLink objects for each 235 // Allows for reserved characters to appear within quoted values or the URI 236 StringBuilder currentLink = new StringBuilder(); 237 final StringTokenizer st = new StringTokenizer(headerValue, ",\"<>", true); 238 while (st.hasMoreTokens()) { 239 final String token = st.nextToken(); 240 if (token.equals(",")) { 241 // Link delimiter, add current link to result and start next link 242 if (!inQuotes && !inUri) { 243 links.add(new FcrepoLink(currentLink.toString().trim())); 244 currentLink = new StringBuilder(); 245 continue; 246 } 247 } else if (token.equals("\"") && !inUri) { 248 inQuotes = !inQuotes; 249 } else if (token.equals("<") && !inQuotes) { 250 inUri = true; 251 } else if (token.equals(">") && !inQuotes) { 252 inUri = false; 253 } 254 255 // Accumulate tokens composing this link 256 currentLink.append(token); 257 } 258 259 if (inQuotes) { 260 throw new IllegalArgumentException("Cannot parse link header, contains unterminated quotes: " 261 + headerValue); 262 } 263 if (inUri) { 264 throw new IllegalArgumentException("Cannot parse link header, contains unterminated URI: " 265 + headerValue); 266 } 267 268 links.add(new FcrepoLink(currentLink.toString().trim())); 269 return links; 270 } 271 272 /** 273 * Builder class for link headers represented as FcrepoLinks 274 * 275 * @author bbpennel 276 */ 277 public static class Builder { 278 279 private URI uri; 280 281 private Map<String, String> params; 282 283 /** 284 * Construct a builder 285 */ 286 public Builder() { 287 this.params = new HashMap<>(); 288 } 289 290 /** 291 * Set the URI for this link 292 * 293 * @param uri URI for link 294 * @return this builder 295 */ 296 public Builder uri(final URI uri) { 297 this.uri = uri; 298 return this; 299 } 300 301 /** 302 * Set the URI for this link 303 * 304 * @param uri URI for link 305 * @return this builder 306 */ 307 public Builder uri(final String uri) { 308 this.uri = URI.create(uri); 309 return this; 310 } 311 312 /** 313 * Set a rel parameter for this link 314 * 315 * @param rel rel param value 316 * @return this builder 317 */ 318 public Builder rel(final String rel) { 319 return param(META_REL, rel); 320 } 321 322 /** 323 * Set a type parameter for this link 324 * 325 * @param type type param value 326 * @return this builder 327 */ 328 public Builder type(final String type) { 329 return param(META_TYPE, type); 330 } 331 332 /** 333 * Set an arbitrary parameter for this link 334 * 335 * @param name name of the parameter 336 * @param value value of the parameter 337 * @return this builder 338 */ 339 public Builder param(final String name, final String value) { 340 params.put(name, stripQuotes(value)); 341 return this; 342 } 343 344 /** 345 * Finish building this link. 346 * 347 * @return newly built link. 348 */ 349 public FcrepoLink build() { 350 return new FcrepoLink(uri, params); 351 } 352 } 353}