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