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