001/*
002 * (C) Copyright 2006-2018 Nuxeo (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 *     Nuxeo - initial API and implementation
018 *
019 */
020
021package org.nuxeo.ecm.directory;
022
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031
032import org.apache.commons.lang3.StringUtils;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.DocumentModelComparator;
035import org.nuxeo.ecm.core.cache.CacheService;
036import org.nuxeo.ecm.core.schema.SchemaManager;
037import org.nuxeo.ecm.core.schema.types.Field;
038import org.nuxeo.ecm.core.schema.types.Schema;
039import org.nuxeo.ecm.directory.api.DirectoryDeleteConstraint;
040import org.nuxeo.runtime.api.Framework;
041import org.nuxeo.runtime.metrics.MetricsService;
042
043import com.codahale.metrics.Counter;
044import com.codahale.metrics.MetricRegistry;
045import com.codahale.metrics.SharedMetricRegistries;
046
047public abstract class AbstractDirectory implements Directory {
048
049    public static final String TENANT_ID_FIELD = "tenantId";
050
051    public final BaseDirectoryDescriptor descriptor;
052
053    protected DirectoryFieldMapper fieldMapper;
054
055    protected final Map<String, List<Reference>> references = new HashMap<>();
056
057    // simple cache system for entry lookups, disabled by default
058    protected final DirectoryCache cache;
059
060    // @since 5.7
061    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
062
063    protected final Counter sessionCount;
064
065    protected final Counter sessionMaxCount;
066
067    protected Map<String, Field> schemaFieldMap;
068
069    protected List<String> types = new ArrayList<>();
070
071    protected Class<? extends Reference> referenceClass;
072
073    protected AbstractDirectory(BaseDirectoryDescriptor descriptor, Class<? extends Reference> referenceClass) {
074        this.referenceClass = referenceClass;
075        this.descriptor = descriptor;
076        // is the directory visible in the ui
077        if (descriptor.types != null) {
078            this.types = Arrays.asList(descriptor.types);
079        }
080        if (!descriptor.template && doSanityChecks()) {
081            if (StringUtils.isEmpty(descriptor.idField)) {
082                throw new DirectoryException("idField configuration is missing for directory: " + getName());
083            }
084            if (StringUtils.isEmpty(descriptor.schemaName)) {
085                throw new DirectoryException("schema configuration is missing for directory " + getName());
086            }
087        }
088
089        sessionCount = registry.counter(MetricRegistry.name("nuxeo", "directories", getName(), "sessions", "active"));
090        sessionMaxCount = registry.counter(MetricRegistry.name("nuxeo", "directories", getName(), "sessions", "max"));
091
092        // add references
093        addInverseReferences(descriptor.getInverseReferences());
094        addReferences(descriptor.getReferences());
095
096        // cache parameterization
097        cache = new DirectoryCache(getName());
098        cache.setEntryCacheName(descriptor.cacheEntryName);
099        cache.setEntryCacheWithoutReferencesName(descriptor.cacheEntryWithoutReferencesName);
100        cache.setNegativeCaching(descriptor.negativeCaching);
101
102    }
103
104    protected boolean doSanityChecks() {
105        return true;
106    }
107
108    @Override
109    public String getName() {
110        return descriptor.name;
111    }
112
113    @Override
114    public String getSchema() {
115        return descriptor.schemaName;
116    }
117
118    @Override
119    public String getParentDirectory() {
120        return descriptor.parentDirectory;
121    }
122
123    @Override
124    public String getIdField() {
125        return descriptor.idField;
126    }
127
128    @Override
129    public String getPasswordField() {
130        return descriptor.passwordField;
131    }
132
133    @Override
134    public boolean isReadOnly() {
135        return descriptor.isReadOnly();
136    }
137
138    public void setReadOnly(boolean readOnly) {
139        descriptor.setReadOnly(readOnly);
140    }
141
142    @Override
143    public void invalidateCaches() throws DirectoryException {
144        cache.invalidateAll();
145        for (Reference ref : getReferences()) {
146            Directory targetDir = ref.getTargetDirectory();
147            if (targetDir != null) {
148                targetDir.invalidateDirectoryCache();
149            }
150        }
151    }
152
153    public DirectoryFieldMapper getFieldMapper() {
154        if (fieldMapper == null) {
155            fieldMapper = new DirectoryFieldMapper();
156        }
157        return fieldMapper;
158    }
159
160    @Deprecated
161    @Override
162    public Reference getReference(String referenceFieldName) {
163        List<Reference> refs = getReferences(referenceFieldName);
164        if (refs == null || refs.isEmpty()) {
165            return null;
166        } else if (refs.size() == 1) {
167            return refs.get(0);
168        } else {
169            throw new DirectoryException(
170                    "Unexpected multiple references for " + referenceFieldName + " in directory " + getName());
171        }
172    }
173
174    @Override
175    public List<Reference> getReferences(String referenceFieldName) {
176        return references.get(referenceFieldName);
177    }
178
179    public boolean isReference(String referenceFieldName) {
180        return references.containsKey(referenceFieldName);
181    }
182
183    public void addReference(Reference reference) {
184        reference.setSourceDirectoryName(getName());
185        String fieldName = reference.getFieldName();
186        List<Reference> fieldRefs;
187        if (references.containsKey(fieldName)) {
188            fieldRefs = references.get(fieldName);
189        } else {
190            references.put(fieldName, fieldRefs = new ArrayList<>(1));
191        }
192        fieldRefs.add(reference);
193    }
194
195    public void addReferences(Reference[] refs) {
196        for (Reference reference : refs) {
197            addReference(reference);
198        }
199    }
200
201    protected void addInverseReferences(InverseReferenceDescriptor[] references) {
202        if (references != null) {
203            Arrays.stream(references).map(InverseReference::new).forEach(this::addReference);
204        }
205    }
206
207    protected void addReferences(ReferenceDescriptor[] references) {
208        if (references != null) {
209            for (ReferenceDescriptor desc : references) {
210                try {
211                    addReference(referenceClass.getDeclaredConstructor(ReferenceDescriptor.class).newInstance(desc));
212                } catch (ReflectiveOperationException e) {
213                    throw new DirectoryException(
214                            "An error occurred while instantiating reference class " + referenceClass.getName(), e);
215                }
216            }
217        }
218    }
219
220    @Override
221    public Collection<Reference> getReferences() {
222        List<Reference> allRefs = new ArrayList<>(2);
223        for (List<Reference> refs : references.values()) {
224            allRefs.addAll(refs);
225        }
226        return allRefs;
227    }
228
229    /**
230     * Helper method to order entries.
231     *
232     * @param entries the list of entries.
233     * @param orderBy an ordered map of field name -> "asc" or "desc".
234     */
235    public void orderEntries(List<DocumentModel> entries, Map<String, String> orderBy) throws DirectoryException {
236        entries.sort(new DocumentModelComparator(getSchema(), orderBy));
237    }
238
239    @Override
240    public DirectoryCache getCache() {
241        return cache;
242    }
243
244    public void removeSession(Session session) {
245        sessionCount.dec();
246    }
247
248    public void addSession(Session session) {
249        sessionCount.inc();
250        if (sessionCount.getCount() > sessionMaxCount.getCount()) {
251            sessionMaxCount.inc();
252        }
253    }
254
255    @Override
256    public void invalidateDirectoryCache() throws DirectoryException {
257        getCache().invalidateAll();
258    }
259
260    @Override
261    public boolean isMultiTenant() {
262        return false;
263    }
264
265    @Override
266    public void shutdown() {
267        sessionCount.dec(sessionCount.getCount());
268        sessionMaxCount.dec(sessionMaxCount.getCount());
269    }
270
271    /**
272     * since @8.4
273     */
274    @Override
275    public List<String> getTypes() {
276        return types;
277    }
278
279    /**
280     * @since 8.4
281     */
282    @Override
283    public List<DirectoryDeleteConstraint> getDirectoryDeleteConstraints() {
284        return descriptor.getDeleteConstraints();
285    }
286
287    /*
288     * Initializes schemaFieldMap. Note that this cannot be called from the Directory constructor because the
289     * SchemaManager initialization itself requires access to directories (and therefore their construction) for fields
290     * having entry resolvers. So an infinite recursion must be avoided.
291     */
292    protected void initSchemaFieldMap() {
293        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
294        Schema schema = schemaManager.getSchema(getSchema());
295        if (schema == null) {
296            throw new DirectoryException(
297                    "Invalid configuration for directory: " + getName() + ", no such schema: " + getSchema());
298        }
299        schemaFieldMap = new LinkedHashMap<>();
300        schema.getFields().forEach(f -> schemaFieldMap.put(f.getName().getLocalName(), f));
301    }
302
303    @Override
304    public Map<String, Field> getSchemaFieldMap() {
305        return schemaFieldMap;
306    }
307
308    protected void fallbackOnDefaultCache() {
309        CacheService cacheService = Framework.getService(CacheService.class);
310        if (cacheService != null) {
311            if (descriptor.cacheEntryName == null) {
312                String cacheEntryName = "cache-" + getName();
313                cache.setEntryCacheName(cacheEntryName);
314                cacheService.registerCache(cacheEntryName);
315            }
316            if (descriptor.cacheEntryWithoutReferencesName == null) {
317                String cacheEntryWithoutReferencesName = "cacheWithoutReference-" + getName();
318                cache.setEntryCacheWithoutReferencesName(cacheEntryWithoutReferencesName);
319                cacheService.registerCache(cacheEntryWithoutReferencesName);
320            }
321        }
322    }
323}