/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package com.celements.store; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import org.python.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.suigeneris.jrcs.rcs.Version; import org.xwiki.cache.Cache; import org.xwiki.cache.CacheException; import org.xwiki.cache.CacheFactory; import org.xwiki.cache.CacheManager; import org.xwiki.cache.config.CacheConfiguration; import org.xwiki.cache.eviction.EntryEvictionConfiguration; import org.xwiki.cache.eviction.LRUEvictionConfiguration; import org.xwiki.component.annotation.Component; import org.xwiki.component.annotation.Requirement; import org.xwiki.component.manager.ComponentLookupException; import org.xwiki.configuration.ConfigurationSource; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.SpaceReference; import org.xwiki.model.reference.WikiReference; import org.xwiki.query.Query; import org.xwiki.query.QueryException; import org.xwiki.query.QueryManager; import com.celements.model.access.exception.MetaDataLoadException; import com.celements.model.classes.metadata.DocumentMetaData; import com.celements.model.classes.metadata.ImmutableDocumentMetaData; import com.celements.model.context.ModelContext; import com.celements.model.util.ModelUtils; import com.celements.model.util.References; import com.google.common.base.Optional; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.doc.XWikiLink; import com.xpn.xwiki.doc.XWikiLock; import com.xpn.xwiki.objects.classes.BaseClass; import com.xpn.xwiki.store.XWikiCacheStoreInterface; import com.xpn.xwiki.store.XWikiStoreInterface; import com.xpn.xwiki.web.Utils; /** * A proxy store implementation that caches Documents when they are first fetched and subsequently * return them from a * cache. It delegates all write and search operations to an underlying store without doing any * caching on them. * * @version $Id$ */ @Component(DocumentCacheStore.COMPONENT_NAME) public class DocumentCacheStore implements XWikiCacheStoreInterface, MetaDataStoreExtension { private final static Logger LOGGER = LoggerFactory.getLogger(DocumentCacheStore.class); private final static Logger LOGGER_DL = LoggerFactory.getLogger(DocumentLoader.class); public static final String COMPONENT_NAME = "DocumentCacheStore"; public static final String PARAM_DOC_CACHE_CAPACITY = "xwiki.store.cache.capacity"; public static final String PARAM_EXIST_CACHE_CAPACITY = "xwiki.store.cache.pageexistcapacity"; public static final String BACKING_STORE_STRATEGY = "celements.store.cache.storeStrategy"; @Requirement("xwikiproperties") private ConfigurationSource config; @Requirement private CacheManager cacheManager; @Requirement private ModelContext context; @Requirement private ModelUtils modelUtils; /** * Lazy initialized according to backing store strategy configuration. * The store field is immutable in the following way: * even though it could be initialized in multiple threads because of missing memory visibility, * it always results in the same value. Thus a thread could not tell if it was multiple times * initialized. Hence no volatile modificator is needed. */ private XWikiStoreInterface store; /** * CAUTION: Lazy initialized of cache thus volatile is needed. */ private volatile Cache<XWikiDocument> docCache; /** * CAUTION: Lazy initialized of cache thus volatile is needed. */ private volatile Cache<Boolean> existCache; private final ConcurrentMap<String, DocumentLoader> documentLoaderMap = new ConcurrentHashMap<>(); void initalize() { if ((this.docCache == null) || (this.existCache == null)) { synchronized (this) { try { if (this.docCache == null) { initializeDocCache(); } if (this.existCache == null) { initializeExistCache(); } } catch (CacheException cacheExp) { LOGGER.error("Failed to initialize document cache.", cacheExp); throw new RuntimeException("FATAL: Failed to initialize document cache.", cacheExp); } } } } private void initializeExistCache() throws CacheException { CacheConfiguration config = new CacheConfiguration(); config.setConfigurationId("xwiki.store.pageexistcache"); LRUEvictionConfiguration lru = new LRUEvictionConfiguration(); lru.setMaxEntries(getExistCacheCapacity()); config.put(EntryEvictionConfiguration.CONFIGURATIONID, lru); existCache = getCacheFactory().newCache(config); } private CacheFactory getCacheFactory() { try { return cacheManager.getCacheFactory(); } catch (ComponentLookupException exp) { throw new RuntimeException("Failed to get cache factory component", exp); } } private int getExistCacheCapacity() { int existCacheCapacity = 10000; String existsCapacity = context.getXWikiContext().getWiki().Param(PARAM_EXIST_CACHE_CAPACITY); if (existsCapacity != null) { try { existCacheCapacity = Integer.parseInt(existsCapacity); } catch (NumberFormatException exp) { LOGGER.warn("Failed to read '{}' using default '{}'", PARAM_EXIST_CACHE_CAPACITY, existCacheCapacity, exp); } } int docCacheCapacity = getDocCacheCapacity(); if (existCacheCapacity < docCacheCapacity) { LOGGER.warn("WARNING: document exists cache capacity is smaller configured than docCache " + "capacity. Ignoring exists cache configuration '{}' and using doc cache capacity '{}'" + " instead.", existCacheCapacity, docCacheCapacity); existCacheCapacity = docCacheCapacity; } return existCacheCapacity; } private void initializeDocCache() throws CacheException { CacheConfiguration config = new CacheConfiguration(); config.setConfigurationId("xwiki.store.pagecache"); LRUEvictionConfiguration lru = new LRUEvictionConfiguration(); lru.setMaxEntries(getDocCacheCapacity()); config.put(EntryEvictionConfiguration.CONFIGURATIONID, lru); docCache = getCacheFactory().newCache(config); } private int getDocCacheCapacity() { int docCacheCapacity = 100; String capacity = context.getXWikiContext().getWiki().Param(PARAM_DOC_CACHE_CAPACITY); if (capacity != null) { try { docCacheCapacity = Integer.parseInt(capacity); } catch (NumberFormatException exp) { LOGGER.warn("Failed to read xwiki.store.cache.capacity using default '{}'", docCacheCapacity, exp); } } return docCacheCapacity; } @Override public void initCache(int docCacheCapacity, int existCacheCapacity, XWikiContext context) throws XWikiException { LOGGER.info("initCache externally called. This is not supported. The document cache initializes" + " automatically on start."); } @Override public XWikiStoreInterface getStore() { // return default store here because XWiki.getHibernateStore() relies on it return Utils.getComponent(XWikiStoreInterface.class); } public XWikiStoreInterface getBackingStore() { if (this.store == null) { String backingStoreHint = getBackingStoreHint(); setStore(Utils.getComponent(XWikiStoreInterface.class, backingStoreHint)); LOGGER.info("backing store initialized '{}' for hint '{}'", this.store, backingStoreHint); } return this.store; } private String getBackingStoreHint() { String strategy = config.getProperty(BACKING_STORE_STRATEGY, "default"); if (COMPONENT_NAME.equals(strategy)) { strategy = "default"; } LOGGER.debug("get backing store hint '{}'", strategy); return strategy; } @Override public synchronized void setStore(XWikiStoreInterface store) { this.store = store; } @Override public void saveXWikiDoc(XWikiDocument doc, XWikiContext context) throws XWikiException { saveXWikiDoc(doc, context, true); } @Override public void saveXWikiDoc(XWikiDocument doc, XWikiContext context, boolean bTransaction) throws XWikiException { getBackingStore().saveXWikiDoc(doc, context, bTransaction); doc.setStore(this.store); removeDocFromCache(doc, true); } @Override public synchronized void flushCache() { LOGGER.warn("flushCache may lead to serious memory visibility problems."); if (this.docCache != null) { this.docCache.dispose(); this.docCache = null; } if (this.existCache != null) { this.existCache.dispose(); this.existCache = null; } } String getKey(DocumentReference docRef) { DocumentReference cacheDocRef = new DocumentReference(context.getWikiRef().getName(), docRef.getLastSpaceReference().getName(), docRef.getName()); return modelUtils.serializeRef(cacheDocRef); } String getKeyWithLang(DocumentReference docRef, String language) { if (Strings.isNullOrEmpty(language)) { return getKey(docRef); } else { return getKey(docRef) + ":" + language; } } String getKeyWithLang(XWikiDocument doc) { String language = doc.getLanguage(); if (language.isEmpty() || language.equals(doc.getDefaultLanguage())) { language = ""; } return getKeyWithLang(doc.getDocumentReference(), language); } private DocumentLoader getDocumentLoader(String key) { DocumentLoader docLoader = documentLoaderMap.get(key); if (docLoader == null) { LOGGER.debug("create document loader for '{}' in thread '{}'", key, Thread.currentThread().getId()); docLoader = new DocumentLoader(key); DocumentLoader setDocLoader = documentLoaderMap.putIfAbsent(key, docLoader); if (setDocLoader != null) { docLoader = setDocLoader; LOGGER.info("replace with existing from map for key '{}' in thread '{}'", key, Thread.currentThread().getId()); } } return docLoader; } InvalidateState removeDocFromCache(XWikiDocument doc, Boolean docExists) { InvalidateState returnState = InvalidateState.CACHE_MISS; Set<String> docKeys = new HashSet<>(); String key = getKey(doc.getDocumentReference()); String origKey = ""; if (doc.getOriginalDocument() != null) { origKey = getKey(doc.getOriginalDocument().getDocumentReference()); docKeys.add(origKey); docKeys.add(getKeyWithLang(doc.getOriginalDocument())); } docKeys.add(key); docKeys.add(getKeyWithLang(doc)); for (String k : docKeys) { InvalidateState invState = invalidateDocCache(k); if (invState == InvalidateState.REMOVED) { returnState = InvalidateState.REMOVED; } else if (returnState != InvalidateState.REMOVED) { returnState = invState; } } if (getExistCache() != null) { if ((doc.getTranslation() == 0) || (docExists == Boolean.TRUE)) { getExistCache().remove(origKey); updateExistsCache(key, docExists); } updateExistsCache(getKeyWithLang(doc), docExists); } return returnState; } private void updateExistsCache(String key, Boolean docExists) { if (docExists == null) { getExistCache().remove(key); } else { getExistCache().set(key, docExists); } } InvalidateState invalidateDocCache(String key) { InvalidateState invalidState = InvalidateState.CACHE_MISS; final DocumentLoader docLoader = documentLoaderMap.get(key); boolean invalidateDocLoader = (docLoader != null); if (invalidateDocLoader) { invalidState = docLoader.invalidate(); } XWikiDocument oldCachedDoc = null; if (getDocCache() != null) { oldCachedDoc = getDocFromCache(key); if (oldCachedDoc != null) { synchronized (oldCachedDoc) { oldCachedDoc = getDocFromCache(key); if (oldCachedDoc != null) { getDocCache().remove(key); invalidState = InvalidateState.REMOVED; } } } } return invalidState; } @Override public XWikiDocument loadXWikiDoc(XWikiDocument doc, XWikiContext context) throws XWikiException { LOGGER.trace("Cache: begin for docRef '{}' in cache", doc.getDocumentReference()); String key = getKey(doc.getDocumentReference()); String keyWithLang = getKeyWithLang(doc); if (doesNotExistsForKey(key) || doesNotExistsForKey(keyWithLang)) { LOGGER.debug("Cache: The document {} does not exist, return an empty one", keyWithLang); doc.setStore(getBackingStore()); doc.setNew(true); // Make sure to always return a document with an original version, even for one that does // not exist. This allows to write more generic code. doc.setOriginalDocument(new XWikiDocument(doc.getDocumentReference())); return doc; } else { LOGGER.debug("Cache: Trying to get doc '{}' from cache", keyWithLang); XWikiDocument cachedoc = getDocFromCache(keyWithLang); if (cachedoc != null) { LOGGER.debug("Cache: got doc '{}' from cache", keyWithLang); } else { cachedoc = getDocumentLoader(keyWithLang).loadDocument(keyWithLang, doc, context); } LOGGER.trace("Cache: end for doc '{}' in cache", keyWithLang); return cachedoc; } } private boolean doesNotExistsForKey(String key) { return getExistCache().get(key) == Boolean.FALSE; } /** * getCache is private, thus for tests we need getDocFromCache to check the cache state */ XWikiDocument getDocFromCache(String key) { return getDocCache().get(key); } @Override public void deleteXWikiDoc(XWikiDocument doc, XWikiContext context) throws XWikiException { getBackingStore().deleteXWikiDoc(doc, context); removeDocFromCache(doc, false); } @Override public List<String> getClassList(XWikiContext context) throws XWikiException { return getBackingStore().getClassList(context); } @Override public int countDocuments(String wheresql, XWikiContext context) throws XWikiException { return getBackingStore().countDocuments(wheresql, context); } @Override public List<DocumentReference> searchDocumentReferences(String wheresql, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentReferences(wheresql, context); } @Override @Deprecated public List<String> searchDocumentsNames(String wheresql, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentsNames(wheresql, context); } @Override public List<DocumentReference> searchDocumentReferences(String wheresql, int nb, int start, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentReferences(wheresql, nb, start, context); } @Override @Deprecated public List<String> searchDocumentsNames(String wheresql, int nb, int start, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentsNames(wheresql, nb, start, context); } @Override public List<DocumentReference> searchDocumentReferences(String wheresql, int nb, int start, String selectColumns, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentReferences(wheresql, nb, start, selectColumns, context); } @Override @Deprecated public List<String> searchDocumentsNames(String wheresql, int nb, int start, String selectColumns, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentsNames(wheresql, nb, start, selectColumns, context); } @Override public List<DocumentReference> searchDocumentReferences(String parametrizedSqlClause, int nb, int start, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentReferences(parametrizedSqlClause, nb, start, parameterValues, context); } @Override @Deprecated public List<String> searchDocumentsNames(String parametrizedSqlClause, int nb, int start, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentsNames(parametrizedSqlClause, nb, start, parameterValues, context); } @Override public List<DocumentReference> searchDocumentReferences(String parametrizedSqlClause, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentReferences(parametrizedSqlClause, parameterValues, context); } @Override @Deprecated public List<String> searchDocumentsNames(String parametrizedSqlClause, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocumentsNames(parametrizedSqlClause, parameterValues, context); } @Override public boolean isCustomMappingValid(BaseClass bclass, String custommapping1, XWikiContext context) throws XWikiException { return getBackingStore().isCustomMappingValid(bclass, custommapping1, context); } @Override public boolean injectCustomMapping(BaseClass doc1class, XWikiContext context) throws XWikiException { return getBackingStore().injectCustomMapping(doc1class, context); } @Override public boolean injectCustomMappings(XWikiDocument doc, XWikiContext context) throws XWikiException { return getBackingStore().injectCustomMappings(doc, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbyname, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbyname, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbyname, boolean customMapping, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbyname, customMapping, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbyname, int nb, int start, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbyname, nb, start, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbyname, boolean customMapping, int nb, int start, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbyname, customMapping, nb, start, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, int nb, int start, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, nb, start, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbyname, boolean customMapping, boolean checkRight, int nb, int start, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbyname, customMapping, checkRight, nb, start, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbylanguage, int nb, int start, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbylanguage, nb, start, parameterValues, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, parameterValues, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbylanguage, boolean customMapping, int nb, int start, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbylanguage, customMapping, nb, start, parameterValues, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, int nb, int start, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, nb, start, parameterValues, context); } @Override public List<XWikiDocument> searchDocuments(String wheresql, boolean distinctbylanguage, boolean customMapping, boolean checkRight, int nb, int start, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().searchDocuments(wheresql, distinctbylanguage, customMapping, checkRight, nb, start, parameterValues, context); } @Override public int countDocuments(String parametrizedSqlClause, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().countDocuments(parametrizedSqlClause, parameterValues, context); } @Override public XWikiLock loadLock(long docId, XWikiContext context, boolean bTransaction) throws XWikiException { return getBackingStore().loadLock(docId, context, bTransaction); } @Override public void saveLock(XWikiLock lock, XWikiContext context, boolean bTransaction) throws XWikiException { getBackingStore().saveLock(lock, context, bTransaction); } @Override public void deleteLock(XWikiLock lock, XWikiContext context, boolean bTransaction) throws XWikiException { getBackingStore().deleteLock(lock, context, bTransaction); } @Override public List<XWikiLink> loadLinks(long docId, XWikiContext context, boolean bTransaction) throws XWikiException { return getBackingStore().loadLinks(docId, context, bTransaction); } @Override public List<DocumentReference> loadBacklinks(DocumentReference documentReference, boolean bTransaction, XWikiContext context) throws XWikiException { return getBackingStore().loadBacklinks(documentReference, bTransaction, context); } @Override @Deprecated public List<String> loadBacklinks(String fullName, XWikiContext context, boolean bTransaction) throws XWikiException { return getBackingStore().loadBacklinks(fullName, context, bTransaction); } @Override public void saveLinks(XWikiDocument doc, XWikiContext context, boolean bTransaction) throws XWikiException { getBackingStore().saveLinks(doc, context, bTransaction); } @Override public void deleteLinks(long docId, XWikiContext context, boolean bTransaction) throws XWikiException { getBackingStore().deleteLinks(docId, context, bTransaction); } @Override public <T> List<T> search(String sql, int nb, int start, XWikiContext context) throws XWikiException { return getBackingStore().search(sql, nb, start, context); } @Override public <T> List<T> search(String sql, int nb, int start, Object[][] whereParams, XWikiContext context) throws XWikiException { return getBackingStore().search(sql, nb, start, whereParams, context); } @Override public <T> List<T> search(String sql, int nb, int start, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().search(sql, nb, start, parameterValues, context); } @Override public <T> List<T> search(String sql, int nb, int start, Object[][] whereParams, List<?> parameterValues, XWikiContext context) throws XWikiException { return getBackingStore().search(sql, nb, start, whereParams, parameterValues, context); } @Override public synchronized void cleanUp(XWikiContext context) { getBackingStore().cleanUp(context); } @Override public boolean isWikiNameAvailable(String wikiName, XWikiContext context) throws XWikiException { return getBackingStore().isWikiNameAvailable(wikiName, context); } @Override public synchronized void createWiki(String wikiName, XWikiContext context) throws XWikiException { getBackingStore().createWiki(wikiName, context); } @Override public synchronized void deleteWiki(String wikiName, XWikiContext context) throws XWikiException { getBackingStore().deleteWiki(wikiName, context); flushCache(); } @Override public boolean exists(XWikiDocument doc, XWikiContext context) throws XWikiException { String key = getKey(doc.getDocumentReference()); Boolean result = getExistCache().get(key); if (result == null) { result = (getDocCache().get(key) != null); if (!result) { result = getBackingStore().exists(doc, context); } getExistCache().set(key, result); } LOGGER.trace("exists return '{}' for '{}'", result, key); return result; } private Cache<XWikiDocument> getDocCache() { initalize(); // make sure cache is initialized return this.docCache; } private Cache<Boolean> getExistCache() { initalize(); // make sure cache is initialized return this.existCache; } @Override public List<String> getCustomMappingPropertyList(BaseClass bclass) { return getBackingStore().getCustomMappingPropertyList(bclass); } @Override public synchronized void injectCustomMappings(XWikiContext context) throws XWikiException { getBackingStore().injectCustomMappings(context); } @Override public void injectUpdatedCustomMappings(XWikiContext context) throws XWikiException { getBackingStore().injectUpdatedCustomMappings(context); } @Override public List<String> getTranslationList(XWikiDocument doc, XWikiContext context) throws XWikiException { return getBackingStore().getTranslationList(doc, context); } @Override public QueryManager getQueryManager() { return getBackingStore().getQueryManager(); } static enum InvalidateState { CACHE_MISS, REMOVED, LOADING_CANCELED, LOADING_MULTI_CANCELED, LOADING_CANCEL_FAILED } private static final int _DOCSTATE_LOADING = 0; private static final int _DOCSTATE_FINISHED = Integer.MAX_VALUE; private class DocumentLoader { private volatile XWikiDocument loadedDoc; private final String key; /** * if loadingState equals _DOCSTATE_LOADING than a valid loading is about to start or in process * if loadingState is lower _DOCSTATE_LOADING than a loading has been successful canceled and a * reload will take place * if loadingState equals _DOCSTATE_FINISHED or is at least greater _DOCSTATE_LOADING loading * finished before any canceling happened */ private final AtomicInteger loadingState = new AtomicInteger(_DOCSTATE_LOADING); private DocumentLoader(String key) { this.key = key; } private InvalidateState invalidate() { InvalidateState invalidState; int beforeState = loadingState.getAndDecrement(); if (beforeState < 0) { if (loadedDoc != null) { LOGGER_DL.warn("should not happen: possible lifelock! {}", this.key); } invalidState = InvalidateState.LOADING_MULTI_CANCELED; } else if (beforeState == 0) { invalidState = InvalidateState.LOADING_CANCELED; } else { invalidState = InvalidateState.LOADING_CANCEL_FAILED; } boolean succInvalidated = beforeState <= 0; LOGGER_DL.debug("invalidated cache during loading document. {}", succInvalidated); return invalidState; } /** * IMPORTANT: do not change anything on the synchronization of this method. * It is a very delicate case and very likely memory visibility breaks in less than 1 out of * 100'000 document loads. Thus it is difficult to test for correctness. */ private XWikiDocument loadDocument(String key, XWikiDocument doc, XWikiContext context) throws XWikiException { checkArgument(key); if (loadedDoc == null) { synchronized (this) { if (loadedDoc == null) { // if a thread is just between the document cache miss and getting the documentLoader // when the documentLoader removes itself from the map, then a new documentLoader is // generated. Therefore we double check here that still no document is in cache. loadedDoc = getDocCache().get(key); if (loadedDoc == null) { XWikiDocument newDoc = null; do { if ((loadingState.getAndSet(_DOCSTATE_LOADING) < _DOCSTATE_LOADING) && (newDoc != null)) { LOGGER_DL.info("DocumentLoader-{}: invalidated docloader '{}' reloading", Thread.currentThread().getId(), key); } // use a further synchronized method call to prevent an unsafe publication of the // new document over the cache newDoc = new DocumentBuilder().buildDocument(key, doc, context); } while (!loadingState.compareAndSet(_DOCSTATE_LOADING, _DOCSTATE_FINISHED)); LOGGER_DL.debug("DocumentLoader-{}: put doc '{}' in cache", Thread.currentThread().getId(), key); final String keyWithLang = getKeyWithLang(newDoc); if (!newDoc.isNew()) { getDocCache().set(keyWithLang, newDoc); getExistCache().set(getKey(newDoc.getDocumentReference()), true); getExistCache().set(keyWithLang, true); } else { LOGGER_DL.debug("DocumentLoader-{}: loading '{}' failed. Setting exists" + " to FALSE for '{}'", Thread.currentThread().getId(), key, keyWithLang); getExistCache().set(keyWithLang, false); } loadedDoc = newDoc; } else { LOGGER_DL.debug("DocumentLoader-{}: found in cache skip loding for '{}'", Thread.currentThread().getId(), key); } documentLoaderMap.remove(key); } } } return loadedDoc; } private void checkArgument(String key) { if (!this.key.equals(key)) { throw new RuntimeException( "DocumentLoader illegally used with a different key (registered key:" + this.key + ", loading doc key: " + key + ")."); } } private class DocumentBuilder { private synchronized XWikiDocument buildDocument(String key, XWikiDocument doc, XWikiContext context) throws XWikiException { LOGGER_DL.trace("DocumentLoader-{}: Trying to get doc '{}' for real", Thread.currentThread().getId(), key); // IMPORTANT: do not clone here. Creating new document is much faster. XWikiDocument buildDoc = new XWikiDocument(doc.getDocumentReference()); buildDoc.setLanguage(doc.getLanguage()); buildDoc = getBackingStore().loadXWikiDoc(buildDoc, context); buildDoc.setStore(getBackingStore()); buildDoc.setFromCache(!buildDoc.isNew()); return buildDoc; } } } @Override public Set<DocumentMetaData> listDocumentMetaData(EntityReference filterRef) { Set<DocumentMetaData> ret = new LinkedHashSet<>(); try { Query query = buildDocumentMetaDataQuery(filterRef); for (Object[] docData : query.<Object[]>execute()) { DocumentReference docRef = new DocumentReference(query.getWiki(), (String) docData[1], (String) docData[0]); ret.add(new ImmutableDocumentMetaData.Builder(docRef).language((String) docData[2]).version( new Version((String) docData[3])).build()); } LOGGER.info("listDocumentMetaData: found {} docs with hql '{}' for filterRef '{}'", ret.size(), filterRef); } catch (QueryException exc) { throw new MetaDataLoadException(filterRef, exc); } if (getBackingStore() instanceof MetaDataStoreExtension) { ret.addAll(((MetaDataStoreExtension) getBackingStore()).listDocumentMetaData(filterRef)); } return ret; } private Query buildDocumentMetaDataQuery(EntityReference filterRef) throws QueryException { StringBuilder sb = new StringBuilder(); sb.append("select distinct doc.name, doc.space, doc.language, doc.version " + "from XWikiDocument as doc"); Map<String, String> bindValues = new HashMap<>(); Optional<SpaceReference> spaceRef = References.extractRef(filterRef, SpaceReference.class); Optional<DocumentReference> docRef = References.extractRef(filterRef, DocumentReference.class); if (spaceRef.isPresent()) { sb.append(" where doc.space = :spaceName"); bindValues.put("spaceName", spaceRef.get().getName()); if (docRef.isPresent()) { sb.append(" and doc.name = :docName"); bindValues.put("docName", docRef.get().getName()); } } String hql = sb.toString(); Query query = getQueryManager().createQuery(hql, Query.HQL); query.setWiki(References.extractRef(filterRef, WikiReference.class).or( context.getWikiRef()).getName()); for (Entry<String, String> bind : bindValues.entrySet()) { query.bindValue(bind.getKey(), bind.getValue()); } return query; } }