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