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.util;
23  
24  import static org.opencastproject.util.Jsons.obj;
25  import static org.opencastproject.util.Jsons.p;
26  import static org.opencastproject.util.data.Monadics.mlist;
27  import static org.opencastproject.util.data.Option.option;
28  import static org.opencastproject.util.data.Tuple.tuple;
29  import static org.opencastproject.util.data.functions.Strings.split;
30  import static org.opencastproject.util.data.functions.Strings.trimToNil;
31  
32  import org.opencastproject.job.api.JaxbJob;
33  import org.opencastproject.job.api.Job;
34  import org.opencastproject.rest.ErrorCodeException;
35  import org.opencastproject.rest.RestConstants;
36  import org.opencastproject.systems.OpencastConstants;
37  import org.opencastproject.util.Jsons.Obj;
38  import org.opencastproject.util.data.Function;
39  import org.opencastproject.util.data.Monadics;
40  import org.opencastproject.util.data.Option;
41  import org.opencastproject.util.data.Tuple;
42  
43  import org.apache.commons.lang3.StringUtils;
44  import org.osgi.service.component.ComponentContext;
45  
46  import java.io.File;
47  import java.io.IOException;
48  import java.io.InputStream;
49  import java.net.URI;
50  import java.util.regex.Pattern;
51  
52  import javax.ws.rs.core.MediaType;
53  import javax.ws.rs.core.Response;
54  
55  /** Utility functions for REST endpoints. */
56  public final class RestUtil {
57  
58    private RestUtil() {
59    }
60  
61    /**
62     * Return the endpoint's server URL and the service path by extracting the relevant parameters from the
63     * ComponentContext.
64     *
65     * @param cc
66     *          ComponentContext to get configuration from
67     * @return (serverUrl, servicePath)
68     * @throws Error
69     *           if the service path is not configured for this component
70     */
71    public static Tuple<String, String> getEndpointUrl(ComponentContext cc) {
72      return getEndpointUrl(cc, OpencastConstants.SERVER_URL_PROPERTY, RestConstants.SERVICE_PATH_PROPERTY);
73    }
74  
75    /**
76     * Return the endpoint's server URL and the service path by extracting the relevant parameters from the
77     * ComponentContext.
78     *
79     * @param cc
80     *          ComponentContext to get configuration from
81     * @param serverUrlKey
82     *          Configuration key for the server URL
83     * @param servicePathKey
84     *          Configuration key for the service path
85     * @return (serverUrl, servicePath)
86     * @throws Error
87     *           if the service path is not configured for this component
88     */
89    public static Tuple<String, String> getEndpointUrl(ComponentContext cc, String serverUrlKey, String servicePathKey) {
90      final String serverUrl = option(cc.getBundleContext().getProperty(serverUrlKey)).getOrElse(
91              UrlSupport.DEFAULT_BASE_URL);
92      final String servicePath = option((String) cc.getProperties().get(servicePathKey)).getOrElse(
93              Option.<String> error(RestConstants.SERVICE_PATH_PROPERTY + " property not configured"));
94      return tuple(serverUrl, servicePath);
95    }
96  
97    /** Create a file response. */
98    public static Response.ResponseBuilder fileResponse(File f, String contentType, Option<String> fileName) {
99      final Response.ResponseBuilder b = Response.ok(f).header("Content-Type", contentType)
100             .header("Content-Length", f.length());
101     for (String fn : fileName)
102       b.header("Content-Disposition", "attachment; filename=" + fn);
103     return b;
104   }
105 
106   /**
107    * create a partial file response
108    *
109    * @param f
110    *          the requested file
111    * @param contentType
112    *          the contentType to send
113    * @param fileName
114    *          the filename to send
115    * @param rangeHeader
116    *          the range header
117    * @return the Responsebuilder
118    * @throws IOException
119    *           if something goes wrong
120    */
121   public static Response.ResponseBuilder partialFileResponse(File f, String contentType, Option<String> fileName,
122           String rangeHeader) throws IOException {
123 
124     String rangeValue = rangeHeader.trim().substring("bytes=".length());
125     long fileLength = f.length();
126     long start;
127     long end;
128     if (rangeValue.startsWith("-")) {
129       end = fileLength - 1;
130       start = fileLength - 1 - Long.parseLong(rangeValue.substring("-".length()));
131     } else {
132       String[] range = rangeValue.split("-");
133       start = Long.parseLong(range[0]);
134       end = range.length > 1 ? Long.parseLong(range[1]) : fileLength - 1;
135     }
136     if (end > fileLength - 1) {
137       end = fileLength - 1;
138     }
139 
140     // send partial response status code
141     Response.ResponseBuilder response = Response.status(206);
142 
143     if (start <= end) {
144       long contentLength = end - start + 1;
145       response.header("Accept-Ranges", "bytes");
146       response.header("Connection", "Close");
147       response.header("Content-Length", contentLength + "");
148       response.header("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
149       response.header("Content-Type", contentType);
150       response.entity(new ChunkedFileInputStream(f, start, end));
151     }
152 
153     return response;
154   }
155 
156   /**
157    * Create a stream response.
158    *
159    * @deprecated use
160    *             {@link org.opencastproject.util.RestUtil.R#ok(java.io.InputStream, String, org.opencastproject.util.data.Option, org.opencastproject.util.data.Option)}
161    *             instead
162    */
163   @Deprecated
164   public static Response.ResponseBuilder streamResponse(InputStream in, String contentType, Option<Long> streamLength,
165           Option<String> fileName) {
166     final Response.ResponseBuilder b = Response.ok(in).header("Content-Type", contentType);
167     for (Long l : streamLength)
168       b.header("Content-Length", l);
169     for (String fn : fileName)
170       b.header("Content-Disposition", "attachment; filename=" + fn);
171     return b;
172   }
173 
174   /**
175    * Return JSON if <code>format</code> == json, XML else.
176    *
177    * @deprecated use {@link #getResponseType(String)}
178    */
179   @Deprecated
180   public static MediaType getResponseFormat(String format) {
181     return "json".equalsIgnoreCase(format) ? MediaType.APPLICATION_JSON_TYPE : MediaType.APPLICATION_XML_TYPE;
182   }
183 
184   /** Return JSON if <code>type</code> == json, XML else. */
185   public static MediaType getResponseType(String type) {
186     return "json".equalsIgnoreCase(type) ? MediaType.APPLICATION_JSON_TYPE : MediaType.APPLICATION_XML_TYPE;
187   }
188 
189   private static final Function<String, String[]> CSV_SPLIT = split(Pattern.compile(","));
190 
191   /**
192    * Split a comma separated request param into a list of trimmed strings discarding any blank parts.
193    * <p>
194    * x=comma,separated,,%20value -&gt; ["comma", "separated", "value"]
195    */
196   public static Monadics.ListMonadic<String> splitCommaSeparatedParam(Option<String> param) {
197     for (String p : param)
198       return mlist(CSV_SPLIT.apply(p)).bind(trimToNil);
199     return mlist();
200   }
201 
202   public static String generateErrorResponse(ErrorCodeException e) {
203     Obj json = obj(p("error", obj(p("code", e.getErrorCode()), p("message", StringUtils.trimToEmpty(e.getMessage())))));
204     return json.toJson();
205   }
206 
207   /** Response builder functions. */
208   public static final class R {
209     private R() {
210     }
211 
212     public static Response ok() {
213       return Response.ok().build();
214     }
215 
216     public static Response ok(Object entity) {
217       return Response.ok().entity(entity).build();
218     }
219 
220     public static Response ok(boolean entity) {
221       return Response.ok().entity(Boolean.toString(entity)).build();
222     }
223 
224     public static Response ok(Jsons.Obj json) {
225       return Response.ok().entity(json.toJson()).type(MediaType.APPLICATION_JSON_TYPE).build();
226     }
227 
228     public static Response ok(Job job) {
229       return Response.ok().entity(new JaxbJob(job)).build();
230     }
231 
232     public static Response ok(MediaType type, Object entity) {
233       return Response.ok(entity, type).build();
234     }
235 
236     /**
237      * Create a response with status OK from a stream.
238      *
239      * @param in
240      *          the input stream to read from
241      * @param contentType
242      *          the content type to set the Content-Type response header to
243      * @param streamLength
244      *          an optional value for the Content-Length response header
245      * @param fileName
246      *          an optional file name for the Content-Disposition response header
247      */
248     public static Response ok(InputStream in, String contentType, Option<Long> streamLength, Option<String> fileName) {
249       return ok(in, option(contentType), streamLength, fileName);
250     }
251 
252     /**
253      * Create a response with status OK from a stream.
254      *
255      * @param in
256      *          the input stream to read from
257      * @param contentType
258      *          the content type to set the Content-Type response header to
259      * @param streamLength
260      *          an optional value for the Content-Length response header
261      * @param fileName
262      *          an optional file name for the Content-Disposition response header
263      */
264     public static Response ok(InputStream in, Option<String> contentType, Option<Long> streamLength,
265             Option<String> fileName) {
266       final Response.ResponseBuilder b = Response.ok(in);
267       for (String t : contentType)
268         b.header("Content-Type", t);
269       for (Long l : streamLength)
270         b.header("Content-Length", l);
271       for (String fn : fileName)
272         b.header("Content-Disposition", "attachment; filename=" + fn);
273       return b.build();
274     }
275 
276     public static Response created(URI location) {
277       return Response.created(location).build();
278     }
279 
280     public static Response notFound() {
281       return Response.status(Response.Status.NOT_FOUND).build();
282     }
283 
284     public static Response notFound(Object entity) {
285       return Response.status(Response.Status.NOT_FOUND).entity(entity).build();
286     }
287 
288     public static Response notFound(Object entity, MediaType type) {
289       return Response.status(Response.Status.NOT_FOUND).entity(entity).type(type).build();
290     }
291 
292     public static Response locked() {
293       return Response.status(423).build();
294     }
295 
296     public static Response serverError() {
297       return Response.serverError().build();
298     }
299 
300     public static Response conflict() {
301       return Response.status(Response.Status.CONFLICT).build();
302     }
303 
304     public static Response noContent() {
305       return Response.noContent().build();
306     }
307 
308     public static Response badRequest() {
309       return Response.status(Response.Status.BAD_REQUEST).build();
310     }
311 
312     public static Response badRequest(String msg) {
313       return Response.status(Response.Status.BAD_REQUEST).entity(msg).build();
314     }
315 
316     public static Response forbidden() {
317       return Response.status(Response.Status.FORBIDDEN).build();
318     }
319 
320     public static Response forbidden(String msg) {
321       return Response.status(Response.Status.FORBIDDEN).entity(msg).build();
322     }
323 
324     public static Response unauthorized(Object entity) {
325       return Response.status(Response.Status.UNAUTHORIZED).entity(entity).build();
326     }
327 
328     public static Response conflict(String msg) {
329       return Response.status(Response.Status.CONFLICT).entity(msg).build();
330     }
331 
332     /**
333      * create a partial file response
334      *
335      * @param f
336      *          the requested file
337      * @param contentType
338      *          the contentType to send
339      * @param fileName
340      *          the filename to send
341      * @param rangeHeader
342      *          the range header
343      * @return the Responsebuilder
344      * @throws IOException
345      *           if something goes wrong
346      */
347 
348     /**
349      * Creates a precondition failed status response
350      *
351      * @return a precondition failed status response
352      */
353     public static Response preconditionFailed() {
354       return Response.status(Response.Status.PRECONDITION_FAILED).build();
355     }
356 
357     /**
358      * Creates a precondition failed status response with a message
359      *
360      * @param message
361      *          The message body
362      * @return a precondition failed status response with a message
363      */
364     public static Response preconditionFailed(String message) {
365       return Response.status(Response.Status.PRECONDITION_FAILED).entity(message).build();
366     }
367 
368   }
369 }