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>// => "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}