package org.dcache.webdav.federation; import com.google.common.base.Splitter; import com.google.common.collect.Lists; import com.google.common.escape.Escaper; import com.google.common.net.PercentEscaper; import io.milton.http.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Strings.isNullOrEmpty; /** * This class represents the information passed by the client in the URL when * a request comes from the GlobalAccessService. The format is documented here: * <p> * https://svnweb.cern.ch/trac/lcgdm/wiki/Dpm/WebDAV/Extensions#GlobalAccessService * <p> * Format is that the query part of a URL contains a list key-value pairs; the * each key and value is joined by '=' and the key-value pairs are joined by * '&'s. This is the normal format used by web-forms when submitting results * via a GET request. * <p> * Certain keys are recognised and the values have the following semantics: * <pre> * rid the replica-id for the replica being requested, * forbidden a comma-list of replica-id for replicas that have been tried * and that would have failed with a 403 FORBIDDEN response, * notfound a comma-list of replicas-id for replicas that have been tried * and that would have failed with a 404 NOT FOUND response, * r<index> a comma-separated pair of replica-id and URL, respectively. * This item repeats a replica that is still to be attempted. * </pre> * <p> * For the {@literal r<index>} fields, {@literal <index>} is some * positive integer. The values of {@literal r<index>} do not repeat. * Considered together, all {@literal r<index>} fields represent a stack of * replicas that are still to be attempted, with {@literal r1} representing the * next replica. * <p> * Note that creating an object is a light-weight operation. The computational * effort of parsing the supplied information happens when {@code #hasNext} * method is called the first time. This call must happen before * {@code #buildLocationWhenNotFound} or {@code #buildLocationWhenForbidden} is * called. */ public class ReplicaInfo { private static final Logger LOG = LoggerFactory.getLogger(ReplicaInfo.class); private static final ReplicaInfo EMPTY_INFO = new ReplicaInfo(); private static final Splitter ON_FIRST_COMMA = Splitter.on(',') .trimResults().limit(2); // Equivalent to Guava's uriQueryStringEscaper(false), but this hasn't been // released yet. private static final Escaper QUERY_STRING_ESCAPER = new PercentEscaper("-._~!$'()*,;@:/?", false); // Used to escape the schema and path part of the redirected URI. private static final Escaper SCHEMA_AND_PATH_ESCAPER = new PercentEscaper("-._~!$'()*,;@:/", false); private final Map<String,String> _parameters; private boolean _isParsed; private String _nextReplica; private String _ourId; private List<String> _remainingReplicas = new ArrayList<>(); public static ReplicaInfo forRequest(Request request) { Map<String,String> parameters = request.getParams(); if (parameters == null) { return EMPTY_INFO; } String r1 = parameters.get("r1"); if (isNullOrEmpty(parameters.get("rid")) || isNullOrEmpty(r1) || r1.indexOf(',') == -1) { LOG.trace("returning empty QueryStringInfo for request"); return EMPTY_INFO; } else { LOG.trace("returning non-empty QueryStringInfo for request"); return new ReplicaInfo(request); } } private ReplicaInfo() { _parameters = null; _isParsed = true; } private ReplicaInfo(Request request) { _parameters = request.getParams(); } private void parseParameters() { _ourId = _parameters.get("rid"); String replica; for (int index = 1; (replica = _parameters.get("r"+index)) != null; index++) { if (_nextReplica == null) { _nextReplica = replica; } else { _remainingReplicas.add(replica); } } _isParsed = true; } public boolean hasNext() { if (!_isParsed) { parseParameters(); } return _ourId != null && _nextReplica != null; } private StringBuilder buildNextReplicaLocation() { StringBuilder sb = new StringBuilder(); List<String> nextReplica = Lists.newArrayList(ON_FIRST_COMMA.split(_nextReplica)); sb.append(SCHEMA_AND_PATH_ESCAPER.escape(nextReplica.get(1))); sb.append("?rid=").append(QUERY_STRING_ESCAPER.escape(nextReplica.get(0))); return sb; } private StringBuilder addRemainingReplicas(StringBuilder sb) { int index = 1; for(String url : this._remainingReplicas) { sb.append('&').append('r').append(index++); sb.append('=').append(QUERY_STRING_ESCAPER.escape(url)); } return sb; } public String buildLocationWhenNotFound() { checkState(_isParsed && _ourId != null && _nextReplica != null); StringBuilder sb = buildNextReplicaLocation(); sb.append(ampersandValueOrEmpty("forbidden")); sb.append('&').append(getAppendedField("notfound", _ourId)); return addRemainingReplicas(sb).toString(); } public String buildLocationWhenForbidden() { checkState(_isParsed && _ourId != null && _nextReplica != null); StringBuilder sb = buildNextReplicaLocation(); sb.append('&').append(getAppendedField("forbidden", _ourId)); sb.append(ampersandValueOrEmpty("notfound")); return addRemainingReplicas(sb).toString(); } private CharSequence ampersandValueOrEmpty(String name) { String item = _parameters.get(name); if (item == null) { return ""; } StringBuilder sb = new StringBuilder(); sb.append('&').append(name); sb.append('=').append(QUERY_STRING_ESCAPER.escape(item)); return sb; } private CharSequence getAppendedField(String name, String item) { StringBuilder sb = new StringBuilder(); sb.append(name).append('='); String existing = _parameters.get(name); if (existing != null) { sb.append(QUERY_STRING_ESCAPER.escape(existing)).append(','); } sb.append(item); return sb; } @Override public String toString() { if (_parameters == null) { return "<EMPTY>"; } if (!_isParsed) { return "<NOT PARSED>"; } return "ourId=" + _ourId + ", next="+ _nextReplica + ", remaining=" + _remainingReplicas; } }