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.io.*;
022import java.net.*;
023import java.nio.charset.*;
024import java.util.*;
025
026import org.apache.juneau.http.*;
027
028/**
029 * An ordered, immutable list of {@link HttpPart} instances that also implements {@link HttpBody} for
030 * URL-encoded form submission.
031 *
032 * <p>
033 * Use this class to send a {@code application/x-www-form-urlencoded} request body, or to represent
034 * a collection of query parameters, form fields, or path variables.
035 *
036 * <p>
037 * Create instances via the static factory methods:
038 * <p class='bjava'>
039 *    <jc>// From parts</jc>
040 *    PartList <jv>parts</jv> = PartList.<jsm>of</jsm>(
041 *       HttpPartBean.<jsm>of</jsm>(<js>"username"</js>, <js>"alice"</js>),
042 *       HttpPartBean.<jsm>of</jsm>(<js>"password"</js>, <js>"secret"</js>)
043 *    );
044 *
045 *    <jc>// From alternating name/value strings</jc>
046 *    PartList <jv>parts2</jv> = PartList.<jsm>ofPairs</jsm>(<js>"username"</js>, <js>"alice"</js>, <js>"password"</js>, <js>"secret"</js>);
047 * </p>
048 *
049 * <h5 class='section'>See Also:</h5><ul>
050 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
051 * </ul>
052 *
053 * @since 9.2.1
054 */
055@SuppressWarnings({
056   "resource" // OutputStream is owned by caller per HttpBody.writeTo contract; must not be closed here
057})
058public final class PartList implements HttpBody, Iterable<HttpPart> {
059
060   private static final Charset UTF_8 = StandardCharsets.UTF_8;
061
062   private final List<HttpPart> parts;
063
064   private PartList(List<HttpPart> parts) {
065      this.parts = List.copyOf(parts);
066   }
067
068   /**
069    * Creates an empty {@link PartList}.
070    *
071    * @return A new instance. Never <jk>null</jk>.
072    */
073   public static PartList empty() {
074      return new PartList(List.of());
075   }
076
077   /**
078    * Creates a {@link PartList} from the given parts.
079    *
080    * @param parts The parts. Must not be <jk>null</jk>.
081    * @return A new instance. Never <jk>null</jk>.
082    */
083   public static PartList of(HttpPart... parts) {
084      reqnn("parts", parts);
085      return new PartList(Arrays.asList(parts));
086   }
087
088   /**
089    * Creates a {@link PartList} from the given list of parts.
090    *
091    * @param parts The parts. Must not be <jk>null</jk>.
092    * @return A new instance. Never <jk>null</jk>.
093    */
094   public static PartList of(List<HttpPart> parts) {
095      reqnn("parts", parts);
096      return new PartList(parts);
097   }
098
099   /**
100    * Creates a {@link PartList} from alternating name/value string pairs.
101    *
102    * <p>
103    * The number of arguments must be even.
104    *
105    * @param pairs Alternating name/value strings. Must not be <jk>null</jk>. Length must be even.
106    * @return A new instance. Never <jk>null</jk>.
107    * @throws IllegalArgumentException If the number of arguments is odd.
108    */
109   public static PartList ofPairs(String... pairs) {
110      reqnn("pairs", pairs);
111      if (pairs.length % 2 != 0)
112         throw iaex("pairs length must be even, got: %s", pairs.length);
113      var list = new ArrayList<HttpPart>(pairs.length / 2);
114      for (int i = 0; i < pairs.length; i += 2)
115         list.add(HttpPartBean.of(pairs[i], pairs[i + 1]));
116      return new PartList(list);
117   }
118
119   /**
120    * Returns the parts in this list.
121    *
122    * @return An unmodifiable list of parts. Never <jk>null</jk>.
123    */
124   public List<HttpPart> getParts() {
125      return parts;
126   }
127
128   /**
129    * Returns the first part with the given name (case-sensitive), or <jk>null</jk> if absent.
130    *
131    * @param name The part name. Must not be <jk>null</jk>.
132    * @return The first matching part, or <jk>null</jk> if absent.
133    */
134   public HttpPart getFirst(String name) {
135      reqnn("name", name);
136      return parts.stream().filter(p -> eq(name, p.getName())).findFirst().orElse(null);
137   }
138
139   /**
140    * Returns the number of parts in this list.
141    *
142    * @return The number of parts.
143    */
144   public int size() {
145      return parts.size();
146   }
147
148   /**
149    * Returns <jk>true</jk> if this list is empty.
150    *
151    * @return <jk>true</jk> if empty.
152    */
153   public boolean isEmpty() {
154      return parts.isEmpty();
155   }
156
157   @Override /* Iterable */
158   public Iterator<HttpPart> iterator() {
159      return parts.iterator();
160   }
161
162   // ------------------------------------------------------------------------------------------------------------------
163   // HttpBody implementation — for application/x-www-form-urlencoded submission
164   // ------------------------------------------------------------------------------------------------------------------
165
166   @Override /* HttpBody */
167   public String getContentType() {
168      return "application/x-www-form-urlencoded";
169   }
170
171   @Override /* HttpBody */
172   public long getContentLength() {
173      return -1;
174   }
175
176   @Override /* HttpBody */
177   public boolean isRepeatable() {
178      return true;
179   }
180
181   @Override /* HttpBody */
182   public void writeTo(OutputStream out) throws IOException {
183      reqnn("out", out);
184      var sb = new StringBuilder();
185      var first = true;
186      for (var part : parts) {
187         var value = part.getValue();
188         if (value == null)
189            continue;
190         if (!first)
191            sb.append('&');
192         sb.append(URLEncoder.encode(part.getName(), UTF_8))
193            .append('=')
194            .append(URLEncoder.encode(value, UTF_8));
195         first = false;
196      }
197      out.write(sb.toString().getBytes(UTF_8));
198   }
199
200   @Override /* Object */
201   public String toString() {
202      var sb = new StringBuilder();
203      var first = true;
204      for (var part : parts) {
205         var value = part.getValue();
206         if (value == null)
207            continue;
208         if (!first)
209            sb.append('&');
210         sb.append(URLEncoder.encode(part.getName(), UTF_8))
211            .append('=')
212            .append(URLEncoder.encode(value, UTF_8));
213         first = false;
214      }
215      return sb.toString();
216   }
217}