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.app;
35  
36  import info.magnolia.cache.browser.app.event.InitializeContainerAfterSuccessfulLoginEvent;
37  import info.magnolia.cache.browser.app.event.RefreshContainerEvent;
38  import info.magnolia.cache.browser.rest.CacheServiceFactory;
39  import info.magnolia.cache.browser.rest.client.CacheService;
40  import info.magnolia.cache.browser.rest.endpoint.CacheEndpoint;
41  import info.magnolia.context.Context;
42  import info.magnolia.context.MgnlContext;
43  import info.magnolia.event.EventBus;
44  import info.magnolia.module.cache.cachekey.CacheKey;
45  import info.magnolia.ui.workbench.container.AbstractContainer;
46  import info.magnolia.ui.workbench.container.Refreshable;
47  
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.Collections;
51  import java.util.HashMap;
52  import java.util.LinkedHashSet;
53  import java.util.List;
54  import java.util.Map;
55  import java.util.Set;
56  
57  import javax.ws.rs.ClientErrorException;
58  
59  import org.apache.commons.lang3.StringUtils;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import com.cedarsoftware.util.io.JsonReader;
64  import com.fasterxml.jackson.databind.JsonNode;
65  import com.google.common.cache.Cache;
66  import com.google.common.cache.CacheBuilder;
67  import com.vaadin.v7.data.Container;
68  import com.vaadin.v7.data.ContainerHelpers;
69  import com.vaadin.v7.data.Item;
70  import com.vaadin.v7.data.Property;
71  
72  /**
73   * Container that holds all cache entries.
74   */
75  public class CacheBrowserContainer extends AbstractContainer implements Container.Indexed, Container.ItemSetChangeNotifier, Container.Sortable, Refreshable {
76  
77      private static final Logger log = LoggerFactory.getLogger(CacheBrowserContainer.class);
78  
79      public static final int DEFAULT_PAGE_LENGTH = 30;
80  
81      public static final int DEFAULT_CACHE_RATIO = 2;
82  
83      /**
84       * Page length = number of items contained in one page.
85       */
86      private int pageLength = DEFAULT_PAGE_LENGTH;
87  
88      /**
89       * Number of items to cache = cacheRatio x pageLength.
90       */
91      private int cacheRatio = DEFAULT_CACHE_RATIO;
92  
93      private int currentOffset;
94  
95      private int size = 0;
96  
97      private Cache<Long, CacheKeyItem> itemIndexes = CacheBuilder.newBuilder().maximumSize(1000).build();
98  
99      private Set<ItemSetChangeListener> itemSetChangeListeners;
100 
101     private final CacheBrowserContentConnectorDefinition contentConnectorDefinition;
102 
103     private final CacheServiceFactory cacheServiceFactory;
104 
105     private Map<String, CacheService> cacheServices = new HashMap<>();
106 
107     private List<String> sortableProperties = new ArrayList<>();
108 
109     private String password;
110 
111     private String username;
112 
113     private String sortOrder;
114 
115     private String sortProperty;
116 
117     public CacheBrowserContainer(CacheBrowserContentConnectorDefinition contentConnectorDefinition, CacheServiceFactory cacheServiceFactory, EventBus eventBus) {
118         this.contentConnectorDefinition = contentConnectorDefinition;
119         this.cacheServiceFactory = cacheServiceFactory;
120         eventBus.addHandler(InitializeContainerAfterSuccessfulLoginEvent.class, new InitializeContainerAfterSuccessfulLoginEvent.Handler() {
121             @Override
122             public void onLogin(InitializeContainerAfterSuccessfulLoginEvent event) {
123                 initialize(event.getUsername(), event.getPassword());
124             }
125         });
126         eventBus.addHandler(RefreshContainerEvent.class, new RefreshContainerEvent.Handler() {
127             @Override
128             public void onRefresh() {
129                 initialize(username, password);
130             }
131         });
132     }
133 
134     public void initialize(String username, String password) {
135         this.username = username;
136         this.password = password;
137         if (StringUtils.isNotBlank(username) || StringUtils.isNotBlank(password)) {
138             cacheServices = cacheServiceFactory.createCacheServices(new String[] {username, password});
139         }
140     }
141 
142     @Override
143     public Item getItem(Object itemId) {
144         for (CacheKeyItem cacheKeyItem : itemIndexes.asMap().values()) {
145             if (cacheKeyItem.getItemId().equals(itemId)) {
146                 return cacheKeyItem;
147             }
148         }
149         return null;
150     }
151 
152     @Override
153     public Collection<CacheKey> getItemIds() {
154         List<CacheKey> itemIds = new ArrayList<>();
155         for (CacheKeyItem cacheKeyItem : itemIndexes.asMap().values()) {
156             itemIds.add(cacheKeyItem.getItemId());
157         }
158         return Collections.unmodifiableList(itemIds);
159     }
160 
161     @Override
162     public Property<?> getContainerProperty(Object itemId, Object propertyId) {
163         final Item item = getItem(itemId);
164         if (item != null) {
165             return item.getItemProperty(propertyId);
166         }
167         log.warn("Couldn't find item {} so property {} can't be retrieved!", itemId, propertyId);
168         return null;
169     }
170 
171     @Override
172     public int size() {
173         if (size == 0) {
174             updateSize();
175         }
176         return size;
177     }
178 
179     @Override
180     public boolean containsId(Object itemId) {
181         for (CacheKeyItem cacheKeyItem : itemIndexes.asMap().values()) {
182             if (cacheKeyItem.getItemId().equals(itemId)) {
183                 return true;
184             }
185         }
186         return false;
187     }
188 
189     @Override
190     public Item addItem(Object itemId) throws UnsupportedOperationException {
191         throw new UnsupportedOperationException();
192     }
193 
194     @Override
195     public Object addItem() throws UnsupportedOperationException {
196         throw new UnsupportedOperationException();
197     }
198 
199     @Override
200     public boolean removeItem(Object itemId) throws UnsupportedOperationException {
201         throw new UnsupportedOperationException();
202     }
203 
204     @Override
205     public boolean removeAllItems() throws UnsupportedOperationException {
206         throw new UnsupportedOperationException();
207     }
208 
209     // Container.Ordered
210 
211     @Override
212     public Object nextItemId(Object itemId) {
213         return getIdByIndex(indexOfId(itemId) + 1);
214     }
215 
216     @Override
217     public Object prevItemId(Object itemId) {
218         return getIdByIndex(indexOfId(itemId) - 1);
219     }
220 
221     @Override
222     public Object firstItemId() {
223         if (size == 0) {
224             return null;
225         }
226         if (itemIndexes.getIfPresent(0l) != null) {
227             updateOffsetAndCache(0);
228         }
229         return itemIndexes.getIfPresent(0l).getItemId();
230     }
231 
232     @Override
233     public Object lastItemId() {
234         final Long lastIx = (long) (size - 1);
235         if (!itemIndexes.asMap().containsKey(lastIx)) {
236             updateOffsetAndCache(size - 1);
237         }
238         return itemIndexes.getIfPresent(lastIx).getItemId();
239     }
240 
241     @Override
242     public boolean isFirstId(Object itemId) {
243         return firstItemId().equals(itemId);
244     }
245 
246     @Override
247     public boolean isLastId(Object itemId) {
248         return lastItemId().equals(itemId);
249     }
250 
251     // Container.Indexed
252 
253     @Override
254     public int indexOfId(Object itemId) {
255         if (!containsId(itemId)) {
256             return -1;
257         }
258         boolean wrappedAround = false;
259         while (!wrappedAround) {
260             for (Long i : itemIndexes.asMap().keySet()) {
261                 if (itemIndexes.getIfPresent(i) != null && containsId(itemId) ) {
262                     return i.intValue();
263                 }
264             }
265             // load in the next page.
266             int nextIndex = (currentOffset / (pageLength * cacheRatio) + 1) * (pageLength * cacheRatio);
267             if (nextIndex >= size) {
268                 // Container wrapped around, start from index 0.
269                 wrappedAround = true;
270                 nextIndex = 0;
271             }
272             updateOffsetAndCache(nextIndex);
273         }
274         return -1;
275     }
276 
277     @Override
278     public Object getIdByIndex(int index) {
279         if (index < 0 || index > size - 1) {
280             return null;
281         }
282         final Long idx = (long) index;
283         if (itemIndexes.asMap().containsKey(idx)) {
284             return itemIndexes.getIfPresent(idx).getItemId();
285         }
286         log.debug("item id {} not found in cache. Need to update offset, fetch new item ids from jcr repo and put them in cache.", index);
287         updateOffsetAndCache(index);
288         return itemIndexes.getIfPresent(idx) != null ? itemIndexes.getIfPresent(idx).getItemId() : null;
289     }
290 
291     @Override
292     public List<?> getItemIds(int startIndex, int numberOfItems) {
293         return ContainerHelpers.getItemIdsUsingGetIdByIndex(startIndex, numberOfItems, this);
294     }
295 
296     @Override
297     public Object addItemAt(int index) throws UnsupportedOperationException {
298         throw new UnsupportedOperationException();
299     }
300 
301     @Override
302     public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
303         throw new UnsupportedOperationException();
304     }
305 
306     @Override
307     public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
308         throw new UnsupportedOperationException();
309     }
310 
311     @Override
312     public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
313         throw new UnsupportedOperationException();
314     }
315 
316     // Container.ItemSetChangeNotifier
317 
318     @Override
319     public void addItemSetChangeListener(ItemSetChangeListener listener) {
320         if (itemSetChangeListeners == null) {
321             itemSetChangeListeners = new LinkedHashSet<>();
322         }
323         itemSetChangeListeners.add(listener);
324     }
325 
326     @Override
327     public void addListener(ItemSetChangeListener listener) {
328         addItemSetChangeListener(listener);
329     }
330 
331     @Override
332     public void removeItemSetChangeListener(ItemSetChangeListener listener) {
333         if (itemSetChangeListeners != null) {
334             itemSetChangeListeners.remove(listener);
335             if (itemSetChangeListeners.isEmpty()) {
336                 itemSetChangeListeners = null;
337             }
338         }
339     }
340 
341     @Override
342     public void removeListener(ItemSetChangeListener listener) {
343         removeItemSetChangeListener(listener);
344     }
345 
346     public void fireItemSetChange() {
347         log.debug("Firing item set changed");
348         if (itemSetChangeListeners != null && !itemSetChangeListeners.isEmpty()) {
349             final Container.ItemSetChangeEvent event = new Container.ItemSetChangeEvent() {
350                 @Override
351                 public Container getContainer() {
352                     return CacheBrowserContainer.this;
353                 }
354             };
355             Object[] array = itemSetChangeListeners.toArray();
356             for (Object anArray : array) {
357                 ItemSetChangeListener listener = (ItemSetChangeListener) anArray;
358                 listener.containerItemSetChange(event);
359             }
360         }
361     }
362 
363     // Container.Sortable
364 
365     @Override
366     public void sort(Object[] propertyId, boolean[] ascending) {
367         clearItemIndexes();
368         resetOffset();
369         sortOrder = ascending[0] ? CacheEndpoint.ASCENDING_ORDER : CacheEndpoint.DESCENDING_ORDER;
370         sortProperty = (String) propertyId[0];
371         getPage();
372     }
373 
374     @Override
375     public Collection<String> getSortableContainerPropertyIds() {
376         return sortableProperties;
377     }
378 
379     // Refreshable
380 
381     @Override
382     public void refresh() {
383         size = 0;
384         clearCacheServices();
385         resetOffset();
386         clearItemIndexes();
387         if (MgnlContext.hasAttribute("cache.browser.app.username", Context.SESSION_SCOPE) && MgnlContext.hasAttribute("cache.browser.app.password", Context.SESSION_SCOPE)) {
388             String username = MgnlContext.getAttribute("cache.browser.app.username", Context.SESSION_SCOPE);
389             String password = MgnlContext.getAttribute("cache.browser.app.password", Context.SESSION_SCOPE);
390             initialize(username, password);
391             getPage();
392         }
393         fireItemSetChange();
394     }
395 
396     // private api
397 
398     private void updateOffsetAndCache(int index) {
399         if (itemIndexes.asMap().containsKey((long) index)) {
400             return;
401         }
402         currentOffset = (index / (pageLength * cacheRatio)) * (pageLength * cacheRatio);
403         if (currentOffset < 0) {
404             resetOffset();
405         }
406         getPage();
407     }
408 
409     private void resetOffset() {
410         currentOffset = 0;
411     }
412 
413     private void getPage() {
414         // iterate over all public instances and get cache keys
415         for (Map.Entry<String, CacheService> entry : cacheServices.entrySet()) {
416             String key = entry.getKey();
417             CacheService cacheService = entry.getValue();
418             String json = null;
419             try {
420                 json = cacheService.getKeys(contentConnectorDefinition.getCacheName(), currentOffset, pageLength * cacheRatio, sortOrder, sortProperty);
421             } catch (ClientErrorException e) {
422                 if (e.getResponse().getEntity() != null && e.getResponse().getEntity() instanceof JsonNode) {
423                     JsonNode errorNode = (JsonNode) e.getResponse().getEntity();
424                     log.warn("Error occurred while obtaining cache keys for cache {} from public instance {}: {}", contentConnectorDefinition.getCacheName(), entry.getKey(), errorNode.get(CacheEndpoint.PROPERTY_ERROR_MESSAGE).textValue());
425                 }
426                 log.warn("Error occurred while obtaining cache keys for cache {} from public instance {}: {}", contentConnectorDefinition.getCacheName(), entry.getKey(), e.getMessage());
427                 e.getResponse().close();
428             }
429             if (json != null) {
430                 List<CacheKey> result = (List<CacheKey>) JsonReader.jsonToJava(json);
431                 updateItems(result, key, cacheService);
432             } else {
433                 updateItems(Collections.<CacheKey>emptyList(), null, null);
434             }
435         }
436     }
437 
438     private void updateItems(List<CacheKey> result, String subscriberName, CacheService cacheService) {
439         long start = System.currentTimeMillis();
440         log.debug("Starting iterating over result set");
441         long rowCount = currentOffset;
442         for (CacheKey cacheKey : result) {
443             CacheKeyItem cacheKeyItem = new CacheKeyItem(contentConnectorDefinition.getCacheName(), cacheKey);
444             if (itemIndexes.asMap().containsValue(cacheKeyItem)) {
445                 for (CacheKeyItem item : itemIndexes.asMap().values()) {
446                     if (cacheKeyItem.equals(item)) {
447                         item.addCacheService(subscriberName, cacheService);
448                     }
449                 }
450             } else {
451                 cacheKeyItem.addCacheService(subscriberName, cacheService);
452                 itemIndexes.put(rowCount++, cacheKeyItem);
453             }
454         }
455         log.debug("Done in {} ms", System.currentTimeMillis() - start);
456     }
457 
458     private void updateSize() {
459         for (Map.Entry<String, CacheService> entry : cacheServices.entrySet()) {
460             CacheService cacheService = entry.getValue();
461             int newSize = 0;
462             try {
463                 newSize = cacheService.getCacheSize(contentConnectorDefinition.getCacheName()).get("size").intValue();
464             } catch (ClientErrorException e) {
465                 if (e.getResponse().getEntity() != null && e.getResponse().getEntity() instanceof JsonNode) {
466                     JsonNode errorNode = (JsonNode) e.getResponse().getEntity();
467                     log.warn("Error occurred while obtaining size of the cache with name {} from public instance {}: {}", contentConnectorDefinition.getCacheName(), entry.getKey(), errorNode.get(CacheEndpoint.PROPERTY_ERROR_MESSAGE).textValue());
468                 }
469                 log.warn("Error occurred while obtaining size of the cache with name {} from public instance {}: {}", contentConnectorDefinition.getCacheName(), entry.getKey(), e.getMessage());
470                 size = 0;
471                 e.getResponse().close();
472             }
473             if (size < newSize) {
474                 size = newSize;
475             }
476         }
477     }
478 
479     private void clearItemIndexes() {
480         itemIndexes.invalidateAll();
481     }
482 
483     private void clearCacheServices() {
484         cacheServices.clear();
485     }
486 }