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