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.IOException;
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    protected final String name;
044
045    protected Cache entryCache;
046
047    protected String entryCacheName = null;
048
049    protected Cache entryCacheWithoutReferences;
050
051    protected String entryCacheWithoutReferencesName = null;
052
053    protected final MetricRegistry metrics = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
054
055    protected final Counter hitsCounter;
056
057    protected final Counter invalidationsCounter;
058
059    protected final Counter maxCounter;
060
061    protected final Counter sizeCounter;
062
063    private final static Log log = LogFactory.getLog(DirectoryCache.class);
064
065    protected DirectoryCache(String name) {
066        this.name = name;
067        hitsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "hits"));
068        invalidationsCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache",
069                "invalidations"));
070        sizeCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "size"));
071        maxCounter = metrics.counter(MetricRegistry.name("nuxeo", "directories", name, "cache", "max"));
072    }
073
074    protected boolean isCacheEnabled() {
075        return (entryCacheName != null && entryCacheWithoutReferencesName != null);
076    }
077
078    public DocumentModel getEntry(String entryId, EntrySource source) throws DirectoryException {
079        return getEntry(entryId, source, true);
080    }
081
082    public DocumentModel getEntry(String entryId, EntrySource source, boolean fetchReferences)
083            throws DirectoryException {
084        if (!isCacheEnabled()) {
085            return source.getEntryFromSource(entryId, fetchReferences);
086        } else if (isCacheEnabled() && (getEntryCache() == null || getEntryCacheWithoutReferences() == null)) {
087
088            log.warn("Your directory configuration for cache is wrong, directory cache will not be used.");
089            if (getEntryCache() == null) {
090                log.warn(String.format(
091                        "The cache for entry '%s' has not been found, please check the cache name or make sure you have deployed it",
092                        entryCacheName));
093            }
094            if (getEntryCacheWithoutReferences() == null) {
095                log.warn(String.format(
096                        "The cache for entry without references '%s' has not been found, please check the cache name or make sure you have deployed it",
097                        entryCacheWithoutReferencesName));
098            }
099
100            return source.getEntryFromSource(entryId, fetchReferences);
101        }
102
103        DocumentModel dm = null;
104        if (fetchReferences) {
105            dm = (DocumentModel) getEntryCache().get(entryId);
106            if (dm == null) {
107                // fetch the entry from the backend and cache it for later
108                // reuse
109                dm = source.getEntryFromSource(entryId, fetchReferences);
110                if (dm != null) {
111                    getEntryCache().put(entryId, dm);
112                    sizeCounter.inc();
113                }
114            } else {
115                hitsCounter.inc();
116            }
117        } else {
118            dm = (DocumentModel) getEntryCacheWithoutReferences().get(entryId);
119            if (dm == null) {
120                // fetch the entry from the backend and cache it for later
121                // reuse
122                dm = source.getEntryFromSource(entryId, fetchReferences);
123                if (dm != null) {
124                    getEntryCacheWithoutReferences().put(entryId, dm);
125                }
126            } else {
127                hitsCounter.inc();
128            }
129        }
130        try {
131            if (dm == null) {
132                return null;
133            }
134            DocumentModel clone = dm.clone();
135            // DocumentModelImpl#clone does not copy context data, hence
136            // propagate the read-only flag manually
137            if (BaseSession.isReadOnlyEntry(dm)) {
138                BaseSession.setReadOnlyEntry(clone);
139            }
140            return clone;
141        } catch (CloneNotSupportedException e) {
142            // will never happen as long a DocumentModelImpl is used
143            return dm;
144        }
145    }
146
147    public void invalidate(List<String> entryIds) {
148        if (isCacheEnabled()) {
149            synchronized (this) {
150                for (String entryId : entryIds) {
151                    getEntryCache().invalidate(entryId);
152                    getEntryCacheWithoutReferences().invalidate(entryId);
153                    sizeCounter.dec();
154                    invalidationsCounter.inc();
155                }
156            }
157        }
158    }
159
160    public void invalidate(String... entryIds) {
161        invalidate(Arrays.asList(entryIds));
162    }
163
164    public void invalidateAll() {
165        if (isCacheEnabled()) {
166            synchronized (this) {
167                long count = sizeCounter.getCount();
168                sizeCounter.dec(count);
169                invalidationsCounter.inc(count);
170                getEntryCache().invalidateAll();
171                getEntryCacheWithoutReferences().invalidateAll();
172            }
173        }
174    }
175
176    public void setEntryCacheName(String entryCacheName) {
177        this.entryCacheName = entryCacheName;
178    }
179
180    public void setEntryCacheWithoutReferencesName(String entryCacheWithoutReferencesName) {
181        this.entryCacheWithoutReferencesName = entryCacheWithoutReferencesName;
182    }
183
184    public Cache getEntryCache() {
185        if (entryCache == null) {
186            entryCache = Framework.getService(CacheService.class).getCache(entryCacheName);
187        }
188        return entryCache;
189    }
190
191    public Cache getEntryCacheWithoutReferences() {
192
193        if (entryCacheWithoutReferences == null) {
194            entryCacheWithoutReferences = Framework.getService(CacheService.class).getCache(
195                    entryCacheWithoutReferencesName);
196        }
197        return entryCacheWithoutReferences;
198    }
199
200}