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}