001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0 (the
006 * "License"); you may not use this file except in compliance with the
007 * License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.http.header;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.util.*;
023import java.util.function.*;
024
025import org.apache.juneau.commons.http.*;
026
027
028/**
029 * Represents an HTTP <c>Content-Disposition</c> header.
030 *
031 * <p>
032 * Content disposition (inline / attachment / filename).
033 *
034 * @since 9.2.1
035 */
036@SuppressWarnings({
037   "java:S1192" // Duplicated "value" literals are HTTP header component keys; a constant would obscure the header grammar.
038})
039public class ContentDisposition extends HttpStringRangesHeader {
040
041   public static final String NAME = "Content-Disposition";
042
043   public ContentDisposition(String value) {
044      super(NAME, value);
045   }
046
047   public ContentDisposition(StringRanges value) {
048      super(NAME, value);
049   }
050
051   private ContentDisposition(Supplier<?> supplier, int lazyMode) {
052      super(NAME, supplier, lazyMode);
053   }
054
055   public static ContentDisposition of(String value) {
056      return new ContentDisposition(value);
057   }
058
059   public static ContentDisposition of(StringRanges value) {
060      return new ContentDisposition(value);
061   }
062
063   public static ContentDisposition ofLazyWire(Supplier<String> supplier) {
064      return new ContentDisposition(supplier, LAZY_WIRE_STRING);
065   }
066
067   public static ContentDisposition ofLazyParsed(Supplier<StringRanges> supplier) {
068      return new ContentDisposition(supplier, LAZY_STRING_RANGES);
069   }
070
071   /**
072    * Returns a {@code Content-Disposition} header value of the form {@code attachment; filename="<filename>"} with
073    * the file name properly escaped (backslashes and double-quotes are backslash-escaped).
074    *
075    * @param filename The attachment filename. Must not be {@code null}, blank, or contain CR/LF characters.
076    * @return A new {@code ContentDisposition} header.
077    * @throws IllegalArgumentException If {@code filename} is invalid.
078    */
079   public static ContentDisposition attachment(String filename) {
080      if (isBlank(filename))
081         throw iaex("Attachment filename must not be null or blank.");
082      for (var i = 0; i < filename.length(); i++) {
083         var c = filename.charAt(i);
084         if (c == '\r' || c == '\n')
085            throw iaex("Attachment filename must not contain CR or LF characters.");
086      }
087      var sb = new StringBuilder(filename.length() + 8);
088      for (var i = 0; i < filename.length(); i++) {
089         var c = filename.charAt(i);
090         if (c == '\\' || c == '"')
091            sb.append('\\');
092         sb.append(c);
093      }
094      return ContentDisposition.of("attachment; filename=\"" + sb + "\"");
095   }
096
097   /**
098    * Creates a new empty {@link Builder}.
099    *
100    * @return A new builder. Never <jk>null</jk>.
101    */
102   public static Builder create() {
103      return new Builder();
104   }
105
106   //------------------------------------------------------------------------------------------------------------------
107   // Inner types
108   //------------------------------------------------------------------------------------------------------------------
109
110   /**
111    * Fluent builder for assembling an HTTP <c>Content-Disposition</c> header value.
112    *
113    * <p>
114    * Composes the disposition-type and parameters defined by
115    * <a class='doclink' href='https://www.rfc-editor.org/rfc/rfc6266'>RFC 6266</a> into the
116    * <c>disposition-type; param=value; ...</c> wire format.  Typed setters cover the common parameters and apply the
117    * correct rendering (quoted-string for textual params, a bare number for <c>size</c>, and a raw
118    * <a class='doclink' href='https://www.rfc-editor.org/rfc/rfc5987'>RFC 5987</a> ext-value for <c>filename*</c>);
119    * {@link #param(String, String)} is a generic escape hatch.  Setters overwrite a previously-set parameter of the
120    * same name while preserving its original position.
121    *
122    * <h5 class='section'>Example:</h5>
123    * <p class='bjava'>
124    *    String <jv>value</jv> = ContentDisposition.<jsm>create</jsm>()
125    *       .attachment()
126    *       .filename(<js>"genome.jpeg"</js>)
127    *       .size(12345)
128    *       .build();
129    *    <jc>// =&gt; "attachment; filename=\"genome.jpeg\"; size=12345"</jc>
130    * </p>
131    *
132    * <h5 class='section'>See Also:</h5><ul>
133    *    <li class='jc'>{@link ContentDisposition}
134    *    <li class='extlink'><a class='doclink' href='https://www.rfc-editor.org/rfc/rfc6266'>RFC 6266 - Use of the Content-Disposition Header Field</a>
135    * </ul>
136    *
137    * @since 10.0.0
138    */
139   public static class Builder {
140
141      private String type;
142      private final Map<String,String> params = m();
143
144      /**
145       * Sets the disposition-type (e.g. <js>"inline"</js>, <js>"attachment"</js>, <js>"form-data"</js>).
146       *
147       * @param value The disposition-type. Must not be <jk>null</jk> or blank.
148       * @return This object.
149       * @throws IllegalArgumentException If {@code value} is <jk>null</jk> or blank.
150       */
151      public Builder type(String value) {
152         reqnn("value", value);
153         var v = value.trim();
154         if (v.isEmpty())
155            throw iaex("disposition-type must not be blank");
156         type = v;
157         return this;
158      }
159
160      /**
161       * Sets the disposition-type to <c>inline</c>.
162       *
163       * @return This object.
164       */
165      public Builder inline() {
166         return type("inline");
167      }
168
169      /**
170       * Sets the disposition-type to <c>attachment</c>.
171       *
172       * @return This object.
173       */
174      public Builder attachment() {
175         return type("attachment");
176      }
177
178      /**
179       * Sets the disposition-type to <c>form-data</c>.
180       *
181       * @return This object.
182       */
183      public Builder formData() {
184         return type("form-data");
185      }
186
187      /**
188       * Sets the <c>name</c> parameter (rendered as a quoted-string).
189       *
190       * @param value The field name. Must not be <jk>null</jk>.
191       * @return This object.
192       */
193      public Builder name(String value) {
194         return quotedParam("name", value);
195      }
196
197      /**
198       * Sets the <c>filename</c> parameter (rendered as a quoted-string with backslash/quote escaping).
199       *
200       * @param value The filename. Must not be <jk>null</jk>.
201       * @return This object.
202       */
203      public Builder filename(String value) {
204         return quotedParam("filename", value);
205      }
206
207      /**
208       * Sets the <c>filename*</c> parameter (rendered as a raw RFC 5987 ext-value, unquoted).
209       *
210       * @param value The encoded ext-value (e.g. <js>"UTF-8''%e2%82%ac%20rates"</js>). Must not be <jk>null</jk>.
211       * @return This object.
212       */
213      public Builder filenameExt(String value) {
214         return tokenParam("filename*", value);
215      }
216
217      /**
218       * Sets the <c>creation-date</c> parameter (rendered as a quoted HTTP-date).
219       *
220       * @param value The creation date. Must not be <jk>null</jk>.
221       * @return This object.
222       */
223      public Builder creationDate(String value) {
224         return quotedParam("creation-date", value);
225      }
226
227      /**
228       * Sets the <c>modification-date</c> parameter (rendered as a quoted HTTP-date).
229       *
230       * @param value The modification date. Must not be <jk>null</jk>.
231       * @return This object.
232       */
233      public Builder modificationDate(String value) {
234         return quotedParam("modification-date", value);
235      }
236
237      /**
238       * Sets the <c>read-date</c> parameter (rendered as a quoted HTTP-date).
239       *
240       * @param value The read date. Must not be <jk>null</jk>.
241       * @return This object.
242       */
243      public Builder readDate(String value) {
244         return quotedParam("read-date", value);
245      }
246
247      /**
248       * Sets the <c>size</c> parameter (rendered as a bare number).
249       *
250       * @param value The size in bytes. Must be non-negative.
251       * @return This object.
252       * @throws IllegalArgumentException If {@code value} is negative.
253       */
254      public Builder size(long value) {
255         if (value < 0)
256            throw iaex("size must be non-negative: %s", value);
257         return tokenParam("size", Long.toString(value));
258      }
259
260      /**
261       * Generic escape hatch for setting any parameter by name, rendered as a quoted-string.
262       *
263       * <p>
264       * Replaces any previously-registered value for the same parameter while preserving its original position.
265       *
266       * @param name The parameter name. Must not be <jk>null</jk> or blank.
267       * @param value The parameter value (quoted automatically). Must not be <jk>null</jk>.
268       * @return This object.
269       * @throws IllegalArgumentException If {@code name} is <jk>null</jk> or blank.
270       */
271      public Builder param(String name, String value) {
272         return quotedParam(name, value);
273      }
274
275      private Builder quotedParam(String name, String value) {
276         reqnn("value", value);
277         params.put(cleanName(name), q(value));
278         return this;
279      }
280
281      private Builder tokenParam(String name, String value) {
282         reqnn("value", value);
283         params.put(cleanName(name), value);
284         return this;
285      }
286
287      private static String cleanName(String name) {
288         reqnn("name", name);
289         var n = name.trim();
290         if (n.isEmpty())
291            throw iaex("parameter name must not be blank");
292         return n;
293      }
294
295      private static String q(String value) {
296         var sb = new StringBuilder(value.length() + 2);
297         sb.append('"');
298         for (var i = 0; i < value.length(); i++) {
299            var c = value.charAt(i);
300            if (c == '\\' || c == '"')
301               sb.append('\\');
302            sb.append(c);
303         }
304         sb.append('"');
305         return sb.toString();
306      }
307
308      /**
309       * Builds the rendered <c>Content-Disposition</c> header value.
310       *
311       * <p>
312       * Returns an empty string when no disposition-type or parameters have been registered.
313       *
314       * @return The header value. Never <jk>null</jk>.
315       */
316      public String build() {
317         var parts = new ArrayList<String>(params.size() + 1);
318         if (type != null)
319            parts.add(type);
320         params.forEach((k, v) -> parts.add(k + "=" + v));
321         return String.join("; ", parts);
322      }
323
324      /**
325       * Builds a {@link ContentDisposition} header bean directly from this builder.
326       *
327       * @return A {@link ContentDisposition} carrying {@link #build()}. Never <jk>null</jk>.
328       */
329      public ContentDisposition toHeader() {
330         return ContentDisposition.of(build());
331      }
332
333      @Override
334      public String toString() {
335         return build();
336      }
337   }
338}