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