View Javadoc
1   /**
2    * This file Copyright (c) 2015-2017 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.objectfactory.Components;
46  import info.magnolia.rest.AbstractEndpoint;
47  
48  import java.lang.reflect.InvocationTargetException;
49  import java.util.ArrayList;
50  import java.util.Collections;
51  import java.util.Comparator;
52  import java.util.List;
53  import java.util.Map;
54  
55  import javax.inject.Inject;
56  import javax.ws.rs.DELETE;
57  import javax.ws.rs.GET;
58  import javax.ws.rs.Path;
59  import javax.ws.rs.PathParam;
60  import javax.ws.rs.Produces;
61  import javax.ws.rs.QueryParam;
62  import javax.ws.rs.core.MediaType;
63  import javax.ws.rs.core.Response;
64  
65  import org.apache.commons.beanutils.BeanUtils;
66  import org.apache.commons.lang3.StringUtils;
67  import com.fasterxml.jackson.databind.node.JsonNodeFactory;
68  import com.fasterxml.jackson.databind.node.ObjectNode;
69  
70  import com.cedarsoftware.util.io.JsonIoException;
71  import com.cedarsoftware.util.io.JsonReader;
72  import com.cedarsoftware.util.io.JsonWriter;
73  import com.google.common.collect.Maps;
74  
75  import io.swagger.annotations.Api;
76  import io.swagger.annotations.ApiOperation;
77  
78  /**
79   * Endpoint for retrieving, downloading and deleting cache entries.
80   */
81  @Api(value = "/cache/v1", description = "The Cache API")
82  @Path("/cache/v1")
83  public class CacheEndpoint extends AbstractEndpoint<CacheEndpointDefinition> {
84  
85      public static final String DEFAULT_REST_URL = "/.rest/cache/v1";
86      public static final String PROPERTY_ERROR_MESSAGE = "errorMessage";
87      public static final String PROPERTY_SIZE = "size";
88      public static final String PROPERTY_ORIGINAL_URL = "originalUrl";
89      public static final String PROPERTY_CONTENT_TYPE = "contentType";
90      public static final String PROPERTY_PLAIN_CONTENT = "plainContent";
91      public static final String PROPERTY_UNSUPPORTED_CACHED_ENTRY_TYPE = "unsupportedCachedEntryType";
92      public static final String ASCENDING_ORDER = "ascending";
93      public static final String DESCENDING_ORDER = "descending";
94  
95      private final CacheFactoryProvider cacheFactoryProvider;
96      private final SimpleTranslator i18n;
97      private final CacheBrowserAppModule module;
98  
99      @Inject
100     public CacheEndpoint(CacheEndpointDefinition endpointDefinition, CacheFactoryProvider cacheFactoryProvider, SimpleTranslator i18n, CacheBrowserAppModule module) {
101         super(endpointDefinition);
102         this.cacheFactoryProvider = cacheFactoryProvider;
103         this.i18n = i18n;
104         this.module = module;
105     }
106 
107     /**
108      * @deprecated since 5.5.3, Please use {@link #CacheEndpoint(CacheEndpointDefinition, CacheFactoryProvider, SimpleTranslator, CacheBrowserAppModule)} instead.
109      */
110     @Deprecated
111     public CacheEndpoint(CacheEndpointDefinition endpointDefinition, CacheFactoryProvider cacheFactoryProvider, SimpleTranslator i18n) {
112         this(endpointDefinition, cacheFactoryProvider, i18n, Components.getComponent(CacheBrowserAppModule.class));
113     }
114 
115     @GET
116     @Path("/ping")
117     @Produces({MediaType.APPLICATION_JSON})
118     @ApiOperation(value = "Checks if the service is alive")
119     public Response ping() {
120         return Response.ok().build();
121     }
122 
123     @GET
124     @Path("/{cacheName}/getAll")
125     @Produces({MediaType.APPLICATION_JSON})
126     @ApiOperation(value = "Get all cache keys", notes = "Returns all cache keys from the specified cache.")
127     public Response getAllKeys(@PathParam("cacheName") String cacheName) {
128         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
129         if (cache == null) {
130             ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
131             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.getAll.cacheNotFound", cacheName));
132             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
133         }
134         ArrayList<Object> keys = new ArrayList<>(cache.getKeys());
135         String json;
136         json = JsonWriter.objectToJson(keys);
137         return Response.status(Response.Status.OK).entity(json).build();
138     }
139 
140     @GET
141     @Path("/{cacheName}/get")
142     @Produces({MediaType.APPLICATION_JSON})
143     @ApiOperation(value = "Get cache keys based on the offset and page length.", notes = "Returns cache keys from the specified cache.")
144     public Response getKeys(@PathParam("cacheName") String cacheName, @QueryParam("offset") int offset, @QueryParam("pageLength") int pageLength, @QueryParam("sortOrder") String sortOrder, @QueryParam("sortProperty") String sortProperty) {
145         ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
146         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
147         if (cache == null) {
148             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.get.cacheNotFound", cacheName));
149             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
150         }
151         if (offset < 0) {
152             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.get.offsetLesserThanZero", cacheName));
153             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
154         }
155         if (pageLength < 1) {
156             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.get.pageLengthLesserThanZero", cacheName));
157             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
158         }
159         List<Object> keys = new ArrayList<>(cache.getKeys());
160         if (StringUtils.isNotBlank(sortOrder) && StringUtils.isNotBlank(sortProperty)) {
161             sortKeys(keys, sortOrder, sortProperty);
162         }
163         String json;
164         if (keys.size() > offset) {
165             int length = (keys.size() > (offset + pageLength)) ? offset + pageLength : keys.size();
166             keys = new ArrayList<>(keys.subList(offset, length));
167         } else {
168             keys = new ArrayList<>();
169         }
170         json = JsonWriter.objectToJson(keys);
171         return Response.status(Response.Status.OK).entity(json).build();
172     }
173 
174     @GET
175     @Path("/{cacheName}/size")
176     @Produces({MediaType.APPLICATION_JSON})
177     @ApiOperation(value = "Get cache size.", notes = "Returns cache size for the specified cache.")
178     public Response getCacheSize(@PathParam("cacheName") String cacheName) {
179         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
180         if (cache == null) {
181             ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
182             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.size.cacheNotFound", cacheName));
183             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
184         }
185         int size = cache.getSize();
186         ObjectNode response = JsonNodeFactory.instance.objectNode();
187         response.put(PROPERTY_SIZE, size);
188         return Response.status(Response.Status.OK).entity(response).build();
189     }
190 
191     @DELETE
192     @Path("/{cacheName}/delete")
193     @ApiOperation(value = "Delete item from cache.", notes = "Deletes item from the cache based on the passed cache key json.")
194     public Response delete(@PathParam("cacheName") String cacheName, @QueryParam("cacheKey") String json) {
195         ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
196         if (StringUtils.isBlank(json)) {
197             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.jsonStringIsBlank"));
198             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
199         }
200         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
201         CacheKey cacheKey;
202         try {
203             cacheKey = deserialiseJsonToCacheKey(json);
204         } catch (JsonIoException e) {
205             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.unableToCreateCacheKeyFromJson", cacheName));
206             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
207         }
208         if (cache == null) {
209             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.cacheNotFound", cacheName));
210             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
211         }
212         if (cacheKey == null) {
213             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.delete.cacheKeyNotFound"));
214             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
215         }
216         cache.remove(cacheKey);
217         return Response.status(Response.Status.OK).build();
218     }
219 
220     /**
221      * Converts given JSON string to {@link CacheKey}.
222      * <p>
223      * Fetches whitelisted classes from {@link CacheBrowserAppModule}, creates a {@link WhitelistAwareClassLoader}
224      * out of them, and finally passes that classloader to {@link JsonReader#jsonToJava(String, Map)} method in order to do the serialisation.
225      * </p>
226      */
227     private CacheKey deserialiseJsonToCacheKey(String json) {
228         Map<String, Object> args = Maps.newHashMap();
229         args.put(CLASSLOADER, new WhitelistAwareClassLoader(CacheBrowserContainer.class.getClassLoader(), module.getWhitelistedKeyClasses()));
230         Object cacheKey = JsonReader.jsonToJava(json, args);
231         if (cacheKey instanceof CacheKey) {
232             return (CacheKey) cacheKey;
233         } else {
234             return null;
235         }
236     }
237 
238     @GET
239     @Path("/{cacheName}/download")
240     @Produces({MediaType.APPLICATION_JSON})
241     @ApiOperation(value = "Get cache content.", notes = "Returns cache content for the specified cache key.")
242     public Response getCacheContent(@PathParam("cacheName") String cacheName, @QueryParam("cacheKey") String json) {
243         ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
244         if (StringUtils.isBlank(json)) {
245             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.jsonStringIsBlank"));
246             return Response.status(Response.Status.BAD_REQUEST).entity(jsonNode).build();
247         }
248         Cache cache = cacheFactoryProvider.get().getCache(cacheName);
249         CacheKey cacheKey;
250         try {
251             cacheKey = deserialiseJsonToCacheKey(json);
252         } catch (JsonIoException e) {
253             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.unableToCreateCacheKeyFromJson", cacheName));
254             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
255         }
256         if (cache == null) {
257             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.cacheNotFound", cacheName));
258             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
259         }
260         if (cacheKey == null) {
261             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.cacheKeyNotFound"));
262             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
263         }
264         Object cachedEntry = cache.getQuiet(cacheKey);
265         if (cachedEntry == null) {
266             jsonNode.put(PROPERTY_ERROR_MESSAGE, i18n.translate("cache.endpoint.download.cachedEntryNotFound"));
267             return Response.status(Response.Status.NOT_FOUND).entity(jsonNode).build();
268         }
269         if (cachedEntry instanceof InMemoryCachedEntry) {
270             InMemoryCachedEntry inMemoryCachedEntry = (InMemoryCachedEntry) cachedEntry;
271             String originalUrl = inMemoryCachedEntry.getOriginalURL();
272             String contentType = inMemoryCachedEntry.getContentType();
273             byte[] plainContent = inMemoryCachedEntry.getPlainContent();
274             jsonNode.put(PROPERTY_ORIGINAL_URL, originalUrl);
275             jsonNode.put(PROPERTY_CONTENT_TYPE, contentType);
276             jsonNode.put(PROPERTY_PLAIN_CONTENT, plainContent);
277         } else {
278             jsonNode.put(PROPERTY_UNSUPPORTED_CACHED_ENTRY_TYPE, i18n.translate("cache.endpoint.download.unsupportedType", cachedEntry.getClass().getCanonicalName(), InMemoryCachedEntry.class.getCanonicalName()));
279         }
280         return Response.status(Response.Status.OK).entity(jsonNode).build();
281     }
282 
283     private void sortKeys(List<Object> keys, final String sortOrder, final String sortProperty) {
284         Collections.sort(keys, new Comparator<Object>() {
285             @Override
286             public int compare(Object o1, Object o2) {
287                 String prop1;
288                 String prop2;
289                 try {
290                     if (sortProperty.equals("extension")) {
291                         prop1 = StringUtils.substringAfterLast(BeanUtils.getProperty(o1, "uri"), ".");
292                         prop2 = StringUtils.substringAfterLast(BeanUtils.getProperty(o2, "uri"), ".");
293                     } else {
294                         prop1 = BeanUtils.getProperty(o1, sortProperty);
295                         prop2 = BeanUtils.getProperty(o2, sortProperty);
296                     }
297                 } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
298                     return 0;
299                 }
300                 if (ASCENDING_ORDER.equals(sortOrder)) {
301                     return prop1.compareTo(prop2);
302                 } else {
303                     return prop2.compareTo(prop1);
304                 }
305             }
306         });
307     }
308 }