001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2009 Sun Microsystems Inc.
015 * Portions Copyright 2010–2011 ApexIdentity Inc.
016 * Portions Copyright 2011-2015 ForgeRock AS.
017 */
018
019package org.forgerock.http.protocol;
020
021
022import static org.forgerock.http.util.Uris.formDecodeParameterNameOrValue;
023import static org.forgerock.http.util.Uris.formEncodeParameterNameOrValue;
024import static org.forgerock.http.util.Uris.urlDecodeQueryParameterNameOrValue;
025import static org.forgerock.http.util.Uris.urlEncodeQueryParameterNameOrValue;
026
027import java.io.IOException;
028import java.net.URISyntaxException;
029import java.util.LinkedHashMap;
030import java.util.List;
031
032import org.forgerock.http.header.ContentLengthHeader;
033import org.forgerock.http.header.ContentTypeHeader;
034import org.forgerock.http.util.MultiValueMap;
035
036/**
037 * Form fields, a case-sensitive multi-string-valued map. The form can be read
038 * from and written to request objects as query parameters (GET) and request
039 * entities (POST).
040 */
041public class Form extends MultiValueMap<String, String> {
042
043    /**
044     * Constructs a new, empty form object.
045     */
046    public Form() {
047        super(new LinkedHashMap<String, List<String>>());
048    }
049
050    /**
051     * Parses a form URL-encoded string containing form parameters and stores them in
052     * this object. Malformed name-value pairs (missing the "=" delimiter) are
053     * simply ignored.
054     *
055     * @param s the form URL-encoded string to parse.
056     * @return this form object.
057     * @deprecated use {@link #fromFormString(String)} instead.
058     */
059    @Deprecated
060    public Form fromString(String s) {
061        return fromFormString(s);
062    }
063
064    /**
065     * Parses a form URL-encoded string containing form parameters and stores them in
066     * this object. Malformed name-value pairs (missing the "=" delimiter) are
067     * simply ignored.
068     *
069     * @param s the form URL-encoded string to parse.
070     * @return this form object.
071     */
072    public Form fromFormString(final String s) {
073        for (String param : s.split("&")) {
074            String[] nv = param.split("=", 2);
075            if (nv.length == 2) {
076                add(formDecodeParameterNameOrValue(nv[0]), formDecodeParameterNameOrValue(nv[1]));
077            }
078        }
079        return this;
080    }
081
082    /**
083     * Parses a URL-encoded query string containing form parameters and stores them in
084     * this object. Malformed name-value pairs (missing the "=" delimiter) are
085     * simply ignored.
086     *
087     * @param s the URL-encoded query string to parse.
088     * @return this form object.
089     */
090    public Form fromQueryString(final String s) {
091        for (String param : s.split("&")) {
092            String[] nv = param.split("=", 2);
093            if (nv.length == 2) {
094                add(urlDecodeQueryParameterNameOrValue(nv[0]), urlDecodeQueryParameterNameOrValue(nv[1]));
095            }
096        }
097        return this;
098    }
099
100    /**
101     * Returns this form in a form URL-encoded string.
102     *
103     * @return the form URL-encoded form.
104     * @deprecated use {@link #toFormString()} instead.
105     */
106    @Deprecated
107    @Override
108    public String toString() {
109        return toFormString();
110    }
111
112    /**
113     * Returns this form in a form URL-encoded string.
114     *
115     * @return the form URL-encoded form.
116     */
117    public String toFormString() {
118        StringBuilder sb = new StringBuilder();
119        for (String name : keySet()) {
120            for (String value : get(name)) {
121                if (sb.length() > 0) {
122                    sb.append('&');
123                }
124                sb.append(formEncodeParameterNameOrValue(name))
125                    .append('=')
126                    .append(formEncodeParameterNameOrValue(value));
127            }
128        }
129        return sb.toString();
130    }
131
132    /**
133     * Returns this form in a URL-encoded query string.
134     *
135     * @return the URL-encoded query string.
136     */
137    public String toQueryString() {
138        StringBuilder sb = new StringBuilder();
139        for (String name : keySet()) {
140            for (String value : get(name)) {
141                if (sb.length() > 0) {
142                    sb.append('&');
143                }
144                sb.append(urlEncodeQueryParameterNameOrValue(name))
145                    .append('=')
146                    .append(urlEncodeQueryParameterNameOrValue(value));
147            }
148        }
149        return sb.toString();
150    }
151
152    /**
153     * Parses the query parameters of a request and stores them in this object.
154     * The object is not cleared beforehand, so this adds to any fields already
155     * in place.
156     *
157     * @param request the request to be parsed.
158     * @return this form object.
159     */
160    public Form fromRequestQuery(Request request) {
161        String query = request.getUri().getRawQuery();
162        if (query != null) {
163            fromQueryString(query);
164        }
165        return this;
166    }
167
168    /**
169     * Sets a request URI with query parameters. This overwrites any query
170     * parameters that may already exist in the request URI.
171     *
172     * @param request the request to set query parameters to.
173     */
174    public void toRequestQuery(Request request) {
175        try {
176            request.getUri().setRawQuery(toQueryString());
177        } catch (URISyntaxException use) {
178            throw new IllegalArgumentException(use);
179        }
180    }
181
182    /**
183     * Appends the form as additional query parameters on an existing request
184     * URI. This leaves any existing query parameters intact.
185     *
186     * @param request the request to append query parameters to.
187     */
188    public void appendRequestQuery(Request request) {
189        StringBuilder sb = new StringBuilder();
190        String uriQuery = request.getUri().getRawQuery();
191        if (uriQuery != null && uriQuery.length() > 0) {
192            sb.append(uriQuery);
193        }
194        String toAppend = toQueryString();
195        if (toAppend != null && toAppend.length() > 0) {
196            if (sb.length() > 0) {
197                sb.append('&');
198            }
199            sb.append(toAppend);
200        }
201        String newQuery = sb.toString();
202        if (newQuery.length() == 0) {
203            newQuery = null;
204        }
205        try {
206            request.getUri().setRawQuery(newQuery);
207        } catch (URISyntaxException use) {
208            throw new IllegalArgumentException(use);
209        }
210    }
211
212    /**
213     * Parses the URL-encoded form entity of a request and stores them in this
214     * object. The object is not cleared beforehand, so this adds to any fields
215     * already in place.
216     *
217     * @param request
218     *            the request to be parsed.
219     * @return this form object.
220     * @throws IOException
221     *             if an I/O exception occurs.
222     */
223    public Form fromRequestEntity(Request request) throws IOException {
224        if (request != null
225                && request.getEntity() != null
226                && "application/x-www-form-urlencoded".equalsIgnoreCase(request.getHeaders()
227                        .getFirst(ContentTypeHeader.class))) {
228            fromFormString(request.getEntity().getString());
229        }
230        return this;
231    }
232
233    /**
234     * Populates a request with the necessary headers and entity for the form to
235     * be submitted as a POST with application/x-www-form-urlencoded content
236     * type. This overwrites any entity that may already be in the request.
237     *
238     * @param request the request to add the form entity to.
239     */
240    public void toRequestEntity(Request request) {
241        String form = toFormString();
242        request.setMethod("POST");
243        request.getHeaders().put(ContentTypeHeader.NAME, "application/x-www-form-urlencoded");
244        request.getHeaders().put(ContentLengthHeader.NAME, form.length());
245        request.getEntity().setString(form);
246    }
247}