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
006 * (the "License"); you may not use this file except in compliance with
007 * the 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.classic.header;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.util.function.*;
023
024import org.apache.juneau.commons.collections.*;
025import org.apache.juneau.commons.http.*;
026import org.apache.juneau.http.*;
027
028/**
029 * Represents a parsed <l>Content-Disposition</l> HTTP request header.
030 *
031 * <p>
032 * In a regular HTTP response, the Content-Disposition response header is a header indicating if the content is expected
033 * to be displayed inline in the browser, that is, as a Web page or as part of a Web page, or as an attachment, that is
034 * downloaded and saved locally.
035 *
036 * <h5 class='figure'>Example</h5>
037 * <p class='bcode'>
038 *    Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
039 * </p>
040 *
041 * <h5 class='topic'>RFC2616 Specification</h5>
042 *
043 * The Expect request-header field is used to indicate that particular server behaviors are required by the client.
044 * <p class='bcode'>
045 * content-disposition = "Content-Disposition" ":"
046 *    disposition-type *( ";" disposition-parm )
047 *    disposition-type = "attachment" | disp-extension-token
048 *    disposition-parm = filename-parm | disp-extension-parm
049 *    filename-parm = "filename" "=" quoted-string
050 * disp-extension-token = token
051 *    disp-extension-parm = token "=" ( token | quoted-string )
052 * </p>
053 *
054 * <h5 class='section'>See Also:</h5><ul>
055 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common</a>
056 *    <li class='extlink'><a class="doclink" href="https://www.w3.org/Protocols/rfc2616/rfc2616.html">Hypertext Transfer Protocol -- HTTP/1.1</a>
057 * </ul>
058 *
059 * @serial exclude
060 */
061@Header("Content-Disposition")
062public class ContentDisposition extends BasicStringRangesHeader {
063   private static final long serialVersionUID = 1L;
064   private static final String NAME = "Content-Disposition";
065
066   private static final Cache<String,ContentDisposition> CACHE = Cache.of(String.class, ContentDisposition.class).build();
067
068   /**
069    * Creates a <l>Content-Disposition</l> header for a response attachment (browser download / save-as).
070    *
071    * <p>
072    * Produces <code>attachment; filename="..."</code> with the filename escaped for use inside an HTTP quoted-string
073    * ({@code \} and {@code "} backslash-escaped). CR and LF are rejected to reduce response-splitting risk.
074    * </p>
075    *
076    * <p>
077    * For non-ASCII filenames, RFC 5987 {@code filename*} is not set; callers needing full Unicode support may build
078    * a header value manually or extend this API later.
079    * </p>
080    *
081    * @param filename Suggested download name. Must not be <jk>null</jk>, blank, or contain CR/LF.
082    * @return A non-<jk>null</jk> header.
083    * @throws IllegalArgumentException If {@code filename} is null, blank, or contains line breaks.
084    */
085   public static ContentDisposition attachment(String filename) {
086      if (filename == null || isBlank(filename))
087         throw iaex("Attachment filename must not be null or blank.");
088      for (var i = 0; i < filename.length(); i++) {
089         var c = filename.charAt(i);
090         if (c == '\r' || c == '\n')
091            throw iaex("Attachment filename must not contain CR or LF characters.");
092      }
093      var escaped = escapeFilenameForQuotedString(filename);
094      return ContentDisposition.of("attachment; filename=\"" + escaped + "\"");
095   }
096
097   private static String escapeFilenameForQuotedString(String filename) {
098      var sb = new StringBuilder(filename.length() + 8);
099      for (var i = 0; i < filename.length(); i++) {
100         var c = filename.charAt(i);
101         if (c == '\\' || c == '"')
102            sb.append('\\');
103         sb.append(c);
104      }
105      return sb.toString();
106   }
107
108   /**
109    * Static creator.
110    *
111    * @param value
112    *    The header value.
113    *    <br>Must be parsable by {@link StringRanges#of(String)}.
114    *    <br>Can be <jk>null</jk>.
115    * @return A new header bean, or <jk>null</jk> if the value is <jk>null</jk>.
116    */
117   public static ContentDisposition of(String value) {
118      return value == null ? null : CACHE.get(value, () -> new ContentDisposition(value));
119   }
120
121   /**
122    * Static creator.
123    *
124    * @param value
125    *    The header value.
126    *    <br>Can be <jk>null</jk>.
127    * @return A new header bean, or <jk>null</jk> if the value is <jk>null</jk>.
128    */
129   public static ContentDisposition of(StringRanges value) {
130      return value == null ? null : new ContentDisposition(value);
131   }
132
133   /**
134    * Static creator with delayed value.
135    *
136    * <p>
137    * Header value is re-evaluated on each call to {@link #getValue()}.
138    *
139    * @param value
140    *    The supplier of the header value.
141    *    <br>Can be <jk>null</jk>.
142    * @return A new header bean, or <jk>null</jk> if the value is <jk>null</jk>.
143    */
144   public static ContentDisposition of(Supplier<StringRanges> value) {
145      return value == null ? null : new ContentDisposition(value);
146   }
147
148   /**
149    * Constructor.
150    *
151    * @param value
152    *    The header value.
153    *    <br>Must be parsable by {@link StringRanges#of(String)}.
154    *    <br>Can be <jk>null</jk> to create a header with no value.
155    */
156   public ContentDisposition(String value) {
157      super(NAME, value);
158   }
159
160   /**
161    * Constructor.
162    *
163    * @param value
164    *    The header value.
165    *    <br>Can be <jk>null</jk> to create a header with no value.
166    */
167   public ContentDisposition(StringRanges value) {
168      super(NAME, value);
169   }
170
171   /**
172    * Constructor with delayed value.
173    *
174    * <p>
175    * Header value is re-evaluated on each call to {@link #getValue()}.
176    *
177    * @param value
178    *    The supplier of the header value.
179    *    <br>Can be <jk>null</jk> to create a header with no value.
180    */
181   public ContentDisposition(Supplier<StringRanges> value) {
182      super(NAME, value);
183   }
184}