/* * Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.scvngr.levelup.core.util; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.scvngr.levelup.core.annotation.LevelUpApi; import com.scvngr.levelup.core.annotation.LevelUpApi.Contract; import net.jcip.annotations.Immutable; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * <p> * A regular expression -based parser implementing RFC5988. * </p> * <p> * This does not check the parameters against the list defined in the RFC. Instead it treats all * parameters as key/value pairs, allowing both quoted and unquoted values according to the grammar * outlined in the RFC. * </p> * * @see <a href="http://www.ietf.org/rfc/rfc5988.txt">RFC5988</a> */ @LevelUpApi(contract = Contract.INTERNAL) public final class LinkHeaderParser { /** * Relation. Value should be a single quoted or unquoted relation type, or a quoted list of * space-separated relation types. */ public static final String PARAMETER_REL = "rel"; /** * Link anchor. Value should be a quoted URI reference. */ public static final String PARAMETER_ANCHOR = "anchor"; /** * A reverse relation. The acceptable values are the same as {@link #PARAMETER_REL}. */ public static final String PARAMETER_REV = "rev"; /** * The language of the link. */ public static final String PARAMETER_HREFLANG = "hreflang"; /** * The media type, similar to \@media in CSS. */ public static final String PARAMETER_MEDIA = "media"; /** * The title of the resource. */ public static final String PARAMETER_TITLE = "title"; /** * The extended title of the resource. */ public static final String PARAMETER_TITLE_STAR = "title*"; /** * The MIME type of the resource. */ public static final String PARAMETER_TYPE = "type"; /** * the allowed characters below are from rfc5987 parmname. */ private static final String REGEX_PARMNAME = "[\\w!#$&+-.^`|~]+"; /** * From RFC5988. */ private static final String REGEX_PTOKEN = "[\\w!#$%&'()*+-./:<=>?@\\[\\]^`{|}~]+"; /** * A quoted string has one binding group in it. */ private static final String REGEX_QUOTED_STRING = "(?:\"([^\"]*)\")"; /** * An individual link parameter. */ private static final String REGEX_PARAMETER = "\\s*;\\s*(" + REGEX_PARMNAME + ")\\s*=\\s*(?:" + REGEX_QUOTED_STRING + "|(" + REGEX_PTOKEN + "))"; /** * Match the whole thing. */ private static final Pattern WEB_LINK = Pattern .compile("\\s*<\\s*([^>\\s]+)\\s*>((?:" + REGEX_PARAMETER + ")*)"); /** * Matches an individual parameter. */ private static final Pattern LINK_PARAMETER = Pattern.compile(REGEX_PARAMETER); /** * Parses the header value and extracts the link and its parameters. This is the equivalent of * passing {@code context=null} to {@link #parseLinkHeader(Uri, String)}. * * @param headerValue the input header value. This is the portion after "Link:". * * @return a {@link LinkHeaderParser.LinkHeader} representing the results of the parse. * @throws com.scvngr.levelup.core.util.LinkHeaderParser.MalformedLinkHeaderException if the * header could not be parsed. */ @NonNull public static LinkHeader parseLinkHeader(@NonNull final String headerValue) throws MalformedLinkHeaderException { return parseLinkHeader(null, headerValue); } /** * Parses the header value and extracts the link and its parameters, resolving the link relative * to the supplied context. Typically the context is the URL of the HTTP request. * * @param context the URI that any relative URIs will be resolved against. This can be null to * disable relative URI resolution. * * @param headerValue the input header value. This is the portion after "Link:". * * @return a {@link LinkHeaderParser.LinkHeader} representing the results of the parse. * @throws com.scvngr.levelup.core.util.LinkHeaderParser.MalformedLinkHeaderException if the header could not be parsed. */ @NonNull public static LinkHeader parseLinkHeader(@Nullable final Uri context, @NonNull final String headerValue) throws MalformedLinkHeaderException { final Matcher linkMatcher = WEB_LINK.matcher(headerValue); if (!linkMatcher.matches()) { throw new MalformedLinkHeaderException("could not parse web link header"); } final Uri link; if (null == context) { link = Uri.parse(linkMatcher.group(1)); } else { link = resolveRelativeUri(context, linkMatcher.group(1)); } final String parameterString = linkMatcher.group(2); final Matcher parameterMatcher = LINK_PARAMETER.matcher(parameterString); final HashMap<String, String> parameters = new HashMap<String, String>(); while (parameterMatcher.find()) { parameters.put(parameterMatcher.group(1), parameterMatcher.group(2)); } return new LinkHeader(link, parameters); } /** * <p> * Resolves the given target URI in the context of the context URI. * </p> * * <p> * Implementation note: The resolution of relative URIs is relatively expensive, as it must be * converted to/from URI/Uri. * </p> * * @param context the context within which to resolve the relative URI. * @param target the target URI, which can be either relative or absolute. * @return the target URI made relative to the context, or the original target URI if it is * already absolute. * @throws com.scvngr.levelup.core.util.LinkHeaderParser.MalformedLinkHeaderException if there is a malformed URI. */ @NonNull private static Uri resolveRelativeUri(@NonNull final Uri context, @NonNull final String target) throws MalformedLinkHeaderException { Uri targetUri = Uri.parse(target); if (!targetUri.isAbsolute()) { try { final URI contextURI = new URI(context.toString()); // ugly, but the only way to do it. targetUri = Uri.parse(contextURI.resolve(target).toASCIIString()); } catch (final URISyntaxException e) { final MalformedLinkHeaderException mwle = new MalformedLinkHeaderException("malformed URI"); mwle.initCause(e); throw mwle; } } return targetUri; } /** * An immutable representation of a "Link:" header. * * @see LinkHeaderParser */ @Immutable public static class LinkHeader { @NonNull private final Uri mLink; @NonNull private final Map<String, String> mParameters; /** * @param link the URI of the link * @param parameters the parameters that describe the link. This will be made immutable. */ public LinkHeader(@NonNull final Uri link, @NonNull final Map<String, String> parameters) { mLink = link; mParameters = NullUtils.nonNullContract(Collections.unmodifiableMap(parameters)); } /** * @return the link. */ @NonNull public final Uri getLink() { return mLink; } /** * @return the link parameters. */ @NonNull public final Map<String, String> getParameters() { return mParameters; } /** * @param name the name of the desired link parameter to return. * @return the value of the given parameter or {@code null} if there is no such parameter. */ @Nullable public final String getParameter(@NonNull final String name) { return mParameters.get(name); } @SuppressWarnings("null") // Generated method. @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mLink == null) ? 0 : mLink.hashCode()); result = prime * result + ((mParameters == null) ? 0 : mParameters.hashCode()); return result; } @SuppressWarnings({ "null", "unused" }) // Generated method. @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final LinkHeader other = (LinkHeader) obj; if (mLink == null) { if (other.mLink != null) { return false; } } else if (!mLink.equals(other.mLink)) { return false; } if (mParameters == null) { if (other.mParameters != null) { return false; } } else if (!mParameters.equals(other.mParameters)) { return false; } return true; } @Override public String toString() { return String.format("WebLink [mLink=%s, mParameters=%s]", mLink, mParameters); } } private LinkHeaderParser() { throw new UnsupportedOperationException("this class cannot be instantiated"); } /** * This is thrown if there is an error parsing the link. * * @see LinkHeaderParser * */ public static class MalformedLinkHeaderException extends Exception { /** * */ private static final long serialVersionUID = 2724907805863316257L; /** * @param message exception message */ public MalformedLinkHeaderException(final String message) { super(message); } } }