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.part; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022import java.util.function.*; 023 024import org.apache.juneau.http.*; 025 026/** 027 * A mutable, ordered list of {@link HttpPart} instances. 028 * 029 * <p> 030 * Mirrors the semantics of {@code PartList} from {@code juneau-rest-common-classic} without the 031 * Apache HttpCore dependency. Part-name comparisons are case-sensitive by default (matching the 032 * classic default for parts; headers use case-insensitive comparisons). 033 * 034 * <p> 035 * Use {@link PartList} when you need an immutable list bound to an HTTP body (e.g. 036 * {@code application/x-www-form-urlencoded}); use {@code HttpPartList} when you need a mutable 037 * builder for default form data / query parameter lists. 038 * 039 * <h5 class='section'>See Also:</h5><ul> 040 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a> 041 * </ul> 042 * 043 * @since 10.0.0 044 */ 045@SuppressWarnings({ 046 "java:S2160" // equals() inherited from ArrayList; list equality is element-based which is correct 047}) 048public class HttpPartList extends ArrayList<HttpPart> { 049 050 private static final long serialVersionUID = 1L; 051 052 /** Represents no part list in annotations. */ 053 public static final class Void extends HttpPartList { 054 private static final long serialVersionUID = 1L; 055 } 056 057 private boolean caseInsensitive; 058 059 /** 060 * Creates a new empty {@link HttpPartList}. 061 * 062 * @return A new instance. Never <jk>null</jk>. 063 */ 064 public static HttpPartList create() { 065 return new HttpPartList(); 066 } 067 068 /** 069 * Creates a new {@link HttpPartList} initialized with the given parts. 070 * 071 * @param parts The parts to include. {@code null} entries are ignored. 072 * @return A new instance. Never <jk>null</jk>. 073 */ 074 public static HttpPartList of(HttpPart...parts) { 075 return new HttpPartList().append(parts); 076 } 077 078 /** 079 * Creates a new {@link HttpPartList} initialized with the given parts. 080 * 081 * @param parts The parts to include. {@code null} entries are ignored. 082 * @return A new instance. Never <jk>null</jk>. 083 */ 084 public static HttpPartList of(List<HttpPart> parts) { 085 return new HttpPartList().append(parts); 086 } 087 088 /** 089 * Creates a new {@link HttpPartList} initialized with the given alternating {@code name}/{@code value} pairs. 090 * 091 * @param pairs Alternating name/value strings. Can be <jk>null</jk> (treated as empty). Length must be even. 092 * @return A new instance. Never <jk>null</jk>. 093 * @throws IllegalArgumentException If the number of arguments is odd. 094 */ 095 public static HttpPartList ofPairs(String...pairs) { 096 var x = new HttpPartList(); 097 if (pairs == null) 098 pairs = new String[0]; 099 req(pairs.length % 2 == 0, "Odd number of parameters passed into HttpPartList.ofPairs()"); 100 for (var i = 0; i < pairs.length; i += 2) 101 x.add(HttpPartBean.of(pairs[i], pairs[i + 1])); 102 return x; 103 } 104 105 /** Constructor. */ 106 public HttpPartList() { 107 super(); 108 } 109 110 /** 111 * Copy constructor. 112 * 113 * @param copyFrom The list to copy. Must not be <jk>null</jk>. 114 */ 115 protected HttpPartList(HttpPartList copyFrom) { 116 super(copyFrom); 117 this.caseInsensitive = copyFrom.caseInsensitive; 118 } 119 120 /** 121 * Sets whether part-name comparisons are case-insensitive. Defaults to {@code false} (case-sensitive). 122 * 123 * @param value New value. 124 * @return This object. 125 */ 126 public HttpPartList caseInsensitive(boolean value) { 127 this.caseInsensitive = value; 128 return this; 129 } 130 131 /** 132 * Appends the given part. 133 * 134 * @param value The part to add. {@code null} is ignored. 135 * @return This object. 136 */ 137 public HttpPartList append(HttpPart value) { 138 if (value != null) 139 add(value); 140 return this; 141 } 142 143 /** 144 * Appends the given parts in order. 145 * 146 * @param values The parts to add. {@code null} entries are ignored. 147 * @return This object. 148 */ 149 public HttpPartList append(HttpPart...values) { 150 if (values != null) 151 for (var v : values) 152 append(v); 153 return this; 154 } 155 156 /** 157 * Appends the given parts in order. 158 * 159 * @param values The parts to add. {@code null} entries are ignored. 160 * @return This object. 161 */ 162 public HttpPartList append(List<HttpPart> values) { 163 if (values != null) 164 values.forEach(this::append); 165 return this; 166 } 167 168 /** 169 * Appends a part constructed from the given name and string value. 170 * 171 * @param name The part name. Must not be <jk>null</jk>. 172 * @param value The part value. Can be <jk>null</jk> to omit the part from the request. 173 * @return This object. 174 */ 175 public HttpPartList append(String name, String value) { 176 return append(HttpPartBean.of(name, value)); 177 } 178 179 /** 180 * Sets the given part as a default. If a part with the same name already exists, this is a no-op 181 * (first-in-chain wins per name). 182 * 183 * @param value The part to set. {@code null} is ignored. 184 * @return This object. 185 */ 186 public HttpPartList setDefault(HttpPart value) { 187 if (value != null && !contains(value.getName())) 188 add(value); 189 return this; 190 } 191 192 /** 193 * Sets the given parts as defaults, in order. For each, if a part with the same name already exists, 194 * the entry is skipped (first-in-chain wins per name). 195 * 196 * @param values The parts to set. {@code null} entries are ignored. 197 * @return This object. 198 */ 199 public HttpPartList setDefault(HttpPart...values) { 200 if (values != null) 201 for (var v : values) 202 setDefault(v); 203 return this; 204 } 205 206 /** 207 * Sets a default part with the given name and value (first-in-chain wins per name). 208 * 209 * @param name The part name. Must not be <jk>null</jk>. 210 * @param value The part value. Can be <jk>null</jk> to omit the part from the request. 211 * @return This object. 212 */ 213 public HttpPartList setDefault(String name, String value) { 214 return setDefault(HttpPartBean.of(name, value)); 215 } 216 217 /** 218 * Sets the given part, replacing any prior entry with the same name. 219 * 220 * @param value The part to set. {@code null} is ignored. 221 * @return This object. 222 */ 223 public HttpPartList set(HttpPart value) { 224 if (value == null) 225 return this; 226 removeAll(value.getName()); 227 add(value); 228 return this; 229 } 230 231 /** 232 * Sets the given parts, each replacing any prior entry with the same name. 233 * 234 * @param values The parts to set. {@code null} entries are ignored. 235 * @return This object. 236 */ 237 public HttpPartList set(HttpPart...values) { 238 if (values != null) 239 for (var v : values) 240 set(v); 241 return this; 242 } 243 244 /** 245 * Sets a part with the given name and value, replacing any prior entry with the same name. 246 * 247 * @param name The part name. Must not be <jk>null</jk>. 248 * @param value The part value. Can be <jk>null</jk> to omit the part from the request. 249 * @return This object. 250 */ 251 public HttpPartList set(String name, String value) { 252 return set(HttpPartBean.of(name, value)); 253 } 254 255 /** 256 * Removes all parts with the given name. 257 * 258 * @param name The part name. Must not be <jk>null</jk>. 259 * @return This object. 260 */ 261 public HttpPartList removeAll(String name) { 262 reqnn("name", name); 263 removeIf(p -> nameMatches(p, name)); 264 return this; 265 } 266 267 /** 268 * Returns {@code true} if a part with the given name is present. 269 * 270 * @param name The part name. 271 * @return {@code true} if present. 272 */ 273 public boolean contains(String name) { 274 return stream().anyMatch(p -> nameMatches(p, name)); 275 } 276 277 /** 278 * Returns the first part with the given name, or {@code null} if absent. 279 * 280 * @param name The part name. 281 * @return The first matching part, or {@code null}. 282 */ 283 public HttpPart getFirst(String name) { 284 return stream().filter(p -> nameMatches(p, name)).findFirst().orElse(null); 285 } 286 287 /** 288 * Returns the last part with the given name, or {@code null} if absent. 289 * 290 * @param name The part name. 291 * @return The last matching part, or {@code null}. 292 */ 293 public HttpPart getLast(String name) { 294 HttpPart last = null; 295 for (var p : this) 296 if (nameMatches(p, name)) 297 last = p; 298 return last; 299 } 300 301 /** 302 * Returns all parts with the given name in insertion order. 303 * 304 * @param name The part name. 305 * @return A new list of matching parts. Never <jk>null</jk>. 306 */ 307 public List<HttpPart> getAll(String name) { 308 return stream().filter(p -> nameMatches(p, name)).toList(); 309 } 310 311 /** 312 * Returns all parts in this list as a typed array. 313 * 314 * @return A new array. Never <jk>null</jk>. 315 */ 316 public HttpPart[] getAll() { 317 return toPartArray(); 318 } 319 320 /** 321 * Performs an action on each part whose name matches. 322 * 323 * @param name The part name. 324 * @param action The action. 325 * @return This object. 326 */ 327 public HttpPartList forEach(String name, Consumer<HttpPart> action) { 328 for (var p : this) 329 if (nameMatches(p, name)) 330 action.accept(p); 331 return this; 332 } 333 334 /** 335 * Returns a copy of this list. 336 * 337 * @return A new copy. Never <jk>null</jk>. 338 */ 339 public HttpPartList copy() { 340 return new HttpPartList(this); 341 } 342 343 /** 344 * Returns an unmodifiable snapshot of this list. 345 * 346 * <p> 347 * Idempotency: if this list is already an {@link Unmodifiable} snapshot, it is returned as-is. 348 * 349 * @return An unmodifiable snapshot of this list, or this list if it is already unmodifiable. 350 */ 351 public HttpPartList unmodifiable() { 352 return this instanceof Unmodifiable ? this : new Unmodifiable(this); 353 } 354 355 /** 356 * Returns <jk>true</jk> if this list is an unmodifiable snapshot. 357 * 358 * @return <jk>true</jk> if this list is an {@link UnmodifiableBean} snapshot. 359 */ 360 public boolean isUnmodifiable() { 361 return this instanceof UnmodifiableBean; 362 } 363 364 /** 365 * Returns the parts in this list as a typed array. 366 * 367 * @return A new array. Never <jk>null</jk>. 368 */ 369 public HttpPart[] toPartArray() { 370 return toArray(new HttpPart[0]); 371 } 372 373 private boolean nameMatches(HttpPart p, String name) { 374 return caseInsensitive ? eqic(p.getName(), name) : eq(p.getName(), name); 375 } 376 377 /** 378 * An unmodifiable snapshot of a {@link HttpPartList}. 379 * 380 * <p> 381 * This is the "collection-mutator-override" variant of the immutability paradigm. Because {@link HttpPartList} 382 * extends {@link ArrayList}, a single funnel cannot intercept the inherited {@code ArrayList} mutators, so this 383 * subclass snapshot-copies the elements in its constructor and overrides the entire collection-mutator surface — its 384 * own fluent mutators plus the inherited {@code ArrayList} mutators (including the {@code iterator()} / 385 * {@code listIterator()} mutators) — to throw {@link UnsupportedOperationException}. Read operations and 386 * non-mutating iteration continue to work. 387 */ 388 public static class Unmodifiable extends HttpPartList implements UnmodifiableBean { 389 390 private static final long serialVersionUID = 1L; 391 392 /** 393 * Constructor. 394 * 395 * @param copyFrom The list to snapshot-copy. Must not be <jk>null</jk>. 396 */ 397 Unmodifiable(HttpPartList copyFrom) { 398 super(copyFrom); 399 } 400 401 // --- Fluent mutators ----------------------------------------------------------------------------------------- 402 403 @Override public HttpPartList append(HttpPart value) { throw uoex(RO); } 404 @Override public HttpPartList append(HttpPart...values) { throw uoex(RO); } 405 @Override public HttpPartList append(List<HttpPart> values) { throw uoex(RO); } 406 @Override public HttpPartList append(String name, String value) { throw uoex(RO); } 407 @Override public HttpPartList caseInsensitive(boolean value) { throw uoex(RO); } 408 @Override public HttpPartList removeAll(String name) { throw uoex(RO); } 409 @Override public HttpPartList set(HttpPart value) { throw uoex(RO); } 410 @Override public HttpPartList set(HttpPart...values) { throw uoex(RO); } 411 @Override public HttpPartList set(String name, String value) { throw uoex(RO); } 412 @Override public HttpPartList setDefault(HttpPart value) { throw uoex(RO); } 413 @Override public HttpPartList setDefault(HttpPart...values) { throw uoex(RO); } 414 @Override public HttpPartList setDefault(String name, String value) { throw uoex(RO); } 415 416 // --- Inherited ArrayList collection mutators ----------------------------------------------------------------- 417 418 @Override public boolean add(HttpPart e) { throw uoex(RO); } 419 @Override public void add(int index, HttpPart element) { throw uoex(RO); } 420 @Override public boolean addAll(Collection<? extends HttpPart> c) { throw uoex(RO); } 421 @Override public boolean addAll(int index, Collection<? extends HttpPart> c) { throw uoex(RO); } 422 @Override public void clear() { throw uoex(RO); } 423 @Override public boolean remove(Object o) { throw uoex(RO); } 424 @Override public HttpPart remove(int index) { throw uoex(RO); } 425 @Override public boolean removeAll(Collection<?> c) { throw uoex(RO); } 426 @Override public boolean removeIf(Predicate<? super HttpPart> filter) { throw uoex(RO); } 427 @Override public void replaceAll(UnaryOperator<HttpPart> operator) { throw uoex(RO); } 428 @Override public boolean retainAll(Collection<?> c) { throw uoex(RO); } 429 @Override public HttpPart set(int index, HttpPart element) { throw uoex(RO); } 430 @Override public void sort(Comparator<? super HttpPart> c) { throw uoex(RO); } 431 432 @Override public Iterator<HttpPart> iterator() { 433 var i = super.iterator(); 434 return new Iterator<>() { 435 @Override public boolean hasNext() { return i.hasNext(); } 436 @Override public HttpPart next() { return i.next(); } 437 @Override public void remove() { throw uoex(RO); } 438 }; 439 } 440 441 @Override public ListIterator<HttpPart> listIterator() { return listIterator(0); } 442 443 @Override public ListIterator<HttpPart> listIterator(int index) { 444 var i = super.listIterator(index); 445 return new ListIterator<>() { 446 @Override public boolean hasNext() { return i.hasNext(); } 447 @Override public HttpPart next() { return i.next(); } 448 @Override public boolean hasPrevious() { return i.hasPrevious(); } 449 @Override public HttpPart previous() { return i.previous(); } 450 @Override public int nextIndex() { return i.nextIndex(); } 451 @Override public int previousIndex() { return i.previousIndex(); } 452 @Override public void add(HttpPart e) { throw uoex(RO); } 453 @Override public void set(HttpPart e) { throw uoex(RO); } 454 @Override public void remove() { throw uoex(RO); } 455 }; 456 } 457 } 458 459 private static final String RO = "Bean is unmodifiable."; 460}