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