001/*
002 * (C) Copyright 2007-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Maxime Hilaire
016 */
017
018package org.nuxeo.ecm.directory;
019
020import java.io.Serializable;
021import java.util.Arrays;
022import java.util.List;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.nuxeo.ecm.core.api.DocumentModel;
027import org.nuxeo.ecm.core.cache.Cache;
028import org.nuxeo.ecm.core.cache.CacheService;
029import org.nuxeo.runtime.api.Framework;
030import org.nuxeo.runtime.metrics.MetricsService;
031
032import com.codahale.metrics.Counter;
033import com.codahale.metrics.MetricRegistry;
034import com.codahale.metrics.SharedMetricRegistries;
035
036/**
037 * Very simple cache system to cache directory entry lookups (not search queries) on top of nuxeo cache
038 * <p>
039 * Beware that this cache is not transaction aware (which is not a problem for LDAP directories anyway).
040 */
041public class DirectoryCache {
042
043    private static final Serializable CACHE_MISS = Boolean.FALSE;
044
045    protected final String name;
046
047    protected Cache entryCache;
048
049    protected String entryCacheName = null;
050
051    protected Cache entryCacheWithoutReferences;
052
053    protected String entryCacheWithoutReferencesName = null;
054
055    protected boolean negativeCaching;
056
057    protected final MetricRegistry metrics = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
058
059    protected final Counter hitsCounter;
060
061    protected final Counter negativeHitsCounter;
062
063    protected final Counter missesCounter;
064
065    protected final Counter invalidationsCounter;
066
067    protected final Counter maxCounter;
068
069    protected final Counter sizeCounter;
070
071    private final static Log log = LogFactory.getLog(DirectoryCache.class);
072
073    protected DirectoryCache(String name) {
074        this.name = name;
075        hitsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "hits"));
076        negativeHitsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "neghits"));
077        missesCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "misses"));
078        invalidationsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache",
079                "invalidations"));
080        sizeCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "size"));
081        maxCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "max"));
082    }
083
084    protected boolean isCacheEnabled() {
085        return (entryCacheName != null && entryCacheWithoutReferencesName != null);
086    }
087
088    public DocumentModel getEntry(String entryId, EntrySource source) throws DirectoryException {
089        return getEntry(entryId, source, true);
090    }
091
092    public DocumentModel getEntry(String entryId, EntrySource source, boolean fetchReferences)
093            throws DirectoryException {
094        if (!isCacheEnabled()) {
095            return source.getEntryFromSource(entryId, fetchReferences);
096        } else if (isCacheEnabled() && (getEntryCache() == null || getEntryCacheWithoutReferences() == null)) {
097
098            log.warn("Your directory configuration for cache is wrong, directory cache will not be used.");
099            if (getEntryCache() == null) {
100                log.warn(String.format(
101                        "The cache for entry '%s' has not been found, please check the cache name or make sure you have deployed it",
102                        entryCacheName));
103            }
104            if (getEntryCacheWithoutReferences() == null) {
105                log.warn(String.format(
106                        "The cache for entry without references '%s' has not been found, please check the cache name or make sure you have deployed it",
107                        entryCacheWithoutReferencesName));
108            }
109
110            return source.getEntryFromSource(entryId, fetchReferences);
111        }
112
113        Cache cache = fetchReferences ? getEntryCache() : getEntryCacheWithoutReferences();
114        Serializable entry = cache.get(entryId);
115        if (CACHE_MISS.equals(entry)) {
116            negativeHitsCounter.inc();
117            return null;
118        }
119        DocumentModel dm = (DocumentModel) entry;
120        if (dm == null) {
121            // fetch the entry from the backend and cache it for later reuse
122            dm = source.getEntryFromSource(entryId, fetchReferences);
123            if (dm != null) {
124                cache.put(entryId, dm);
125                if (fetchReferences) {
126                    sizeCounter.inc();
127                }
128            } else if (negativeCaching) {
129                cache.put(entryId, CACHE_MISS);
130            }
131            missesCounter.inc();
132        } else {
133            hitsCounter.inc();
134        }
135        try {
136            if (dm == null) {
137                return null;
138            }
139            DocumentModel clone = dm.clone();
140            // DocumentModelImpl#clone does not copy context data, hence
141            // propagate the read-only flag manually
142            if (BaseSession.isReadOnlyEntry(dm)) {
143                BaseSession.setReadOnlyEntry(clone);
144            }
145            return clone;
146        } catch (CloneNotSupportedException e) {
147            // will never happen as long a DocumentModelImpl is used
148            return dm;
149        }
150    }
151
152    public void invalidate(List<String> entryIds) {
153        if (isCacheEnabled()) {
154            synchronized (this) {
155                for (String entryId : entryIds) {
156                    getEntryCache().invalidate(entryId);
157                    getEntryCacheWithoutReferences().invalidate(entryId);
158                    sizeCounter.dec();
159                    invalidationsCounter.inc();
160                }
161            }
162        }
163    }
164
165    public void invalidate(String... entryIds) {
166        invalidate(Arrays.asList(entryIds));
167    }
168
169    public void invalidateAll() {
170        if (isCacheEnabled()) {
171            synchronized (this) {
172                long count = sizeCounter.getCount();
173                sizeCounter.dec(count);
174                invalidationsCounter.inc(count);
175                getEntryCache().invalidateAll();
176                getEntryCacheWithoutReferences().invalidateAll();
177            }
178        }
179    }
180
181    public void setEntryCacheName(String entryCacheName) {
182        this.entryCacheName = entryCacheName;
183    }
184
185    public void setEntryCacheWithoutReferencesName(String entryCacheWithoutReferencesName) {
186        this.entryCacheWithoutReferencesName = entryCacheWithoutReferencesName;
187    }
188
189    public void setNegativeCaching(Boolean negativeCaching) {
190        this.negativeCaching = Boolean.TRUE.equals(negativeCaching);
191    }
192
193    public Cache getEntryCache() {
194        if (entryCache == null) {
195            entryCache = Framework.getService(CacheService.class).getCache(entryCacheName);
196        }
197        return entryCache;
198    }
199
200    public Cache getEntryCacheWithoutReferences() {
201
202        if (entryCacheWithoutReferences == null) {
203            entryCacheWithoutReferences = Framework.getService(CacheService.class).getCache(
204                    entryCacheWithoutReferencesName);
205        }
206        return entryCacheWithoutReferences;
207    }
208
209}