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