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.http.commons.domain;
019
020import static java.util.Arrays.asList;
021import static java.util.Optional.ofNullable;
022import static org.fcrepo.kernel.api.RdfLexicon.PREFER_CONTAINMENT;
023import static org.fcrepo.kernel.api.RdfLexicon.PREFER_MEMBERSHIP;
024import static org.fcrepo.kernel.api.RdfLexicon.PREFER_MINIMAL_CONTAINER;
025import static org.fcrepo.kernel.api.RdfLexicon.PREFER_SERVER_MANAGED;
026import static org.fcrepo.kernel.api.RdfLexicon.EMBED_CONTAINED;
027import static org.fcrepo.kernel.api.RdfLexicon.INBOUND_REFERENCES;
028
029import org.glassfish.jersey.message.internal.HttpHeaderReader;
030
031import javax.servlet.http.HttpServletResponse;
032
033import java.text.ParseException;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Optional;
038
039/**
040 * Parse a single prefer tag, value and any optional parameters
041 *
042 * @author cabeer
043 */
044public class PreferTag implements Comparable<PreferTag> {
045    private final String tag;
046    private String value = "";
047    private Map<String, String> params = new HashMap<>();
048
049    /**
050     * Create an empty PreferTag
051     * @return the empty PreferTag
052     */
053    public static PreferTag emptyTag() {
054        return new PreferTag((String)null);
055    }
056
057    /**
058     * Create a new PreferTag from an existing tag
059     * @param preferTag the preferTag
060     */
061    protected PreferTag(final PreferTag preferTag) {
062        tag = preferTag.getTag();
063        value = preferTag.getValue();
064        params = preferTag.getParams();
065    }
066
067    /**
068     * Parse the prefer tag and parameters out of the header
069     * @param reader the reader
070     */
071    private PreferTag(final HttpHeaderReader reader) {
072
073        // Skip any white space
074        reader.hasNext();
075
076        if (reader.hasNext()) {
077            try {
078                tag = Optional.ofNullable(reader.nextToken())
079                          .map(CharSequence::toString).orElse(null);
080
081                if (reader.hasNextSeparator('=', true)) {
082                    reader.next();
083
084                    value = Optional.ofNullable(reader.nextTokenOrQuotedString())
085                            .    map(CharSequence::toString)
086                                .orElse(null);
087                }
088
089                if (reader.hasNext()) {
090                    params = HttpHeaderReader.readParameters(reader);
091                    if ( params == null ) {
092                        params = new HashMap<>();
093                    }
094                }
095            } catch (final ParseException e) {
096                throw new IllegalArgumentException("Could not parse 'Prefer' header", e);
097            }
098        } else {
099            tag = "";
100        }
101    }
102
103    /**
104     * Create a blank prefer tag
105     * @param inputTag the input tag
106     */
107    public PreferTag(final String inputTag) {
108        this(HttpHeaderReader.newInstance(inputTag));
109    }
110
111    /**
112     * Get the tag name
113     * @return tag name
114     */
115    public String getTag() {
116        return tag;
117    }
118
119    /**
120     * Get the default value for the tag
121     * @return default value for the tag
122     */
123    public String getValue() {
124        return value;
125    }
126
127    /**
128     * Get any additional parameters for the prefer tag
129     * @return additional parameters for the prefer tag
130     */
131    public Map<String,String> getParams() {
132        return params;
133    }
134
135    /**
136     * Add appropriate response headers to indicate that the incoming preferences were acknowledged
137     * @param servletResponse the servlet response
138     */
139    public void addResponseHeaders(final HttpServletResponse servletResponse) {
140
141        final String receivedParam = ofNullable(params.get("received")).orElse("");
142        final List<String> includes = asList(ofNullable(params.get("include")).orElse(" ").split(" "));
143        final List<String> omits = asList(ofNullable(params.get("omit")).orElse(" ").split(" "));
144
145        final StringBuilder includeBuilder = new StringBuilder();
146        final StringBuilder omitBuilder = new StringBuilder();
147
148        if (!(value.equals("minimal") || receivedParam.equals("minimal"))) {
149            final List<String> appliedPrefs = asList(PREFER_SERVER_MANAGED.toString(),
150                    PREFER_MINIMAL_CONTAINER.toString(),
151                    PREFER_MEMBERSHIP.toString(),
152                    PREFER_CONTAINMENT.toString());
153            final List<String> includePrefs = asList(EMBED_CONTAINED.toString(),
154                    INBOUND_REFERENCES.toString());
155            includes.forEach(param -> includeBuilder.append(
156                    (appliedPrefs.contains(param) || includePrefs.contains(param)) ? param + " " : ""));
157
158            // Note: include params prioritized over omits during implementation
159            omits.forEach(param -> omitBuilder.append(
160                    (appliedPrefs.contains(param) && !includes.contains(param)) ? param + " " : ""));
161        }
162
163        // build the header for Preference Applied
164        final String appliedReturn = value.equals("minimal") ? "return=minimal" : "return=representation";
165        final String appliedReceived = receivedParam.equals("minimal") ? "received=minimal" : "";
166
167        final StringBuilder preferenceAppliedBuilder = new StringBuilder(appliedReturn);
168        preferenceAppliedBuilder.append(appliedReceived.length() > 0 ? "; " + appliedReceived : "");
169        appendHeaderParam(preferenceAppliedBuilder, "include", includeBuilder.toString().trim());
170        appendHeaderParam(preferenceAppliedBuilder, "omit", omitBuilder.toString().trim());
171
172        servletResponse.addHeader("Preference-Applied", preferenceAppliedBuilder.toString().trim());
173
174        servletResponse.addHeader("Vary", "Prefer");
175    }
176
177    private void appendHeaderParam(final StringBuilder builder, final String paramName, final String paramValue) {
178        if (paramValue.length() > 0) {
179            builder.append("; " + paramName + "=\"" + paramValue.trim() + "\"");
180        }
181    }
182
183    /**
184     * We consider tags with the same name to be equal, because <a
185     * href="http://tools.ietf.org/html/rfc7240#page-4">the definition of Prefer headers</a> does not permit that tags
186     * with the same name be consumed except by selecting for the first appearing tag.
187     *
188     * @see java.lang.Comparable#compareTo(java.lang.Object)
189     */
190    @Override
191    public int compareTo(final PreferTag otherTag) {
192        return getTag().compareTo(otherTag.getTag());
193    }
194
195    @Override
196    public boolean equals(final Object obj) {
197        if ((obj instanceof PreferTag)) {
198            return getTag().equals(((PreferTag) obj).getTag());
199        }
200        return false;
201    }
202
203    @Override
204    public int hashCode() {
205        if (getTag() == null) {
206            return 0;
207        }
208        return getTag().hashCode();
209    }
210}