View Javadoc
1   /**
2    * This file Copyright (c) 2015-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cache.browser.rest.endpoint;
35  
36  import static com.cedarsoftware.util.io.JsonReader.CLASSLOADER;
37  
38  import info.magnolia.cache.browser.CacheBrowserAppModule;
39  import info.magnolia.cache.browser.app.CacheBrowserContainer;
40  import info.magnolia.i18nsystem.SimpleTranslator;
41  import info.magnolia.module.cache.Cache;
42  import info.magnolia.module.cache.cachekey.CacheKey;
43  import info.magnolia.module.cache.filter.InMemoryCachedEntry;
44  import info.magnolia.module.cache.inject.CacheFactoryProvider;
45  import info.magnolia.rest.AbstractEndpoint;
46  
47  import java.lang.reflect.InvocationTargetException;
48  import java.util.ArrayList;
49  import java.util.Collections;
50  import java.util.Comparator;
51  import java.util.List;
52  import java.util.Map;
53  
54  import javax.inject.Inject;
55  import javax.ws.rs.DELETE;
56  import javax.ws.rs.GET;
57  import javax.ws.rs.Path;
58  import javax.ws.rs.PathParam;
59  import javax.ws.rs.Produces;
60  import javax.ws.rs.QueryParam;
61  import javax.ws.rs.core.MediaType;
62  import javax.ws.rs.core.Response;
63  
64  import org.apache.commons.beanutils.BeanUtils;
65  import org.apache.commons.lang3.StringUtils;
66  
67  import com.cedarsoftware.util.io.JsonIoException;
68  import com.cedarsoftware.util.io.JsonReader;
69  import com.cedarsoftware.util.io.JsonWriter;
70  import com.fasterxml.jackson.databind.node.JsonNodeFactory;
71  import com.fasterxml.jackson.databind.node.ObjectNode;
72  import com.google.common.collect.Maps;
73  
74  import io.swagger.annotations.Api;
75  import io.swagger.annotations.ApiOperation;
76  
77  /**
78   * Endpoint for retrieving, downloading and deleting cache entries.
79   */
80  @Api(value = "/cache/v1", description = "The Cache API")
81  @Path("/cache/v1")
82  public class CacheEndpoint extends AbstractEndpoint<CacheEndpointDefinition> {
83  
84      public static final String DEFAULT_REST_URL = "/.rest/cache/v1";
85      public static final String PROPERTY_ERROR_MESSAGE = "errorMessage";
86      public static final String PROPERTY_SIZE = "size";
87      public static final String PROPERTY_ORIGINAL_URL = "originalUrl";
88      public static final String PROPERTY_CONTENT_TYPE = "contentType";
89      public static final String PROPERTY_PLAIN_CONTENT = "plainContent";
90      public static final String PROPERTY_UNSUPPORTED_CACHED_ENTRY_TYPE = "unsupportedCachedEntryType";
91      public static final String ASCENDING_ORDER = "ascending";
92      public static final String DESCENDING_ORDER = "descending";
93  
94      private final CacheFactoryProvider cacheFactoryProvider;
95      private final SimpleTranslator i18n;
96      private final CacheBrowserAppModule module;
97  
98      @Inject
99      public CacheEndpoint(CacheEndpointDefinition endpointDefinition, CacheFactoryProvider cacheFactoryProvider, SimpleTranslator i18n, CacheBrowserAppModule module) {
100         super(endpointDefinition);
101         this.cacheFactoryProvider = cacheFactoryProvider;
102         this.i18n = i18n;
103         this.module = module;
104     }
105 
106     @GET
107     @Path("/ping")
108     @Produces({MediaType.APPLICATION_JSON})
109     @ApiOperation(value = "Checks if the service is alive")
110     public Response ping() {
111         return Response.ok().build();
112     }
113 
114     @GET
115     @Path("/{cacheName}/getAll")
116     @Produces({MediaType.APPLICATION_JSON})
117     @ApiOperation(value = "Get all cache keys", notes = "Returns all cache keys from the specified cache.")
118     public Response getAllKeys(@PathParam("cacheName") String cacheName) {
119         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
120         if (cache == null) {
121             ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
122             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.getAll.cacheNotFound", cacheName));
123             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
124         }
125         ArrayList<Object> keys = new ArrayList<>(cache.getKeys());
126         String json;
127         json = JsonWriter.objectToJson(keys);
128         return Response.status(Response.Status.OK).entity(json).build();
129     }
130 
131     @GET
132     @Path("/{cacheName}/get")
133     @Produces({MediaType.APPLICATION_JSON})
134     @ApiOperation(value = "Get cache keys based on the offset and page length.", notes = "Returns cache keys from the specified cache.")
135     public Response getKeys(@PathParam("cacheName") String cacheName, @QueryParam("offset") int offset, @QueryParam("pageLength") int pageLength, @QueryParam("sortOrder") String sortOrder, @QueryParam("sortProperty") String sortProperty) {
136         ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
137         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
138         if (cache == null) {
139             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.get.cacheNotFound", cacheName));
140             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
141         }
142         if (offset < 0) {
143             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.get.offsetLesserThanZero", cacheName));
144             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
145         }
146         if (pageLength < 1) {
147             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.get.pageLengthLesserThanZero", cacheName));
148             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
149         }
150         List<Object> keys = new ArrayList<>(cache.getKeys());
151         if (StringUtils.isNotBlank(sortOrder) && StringUtils.isNotBlank(sortProperty)) {
152             sortKeys(keys, sortOrder, sortProperty);
153         }
154         String json;
155         if (keys.size() > offset) {
156             int length = (keys.size() > (offset + pageLength)) ? offset + pageLength : keys.size();
157             keys = new ArrayList<>(keys.subList(offset, length));
158         } else {
159             keys = new ArrayList<>();
160         }
161         json = JsonWriter.objectToJson(keys);
162         return Response.status(Response.Status.OK).entity(json).build();
163     }
164 
165     @GET
166     @Path("/{cacheName}/size")
167     @Produces({MediaType.APPLICATION_JSON})
168     @ApiOperation(value = "Get cache size.", notes = "Returns cache size for the specified cache.")
169     public Response getCacheSize(@PathParam("cacheName") String cacheName) {
170         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
171         if (cache == null) {
172             ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
173             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.size.cacheNotFound", cacheName));
174             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
175         }
176         int size = cache.getSize();
177         ObjectNode response = JsonNodeFactory.instance.objectNode();
178         response.put(PROPERTY_SIZE, size);
179         return Response.status(Response.Status.OK).entity(response).build();
180     }
181 
182     @DELETE
183     @Path("/{cacheName}/delete")
184     @ApiOperation(value = "Delete item from cache.", notes = "Deletes item from the cache based on the passed cache key json.")
185     public Response delete(@PathParam("cacheName") String cacheName, @QueryParam("cacheKey") String json) {
186         ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
187         if (StringUtils.isBlank(json)) {
188             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.jsonStringIsBlank"));
189             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
190         }
191         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
192         CacheKey cacheKey;
193         try {
194             cacheKey = deserialiseJsonToCacheKey(json);
195         } catch (JsonIoException e) {
196             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.unableToCreateCacheKeyFromJson", cacheName));
197             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
198         }
199         if (cache == null) {
200             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.cacheNotFound", cacheName));
201             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
202         }
203         if (cacheKey == null) {
204             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.cacheKeyNotFound"));
205             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
206         }
207         cache.remove(cacheKey);
208         return Response.status(Response.Status.OK).build();
209     }
210 
211     /**
212      * Converts given JSON string to {@link CacheKey}.
213      * <p>
214      * Fetches whitelisted classes from {@link CacheBrowserAppModule}, creates a {@link WhitelistAwareClassLoader}
215      * out of them, and finally passes that classloader to {@link JsonReader#jsonToJava(String, Map)} method in order to do the serialisation.
216      * </p>
217      */
218     private CacheKey deserialiseJsonToCacheKey(String json) {
219         Map<String, Object> args = Maps.newHashMap();
220         args.put(CLASSLOADER, new WhitelistAwareClassLoader(CacheBrowserContainer.class.getClassLoader(), module.getWhitelistedKeyClasses()));
221         Object cacheKey = JsonReader.jsonToJava(json, args);
222         if (cacheKey instanceof CacheKey) {
223             return (CacheKey) cacheKey;
224         } else {
225             return null;
226         }
227     }
228 
229     @GET
230     @Path("/{cacheName}/download")
231     @Produces({MediaType.APPLICATION_JSON})
232     @ApiOperation(value = "Get cache content.", notes = "Returns cache content for the specified cache key.")
233     public Response getCacheContent(@PathParam("cacheName") String cacheName, @QueryParam("cacheKey") String json) {
234         ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
235         if (StringUtils.isBlank(json)) {
236             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.jsonStringIsBlank"));
237             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
238         }
239         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
240         CacheKey cacheKey;
241         try {
242             cacheKey = deserialiseJsonToCacheKey(json);
243         } catch (JsonIoException e) {
244             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.unableToCreateCacheKeyFromJson", cacheName));
245             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
246         }
247         if (cache == null) {
248             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.cacheNotFound", cacheName));
249             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
250         }
251         if (cacheKey == null) {
252             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.cacheKeyNotFound"));
253             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
254         }
255         Object cachedEntry = cache.getQuiet(cacheKey);
256         if (cachedEntry == null) {
257             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.cachedEntryNotFound"));
258             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
259         }
260         if (cachedEntry instanceof InMemoryCachedEntry) {
261             InMemoryCachedEntry inMemoryCachedEntry = (InMemoryCachedEntry) cachedEntry;
262             String originalUrl = inMemoryCachedEntry.getOriginalURL();
263             String contentType = inMemoryCachedEntry.getContentType();
264             byte[] plainContent = inMemoryCachedEntry.getPlainContent();
265             jsonNode.put(PROPERTY_ORIGINAL_URL, originalUrl);
266             jsonNode.put(PROPERTY_CONTENT_TYPE, contentType);
267             jsonNode.put(PROPERTY_PLAIN_CONTENT, plainContent);
268         } else {
269             jsonNode.put(PROPERTY_UNSUPPORTED_CACHED_ENTRY_TYPE, i18n.translate("cache.endpoint.download.unsupportedType", cachedEntry.getClass().getCanonicalName(), InMemoryCachedEntry.class.getCanonicalName()));
270         }
271         return Response.status(Response.Status.OK).entity(jsonNode).build();
272     }
273 
274     private void sortKeys(List<Object> keys, final String sortOrder, final String sortProperty) {
275         Collections.sort(keys, new Comparator<Object>() {
276             @Override
277             public int compare(Object o1, Object o2) {
278                 String prop1;
279                 String prop2;
280                 try {
281                     if (sortProperty.equals("extension")) {
282                         prop1 = StringUtils.substringAfterLast(BeanUtils.getProperty(o1, "uri"), ".");
283                         prop2 = StringUtils.substringAfterLast(BeanUtils.getProperty(o2, "uri"), ".");
284                     } else {
285                         prop1 = BeanUtils.getProperty(o1, sortProperty);
286                         prop2 = BeanUtils.getProperty(o2, sortProperty);
287                     }
288                 } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
289                     return 0;
290                 }
291                 if (ASCENDING_ORDER.equals(sortOrder)) {
292                     return prop1.compareTo(prop2);
293                 } else {
294                     return prop2.compareTo(prop1);
295                 }
296             }
297         });
298     }
299 }