View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.index.service.util;
23  
24  import static com.entwinemedia.fn.data.json.Jsons.arr;
25  import static com.entwinemedia.fn.data.json.Jsons.f;
26  import static com.entwinemedia.fn.data.json.Jsons.obj;
27  import static com.entwinemedia.fn.data.json.Jsons.v;
28  import static java.lang.String.format;
29  
30  import org.opencastproject.util.DateTimeSupport;
31  import org.opencastproject.util.data.Tuple;
32  import org.opencastproject.util.requests.SortCriterion;
33  
34  import com.entwinemedia.fn.Fx;
35  import com.entwinemedia.fn.data.json.Field;
36  import com.entwinemedia.fn.data.json.JValue;
37  import com.entwinemedia.fn.data.json.SimpleSerializer;
38  
39  import org.apache.commons.lang3.StringUtils;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  import java.io.BufferedOutputStream;
44  import java.io.IOException;
45  import java.io.OutputStream;
46  import java.net.URLDecoder;
47  import java.nio.charset.StandardCharsets;
48  import java.util.ArrayList;
49  import java.util.Date;
50  import java.util.HashMap;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Optional;
54  import java.util.StringTokenizer;
55  
56  import javax.ws.rs.WebApplicationException;
57  import javax.ws.rs.core.MediaType;
58  import javax.ws.rs.core.Response;
59  import javax.ws.rs.core.Response.Status;
60  import javax.ws.rs.core.StreamingOutput;
61  
62  /**
63   * Utils method for the Rest Endpoint implementation
64   */
65  public final class RestUtils {
66    private static final Logger logger = LoggerFactory.getLogger(RestUtils.class);
67  
68    private static final SimpleSerializer serializer = new SimpleSerializer();
69  
70    private RestUtils() {
71    }
72  
73    /**
74     * Create an OK (200) response with the given JSON as body
75     *
76     * @param json
77     *          the JSON string to add to the response body.
78     * @return an OK response
79     */
80    public static Response okJson(JValue json) {
81      return Response.ok(stream(serializer.fn.toJson(json)), MediaType.APPLICATION_JSON_TYPE).build();
82    }
83  
84    /**
85     * Create an CONFLICT (409) response with the given JSON as body
86     *
87     * @param json
88     *          the JSON string to add to the response body.
89     * @return an OK response
90     */
91    public static Response conflictJson(JValue json) {
92      return Response.status(Status.CONFLICT).entity(stream(serializer.fn.toJson(json)))
93              .type(MediaType.APPLICATION_JSON_TYPE).build();
94    }
95  
96    /**
97     * Create a NOT FOUND (404) response with the given messages and arguments
98     *
99     * @param msg
100    * @param args
101    * @return a NOT FOUND response
102    */
103   public static Response notFound(String msg, Object... args) {
104     return Response.status(Status.NOT_FOUND).entity(format(msg, args)).type(MediaType.TEXT_PLAIN_TYPE).build();
105   }
106 
107   /**
108    * Create a NOT FOUND (404) response with the given JSON as body
109    *
110    * @param json
111    *          the JSON string to add to the response body.
112    * @return a NOT FOUND response
113    */
114   public static Response notFoundJson(JValue json) {
115     return Response.status(Status.NOT_FOUND).entity(stream(serializer.fn.toJson(json)))
116         .type(MediaType.APPLICATION_JSON_TYPE).build();
117   }
118 
119   /**
120    * Create an INTERNAL SERVER ERROR (500) response with the given JSON as body
121    *
122    * @param json
123    *          the JSON string to add to the response body.
124    * @return an INTERNAL SERVER ERROR response
125    */
126   public static Response serverErrorJson(JValue json) {
127     return Response.status(Status.INTERNAL_SERVER_ERROR).entity(stream(serializer.fn.toJson(json)))
128         .type(MediaType.APPLICATION_JSON_TYPE).build();
129   }
130 
131   /**
132    * Return the given list of value with the standard format for JSON list value with offset, limit and total
133    * information. See also
134    * {@link org.opencastproject.index.service.util.RestUtils#okJsonList(List, Optional, Optional, long)}.
135    *
136    * @param jsonList
137    *          The list of value to return
138    * @param offset
139    *          The result offset
140    * @param limit
141    *          The maximal list size
142    * @param total
143    *          The amount of available items in the system
144    * @return a {@link Response} with an JSON object formatted like above as body.
145    * @throws IllegalArgumentException
146    *           if the value list is null
147    */
148   public static Response okJsonList(List<JValue> jsonList, int offset, int limit, long total) {
149     return okJsonList(jsonList, Optional.of(offset), Optional.of(limit), total);
150   }
151 
152   /**
153    * Return the given list of value with the standard format for JSON list value with offset, limit and total
154    * information. The JSON object in the response body has the following format:
155    *
156    * <pre>
157    * {
158    *  results: [
159    *    // array containing all the object from the given list
160    *  ],
161    *  count: 12, // The number of item returned (size of the given list)
162    *  offset: 2, // The result offset (given parameter)
163    *  limit: 12, // The maximal size of the list (given parameter)
164    *  total: 123 // The total number of items available in the system (given parameter)
165    * }
166    * </pre>
167    *
168    * Limit and offset are optional.
169    *
170    * @param jsonList
171    *          The list of value to return
172    * @param optOffset
173    *          The result offset (optional)
174    * @param optLimit
175    *          The maximal list size (optional)
176    * @param total
177    *          The amount of available items in the system
178    * @return a {@link Response} with an JSON object formatted like above as body.
179    * @throws IllegalArgumentException
180    *           if the value list is null
181    */
182   public static Response okJsonList(List<JValue> jsonList, Optional<Integer> optOffset, Optional<Integer> optLimit,
183           long total) {
184     if (jsonList == null)
185       throw new IllegalArgumentException("The list of value must not be null.");
186 
187     ArrayList<Field> fields = new ArrayList<>();
188     fields.add(f("results", arr(jsonList)));
189     fields.add(f("count", v(jsonList.size())));
190     fields.add(f("total", v(total)));
191 
192     if (optOffset.isPresent()) {
193       fields.add(f("offset", v(optOffset.get())));
194     }
195     if (optLimit.isPresent()) {
196       fields.add(f("limit", v(optLimit.get())));
197     }
198 
199     return okJson(obj(fields));
200   }
201 
202   /**
203    * Create a streaming response entity. Pass it as an entity parameter to one of the response builder methods like
204    * {@link org.opencastproject.util.RestUtil.R#ok(Object)}.
205    */
206   public static StreamingOutput stream(final Fx<OutputStream> out) {
207     return s -> {
208       try (BufferedOutputStream bs = new BufferedOutputStream(s)) {
209         out.apply(bs);
210       }
211     };
212   }
213 
214   /**
215    * Parse a sort query parameter to a set of {@link SortCriterion}. The parameter has to be of the following form:
216    * {@code <field name>:ASC|DESC}
217    *
218    * @param sort
219    *          the parameter string to parse (will be checked if blank)
220    * @return a set of sort criterion, never {@code null}
221    */
222   public static ArrayList<SortCriterion> parseSortQueryParameter(String sort) throws WebApplicationException {
223     ArrayList<SortCriterion> sortOrders = new ArrayList<>();
224 
225     if (StringUtils.isNotBlank(sort)) {
226       StringTokenizer tokenizer = new StringTokenizer(sort, ",");
227       while (tokenizer.hasMoreTokens()) {
228         try {
229           sortOrders.add(SortCriterion.parse(tokenizer.nextToken()));
230         } catch (IllegalArgumentException e) {
231           throw new WebApplicationException(Status.BAD_REQUEST);
232         }
233       }
234     }
235 
236     return sortOrders;
237   }
238 
239   /**
240    * Parse the UTC format date range string to two Date objects to represent a range of dates.
241    * <p>
242    * Sample UTC date range format string:<br>
243    * i.e. yyyy-MM-ddTHH:mm:ssZ/yyyy-MM-ddTHH:mm:ssZ e.g. 2014-09-27T16:25Z/2014-09-27T17:55Z
244    * </p>
245    *
246    * @param fromToDateRange
247    *          The string that represents the UTC formed date range.
248    * @return A Tuple with the two Dates
249    * @throws IllegalArgumentException
250    *           Thrown if the input string is malformed
251    */
252   public static Tuple<Date, Date> getFromAndToDateRange(String fromToDateRange) {
253     String[] dates = fromToDateRange.split("/");
254     if (dates.length != 2) {
255       logger.warn("The date range '{}' is malformed", fromToDateRange);
256       throw new IllegalArgumentException("The date range string is malformed");
257     }
258 
259     Date fromDate = null;
260     try {
261       fromDate = new Date(DateTimeSupport.fromUTC(dates[0]));
262     } catch (Exception e) {
263       logger.warn("Unable to parse from date parameter '{}'", dates[0]);
264       throw new IllegalArgumentException("Unable to parse from date parameter");
265     }
266 
267     Date toDate = null;
268     try {
269       toDate = new Date(DateTimeSupport.fromUTC(dates[1]));
270     } catch (Exception e) {
271       logger.warn("Unable to parse to date parameter '{}'", dates[1]);
272       throw new IllegalArgumentException("Unable to parse to date parameter");
273     }
274 
275     return new Tuple<Date, Date>(fromDate, toDate);
276   }
277 
278   /**
279    * Parse the filter to a {@link Map}
280    *
281    * @param filter
282    *          the filters
283    * @return the map of filter name and values
284    */
285   public static Map<String, String> parseFilter(String filter) {
286     Map<String, String> filters = new HashMap<>();
287     if (StringUtils.isNotBlank(filter)) {
288       for (String f : filter.split(",")) {
289         String[] filterTuple = f.split(":");
290         if (filterTuple.length < 2) {
291           logger.debug("No value for filter '{}' in filters list: {}", filterTuple[0], filter);
292           continue;
293         }
294         // use substring because dates also contain : so there might be more than two parts
295         filters.put(filterTuple[0].trim(), URLDecoder.decode(f.substring(filterTuple[0].length() + 1).trim(),
296             StandardCharsets.UTF_8));
297       }
298     }
299     return filters;
300   }
301 
302   public static String getJsonString(JValue json) throws WebApplicationException, IOException {
303     OutputStream output = new OutputStream() {
304       private StringBuilder string = new StringBuilder();
305 
306       @Override
307       public void write(int b) throws IOException {
308         this.string.append((char) b);
309       }
310 
311       @Override
312       public String toString() {
313         return this.string.toString();
314       }
315     };
316 
317     stream(serializer.fn.toJson(json)).write(output);
318 
319     return output.toString();
320   }
321 
322   /**
323    * Get a {@link String} value from a {@link JValue} ignoring errors.
324    *
325    * @param json
326    *          The {@link JValue} to convert to a {@link String}
327    * @return The {@link String} representation of the {@link JValue} or an empty string if there was an error.
328    */
329   public static String getJsonStringSilent(JValue json) {
330     try {
331       return getJsonString(json);
332     } catch (Exception e) {
333       return "";
334     }
335   }
336 }