1 /*
2 * Copyright 2011 Vincent Behar
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.rundeck.api;
17
18 import java.io.File;
19 import java.io.InputStream;
20 import java.nio.charset.Charset;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Date;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Properties;
28 import org.apache.commons.lang.StringUtils;
29 import org.apache.http.NameValuePair;
30 import org.apache.http.message.BasicNameValuePair;
31 import org.dom4j.Document;
32 import org.rundeck.api.generator.XmlDocumentGenerator;
33 import org.rundeck.api.util.ParametersUtil;
34
35 /**
36 * Builder for API paths
37 *
38 * @author Vincent Behar
39 */
40 class ApiPathBuilder {
41
42 /** Internally, we store everything in a {@link StringBuilder} */
43 private final StringBuilder apiPath;
44
45 private String accept="text/xml";
46
47 /** When POSTing, we can add attachments */
48 private final Map<String, InputStream> attachments;
49 private final List<NameValuePair> form = new ArrayList<NameValuePair>();
50 private Document xmlDocument;
51 private InputStream contentStream;
52 private byte[] contents;
53 private File contentFile;
54 private String contentType;
55 private String requiredContentType;
56 private boolean emptyContent = false;
57
58 /** Marker for using the right separator between parameters ("?" or "&") */
59 private boolean firstParamDone = false;
60
61 /**
62 * Build a new instance, for the given "path" (the "path" is the part before the parameters. The path and the
63 * parameters are separated by a "?")
64 *
65 * @param paths elements of the path
66 */
67 public ApiPathBuilder(String... paths) {
68 apiPath = new StringBuilder();
69 attachments = new HashMap<String, InputStream>();
70 paths(paths);
71 }
72
73 public ApiPathBuilder paths(String... paths) {
74 if (paths != null) {
75 for (String path : paths) {
76 if (StringUtils.isNotBlank(path)) {
77 append(path);
78 }
79 }
80 }
81 return this;
82 }
83
84 /**
85 * Set the accept header
86 */
87 public ApiPathBuilder accept(String mimeTypes) {
88 accept=mimeTypes;
89 return this;
90 }
91 /**
92 * Visit a {@link BuildsParameters} and add the parameters
93 */
94 public ApiPathBuilder param(BuildsParameters params) {
95 params.buildParameters(this, false);
96 return this;
97 }
98 /**
99 * Visit a {@link BuildsParameters} and add the parameters
100 */
101 public ApiPathBuilder field(BuildsParameters params) {
102 params.buildParameters(this, true);
103 return this;
104 }
105 /**
106 * Append the given parameter (key and value). This will only append the parameter if it is not blank (null, empty
107 * or whitespace), and make sure to add the right separator ("?" or "&") before. The key and value will be separated
108 * by the "=" character. Also, the value will be url-encoded.
109 *
110 * @param key of the parameter. Must not be null or empty
111 * @param value of the parameter. May be null/empty/blank. Will be url-encoded.
112 * @return this, for method chaining
113 */
114 public ApiPathBuilder param(String key, String value) {
115 if (StringUtils.isNotBlank(value)) {
116 appendSeparator();
117 append(key);
118 append("=");
119 append(ParametersUtil.urlEncode(value));
120 }
121 return this;
122 }
123
124 /**
125 * Append the given parameter (key and value). This will only append the parameter if it is not blank (null, empty
126 * or whitespace), and make sure to add the right separator ("?" or "&") before. The key and value will be separated
127 * by the "=" character. Also, the value will be url-encoded.
128 *
129 * @param key of the parameter. Must not be null or empty
130 * @param value of the parameter. May be null/empty/blank. Will be url-encoded.
131 * @return this, for method chaining
132 */
133 public ApiPathBuilder param(final String key, final Collection<String> values) {
134 for(final String value: values){
135 if (StringUtils.isNotBlank(value)) {
136 appendSeparator();
137 append(key);
138 append("=");
139 append(ParametersUtil.urlEncode(value));
140 }
141 }
142 return this;
143 }
144
145 /**
146 * Append multiple values for the given Form field. This will be appended if it is not blank (null, empty
147 * or whitespace). The form field values will only be used for a "post" request
148 *
149 * @param key of the field name. Must not be null or empty
150 * @param values of the field. May be null/empty/blank. Will be url-encoded.
151 * @return this, for method chaining
152 */
153 public ApiPathBuilder field(final String key, final Collection<String> values) {
154 if (null!=values) {
155 for(final String value: values){
156 if (StringUtils.isNotBlank(value)) {
157 form.add(new BasicNameValuePair(key, value));
158 }
159 }
160 }
161 return this;
162 }
163
164 /**
165 * Append a single value for the given Form field. This will be appended if it is not blank (null, empty
166 * or whitespace). The form field values will only be used for a "post" request
167 *
168 * @param key of the field name. Must not be null or empty
169 * @param value of the field. May be null/empty/blank. Will be url-encoded.
170 * @return this, for method chaining
171 */
172 public ApiPathBuilder field(final String key, final String value) {
173 if (StringUtils.isNotBlank(value)) {
174 form.add(new BasicNameValuePair(key, value));
175 }
176 return this;
177 }
178
179 /**
180 * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure
181 * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character. Also,
182 * the value will be converted to lower-case.
183 *
184 * @param key of the parameter. Must not be null or empty
185 * @param value of the parameter. May be null
186 * @return this, for method chaining
187 */
188 public ApiPathBuilder param(String key, Enum<?> value) {
189 if (value != null) {
190 param(key, StringUtils.lowerCase(value.toString()));
191 }
192 return this;
193 }
194
195 /**
196 * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure
197 * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character.
198 *
199 * @param key of the parameter. Must not be null or empty
200 * @param value of the parameter. May be null
201 * @return this, for method chaining
202 */
203 public ApiPathBuilder param(String key, Date value) {
204 if (value != null) {
205 param(key, value.getTime());
206 }
207 return this;
208 }
209
210 /**
211 * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure
212 * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character.
213 *
214 * @param key of the parameter. Must not be null or empty
215 * @param value of the parameter. May be null
216 * @return this, for method chaining
217 */
218 public ApiPathBuilder param(String key, Long value) {
219 if (value != null) {
220 param(key, value.toString());
221 }
222 return this;
223 }
224
225 /**
226 * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure
227 * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character.
228 *
229 * @param key of the parameter. Must not be null or empty
230 * @param value of the parameter. May be null
231 * @return this, for method chaining
232 */
233 public ApiPathBuilder param(String key, Integer value) {
234 if (value != null) {
235 param(key, value.toString());
236 }
237 return this;
238 }
239
240 /**
241 * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure
242 * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character.
243 *
244 * @param key of the parameter. Must not be null or empty
245 * @param value of the parameter. May be null
246 * @return this, for method chaining
247 */
248 public ApiPathBuilder param(String key, Boolean value) {
249 if (value != null) {
250 param(key, value.toString());
251 }
252 return this;
253 }
254
255 /**
256 * Append the given node filters, only if it is not null/empty
257 *
258 * @param nodeFilters may be null/empty
259 * @return this, for method chaining
260 * @see ParametersUtil#generateNodeFiltersString(Properties)
261 */
262 public ApiPathBuilder nodeFilters(Properties nodeFilters) {
263 String filters = ParametersUtil.generateNodeFiltersString(nodeFilters);
264 if (StringUtils.isNotBlank(filters)) {
265 appendSeparator();
266 append(filters);
267 }
268 return this;
269 }
270
271 /**
272 * When POSTing a request, add the given {@link InputStream} as an attachment to the content of the request. This
273 * will only add the stream if it is not null.
274 *
275 * @param name of the attachment. Must not be null or empty
276 * @param stream. May be null
277 * @return this, for method chaining
278 */
279 public ApiPathBuilder attach(String name, InputStream stream) {
280 if (stream != null) {
281 attachments.put(name, stream);
282 }
283 return this;
284 }
285 /**
286 * When POSTing a request, use the given {@link InputStream} as the content of the request. This
287 * will only add the stream if it is not null.
288 *
289 * @param contentType MIME content type ofr hte request
290 * @param stream content stream
291 * @return this, for method chaining
292 */
293 public ApiPathBuilder content(final String contentType, final InputStream stream) {
294 if (stream != null && contentType != null) {
295 this.contentStream=stream;
296 this.contentType=contentType;
297 }
298 return this;
299 }
300 /**
301 * When POSTing a request, use the given {@link File} as the content of the request. This
302 * will only add the stream if it is not null.
303 *
304 * @param contentType MIME content type ofr hte request
305 * @param file content from a file
306 * @return this, for method chaining
307 */
308 public ApiPathBuilder content(final String contentType, final File file) {
309 if (file != null && contentType != null) {
310 this.contentFile=file;
311 this.contentType=contentType;
312 }
313 return this;
314 }
315 /**
316 * When POSTing a request, use the given data as the content of the request. This
317 * will only add the stream if it is not null.
318 *
319 * @param contentType MIME content type ofr hte request
320 * @param contents content
321 * @return this, for method chaining
322 */
323 public ApiPathBuilder content(final String contentType, final byte[] contents) {
324 if (contents != null && contentType != null) {
325 this.contents=contents;
326 this.contentType=contentType;
327 }
328 return this;
329 }
330 /**
331 * When POSTing a request, use the given string as the content of the request. This
332 * will only add the stream if it is not null.
333 *
334 * @param contentType MIME content type ofr hte request
335 * @param contents content
336 * @return this, for method chaining
337 */
338 public ApiPathBuilder content(final String contentType, final String contents) {
339 return content(contentType, contents.getBytes(Charset.forName("UTF-8")));
340 }
341 /**
342 * When POSTing a request, send an empty request.
343 *
344 * @return this, for method chaining
345 */
346 public ApiPathBuilder emptyContent() {
347 this.emptyContent=true;
348 return this;
349 }
350 /**
351 * When POSTing a request, add the given XMl Document as the content of the request.
352 *
353 * @param document XMl document to send
354 * @return this, for method chaining
355 */
356 public ApiPathBuilder xml(final Document document) {
357 if (document != null) {
358 xmlDocument = document;
359 }
360 return this;
361 }
362 /**
363 * When POSTing a request, add the given XMl Document as the content of the request.
364 *
365 * @param document XMl document to send
366 * @return this, for method chaining
367 */
368 public ApiPathBuilder xml(final XmlDocumentGenerator document) {
369 if (document != null) {
370 xmlDocument = document.generateXmlDocument();
371 }
372 return this;
373 }
374
375 /**
376 * @return all attachments to be POSTed, with their names
377 */
378 public Map<String, InputStream> getAttachments() {
379 return attachments;
380 }
381
382 @Override
383 public String toString() {
384 return apiPath.toString();
385 }
386
387 /**
388 * Append the given string
389 *
390 * @param str to append
391 */
392 private void append(String str) {
393 apiPath.append(str);
394 }
395
396 /**
397 * Append the right separator "?" or "&" between 2 parameters
398 */
399 private void appendSeparator() {
400 if (firstParamDone) {
401 append("&");
402 } else {
403 append("?");
404 firstParamDone = true;
405 }
406 }
407
408 /**
409 * Form fields for POST request
410 */
411 public List<NameValuePair> getForm() {
412 return form;
413 }
414
415 /**
416 * Return true if there are any Attachments or Form data for a POST request.
417 */
418 public boolean hasPostContent() {
419 return getAttachments().size() > 0 || getForm().size() > 0 || null != xmlDocument;
420 }
421
422 /**
423 * Accept header value, default "text/xml"
424 */
425 public String getAccept() {
426 return accept;
427 }
428
429 public Document getXmlDocument() {
430 return xmlDocument;
431 }
432
433 public InputStream getContentStream() {
434 return contentStream;
435 }
436
437 public String getContentType() {
438 return contentType;
439 }
440
441 public File getContentFile() {
442 return contentFile;
443 }
444
445 public boolean isEmptyContent() {
446 return emptyContent;
447 }
448
449 public ApiPathBuilder requireContentType(String contentType) {
450 this.requiredContentType=contentType;
451 return this;
452 }
453
454 public String getRequiredContentType() {
455 return requiredContentType;
456 }
457
458 public byte[] getContents() {
459 return contents;
460 }
461
462 /**
463 * BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder}
464 *
465 * @author Greg Schueler <a href="mailto:greg@dtosolutions.com">greg@dtosolutions.com</a>
466 */
467 public static interface BuildsParameters {
468 /**
469 * Add the parameters or form fields to the ApiPathBuilder
470 *
471 * @param builder the builder
472 * @param doPost if true, use form fields, otherwise use query parameters
473 */
474 public boolean buildParameters(ApiPathBuilder builder, boolean doPost);
475 }
476 }