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 static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
024import static org.apache.commons.lang3.StringUtils.isBlank;
025import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_LEGACY;
026import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_NEVER_LOAD;
027import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_REJECT_DUPLICATE;
028import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_UPDATE_DUPLICATE;
029
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collection;
033import java.util.HashMap;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.function.Consumer;
038
039import org.apache.commons.lang3.StringUtils;
040import org.apache.logging.log4j.LogManager;
041import org.apache.logging.log4j.Logger;
042import org.nuxeo.ecm.core.api.Blob;
043import org.nuxeo.ecm.core.api.DocumentModel;
044import org.nuxeo.ecm.core.api.DocumentModelComparator;
045import org.nuxeo.ecm.core.cache.CacheService;
046import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
047import org.nuxeo.ecm.core.query.sql.model.OrderByList;
048import org.nuxeo.ecm.core.schema.SchemaManager;
049import org.nuxeo.ecm.core.schema.types.Field;
050import org.nuxeo.ecm.core.schema.types.Schema;
051import org.nuxeo.ecm.directory.api.DirectoryDeleteConstraint;
052import org.nuxeo.runtime.api.Framework;
053import org.nuxeo.runtime.metrics.MetricsService;
054import org.nuxeo.runtime.transaction.TransactionHelper;
055
056import io.dropwizard.metrics5.Counter;
057import io.dropwizard.metrics5.MetricName;
058import io.dropwizard.metrics5.MetricRegistry;
059import io.dropwizard.metrics5.SharedMetricRegistries;
060
061public abstract class AbstractDirectory implements Directory {
062
063    private static final Logger log = LogManager.getLogger(AbstractDirectory.class);
064
065    public static final String TENANT_ID_FIELD = "tenantId";
066
067    public final BaseDirectoryDescriptor descriptor;
068
069    protected DirectoryFieldMapper fieldMapper;
070
071    protected final Map<String, List<Reference>> references = new HashMap<>();
072
073    // simple cache system for entry lookups, disabled by default
074    protected final DirectoryCache cache;
075
076    // @since 5.7
077    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
078
079    protected final Counter sessionCount;
080
081    protected final Counter sessionMaxCount;
082
083    protected Map<String, Field> schemaFieldMap;
084
085    protected List<String> types = new ArrayList<>();
086
087    protected Class<? extends Reference> referenceClass;
088
089    protected AbstractDirectory(BaseDirectoryDescriptor descriptor, Class<? extends Reference> referenceClass) {
090        this.referenceClass = referenceClass;
091        this.descriptor = descriptor;
092        // is the directory visible in the ui
093        if (descriptor.types != null) {
094            this.types = Arrays.asList(descriptor.types);
095        }
096        if (!descriptor.template && doSanityChecks()) {
097            if (StringUtils.isEmpty(descriptor.idField)) {
098                throw new DirectoryException("idField configuration is missing for directory: " + getName());
099            }
100            if (StringUtils.isEmpty(descriptor.schemaName)) {
101                throw new DirectoryException("schema configuration is missing for directory " + getName());
102            }
103        }
104
105        sessionCount = registry.counter(MetricName.build("nuxeo", "directories", "directory", "sessions", "active")
106                                                  .tagged("directory", getName()));
107        sessionMaxCount = registry.counter(MetricName.build("nuxeo", "directories", "directory", "sessions", "max")
108                                                     .tagged("directory", getName()));
109
110        // add references
111        addReferences();
112        addInverseReferences();
113
114        // cache parameterization
115        cache = new DirectoryCache(getName());
116        cache.setEntryCacheName(descriptor.cacheEntryName);
117        cache.setEntryCacheWithoutReferencesName(descriptor.cacheEntryWithoutReferencesName);
118        cache.setNegativeCaching(descriptor.negativeCaching);
119
120    }
121
122    protected boolean doSanityChecks() {
123        return true;
124    }
125
126    @Override
127    public void initialize() {
128        initSchemaFieldMap();
129    }
130
131    /**
132     * @deprecated since 11.1, use {@link #loadDataOnInit(boolean)} instead
133     */
134    @Deprecated(since = "11.1")
135    protected void loadData() {
136        loadDataOnInit(false);
137    }
138
139    protected void loadDataOnInit(boolean tableExists) {
140        String dataFileName = descriptor.getDataFileName();
141        if (isBlank(dataFileName)) {
142            return;
143        }
144        String dataLoadingPolicy = descriptor.getDataLoadingPolicy();
145        if (tableExists
146                && (DATA_LOADING_POLICY_LEGACY.equals(dataLoadingPolicy) || descriptor.isAutoincrementIdField())) {
147            log.debug("Don't load directory: {} on init as table already exists", this::getName);
148            return;
149        }
150        if (DATA_LOADING_POLICY_NEVER_LOAD.equals(dataLoadingPolicy)) {
151            log.debug("Don't load directory: {} on init due to never load policy", this::getName);
152            return;
153        }
154        Blob blob = DirectoryCSVLoader.createBlob(dataFileName);
155        log.info("Load directory: {} with dataLoadingPolicy: {} and file: {}", getName(), dataLoadingPolicy,
156                dataFileName);
157        TransactionHelper.runInTransaction(() -> Framework.doPrivileged(() -> loadFromCSV(blob, dataLoadingPolicy)));
158    }
159
160    @Override
161    public void loadFromCSV(Blob dataBlob, String dataLoadingPolicy) {
162        if (dataBlob == null) {
163            throw new DirectoryException("dataBlob must not be null", SC_BAD_REQUEST);
164        }
165        BaseDirectoryDescriptor.checkDataLoadingPolicy(dataLoadingPolicy);
166        try (Session session = getSession()) {
167            String schemaName = getSchema();
168            Schema schema = Framework.getService(SchemaManager.class).getSchema(schemaName);
169            Consumer<Map<String, Object>> loader = new CSVLoaderConsumer(dataLoadingPolicy, session, schemaName);
170            DirectoryCSVLoader.loadData(dataBlob, descriptor.getDataFileCharacterSeparator(), schema, loader);
171            invalidateCaches();
172        }
173    }
174
175    @Override
176    public void initializeReferences() {
177        // nothing, but may be subclassed
178    }
179
180    @Override
181    public void initializeInverseReferences() {
182        for (Reference reference : getReferences()) {
183            if (reference instanceof InverseReference) {
184                ((InverseReference) reference).initialize();
185            }
186        }
187    }
188
189    @Override
190    public String getName() {
191        return descriptor.name;
192    }
193
194    @Override
195    public String getSchema() {
196        return descriptor.schemaName;
197    }
198
199    @Override
200    public String getParentDirectory() {
201        return descriptor.parentDirectory;
202    }
203
204    @Override
205    public String getIdField() {
206        return descriptor.idField;
207    }
208
209    @Override
210    public String getPasswordField() {
211        return descriptor.passwordField;
212    }
213
214    @Override
215    public boolean isReadOnly() {
216        return descriptor.isReadOnly();
217    }
218
219    public void setReadOnly(boolean readOnly) {
220        descriptor.setReadOnly(readOnly);
221    }
222
223    @Override
224    public void invalidateCaches() {
225        cache.invalidateAll();
226        for (Reference ref : getReferences()) {
227            Directory targetDir = ref.getTargetDirectory();
228            if (targetDir != null) {
229                targetDir.invalidateDirectoryCache();
230            }
231        }
232    }
233
234    public DirectoryFieldMapper getFieldMapper() {
235        if (fieldMapper == null) {
236            fieldMapper = new DirectoryFieldMapper();
237        }
238        return fieldMapper;
239    }
240
241    @Deprecated
242    @Override
243    public Reference getReference(String referenceFieldName) {
244        List<Reference> refs = getReferences(referenceFieldName);
245        if (refs == null || refs.isEmpty()) {
246            return null;
247        } else if (refs.size() == 1) {
248            return refs.get(0);
249        } else {
250            throw new DirectoryException(
251                    "Unexpected multiple references for " + referenceFieldName + " in directory " + getName());
252        }
253    }
254
255    @Override
256    public List<Reference> getReferences(String referenceFieldName) {
257        return references.get(referenceFieldName);
258    }
259
260    public boolean isReference(String referenceFieldName) {
261        return references.containsKey(referenceFieldName);
262    }
263
264    public void addReference(Reference reference) {
265        reference.setSourceDirectoryName(getName());
266        String fieldName = reference.getFieldName();
267        references.computeIfAbsent(fieldName, k -> new ArrayList<>(1)).add(reference);
268    }
269
270    protected void addReferences() {
271        ReferenceDescriptor[] descs = descriptor.getReferences();
272        if (descs != null) {
273            Arrays.stream(descs).map(this::newReference).forEach(this::addReference);
274        }
275    }
276
277    protected Reference newReference(ReferenceDescriptor desc) {
278        try {
279            return referenceClass.getDeclaredConstructor(ReferenceDescriptor.class).newInstance(desc);
280        } catch (ReflectiveOperationException e) {
281            throw new DirectoryException(
282                    "An error occurred while instantiating reference class " + referenceClass.getName(), e);
283        }
284    }
285
286    protected void addInverseReferences() {
287        InverseReferenceDescriptor[] descs = descriptor.getInverseReferences();
288        if (descs != null) {
289            Arrays.stream(descs).map(InverseReference::new).forEach(this::addReference);
290        }
291    }
292
293    @Override
294    public Collection<Reference> getReferences() {
295        List<Reference> allRefs = new ArrayList<>(2);
296        for (List<Reference> refs : references.values()) {
297            allRefs.addAll(refs);
298        }
299        return allRefs;
300    }
301
302    /**
303     * Helper method to order entries.
304     *
305     * @param entries the list of entries.
306     * @param orderBy an ordered map of field name -@gt; "asc" or "desc".
307     */
308    public void orderEntries(List<DocumentModel> entries, Map<String, String> orderBy) {
309        entries.sort(new DocumentModelComparator(getSchema(), orderBy));
310    }
311
312    /**
313     * Helper to create an old-style ordering map.
314     *
315     * @since 10.3
316     */
317    public static Map<String, String> makeOrderBy(OrderByList orders) {
318        Map<String, String> orderBy = new HashMap<>();
319        for (OrderByExpr ob : orders) {
320            String ascOrDesc = ob.isDescending ? "desc" : DocumentModelComparator.ORDER_ASC;
321            orderBy.put(ob.reference.name, ascOrDesc);
322        }
323        return orderBy;
324    }
325
326    @Override
327    public DirectoryCache getCache() {
328        return cache;
329    }
330
331    public void removeSession(Session session) {
332        sessionCount.dec();
333    }
334
335    public void addSession(Session session) {
336        sessionCount.inc();
337        if (sessionCount.getCount() > sessionMaxCount.getCount()) {
338            sessionMaxCount.inc();
339        }
340    }
341
342    @Override
343    public void invalidateDirectoryCache() {
344        getCache().invalidateAll();
345    }
346
347    @Override
348    public boolean isMultiTenant() {
349        return false;
350    }
351
352    @Override
353    public void shutdown() {
354        sessionCount.dec(sessionCount.getCount());
355        sessionMaxCount.dec(sessionMaxCount.getCount());
356    }
357
358    /**
359     * since @8.4
360     */
361    @Override
362    public List<String> getTypes() {
363        return types;
364    }
365
366    /**
367     * @since 8.4
368     */
369    @Override
370    public List<DirectoryDeleteConstraint> getDirectoryDeleteConstraints() {
371        return descriptor.getDeleteConstraints();
372    }
373
374    /*
375     * Initializes schemaFieldMap. Note that this cannot be called from the Directory constructor because the
376     * SchemaManager initialization itself requires access to directories (and therefore their construction) for fields
377     * having entry resolvers. So an infinite recursion must be avoided.
378     */
379    protected void initSchemaFieldMap() {
380        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
381        Schema schema = schemaManager.getSchema(getSchema());
382        if (schema == null) {
383            throw new DirectoryException(
384                    "Invalid configuration for directory: " + getName() + ", no such schema: " + getSchema());
385        }
386        schemaFieldMap = new LinkedHashMap<>();
387        schema.getFields().forEach(f -> schemaFieldMap.put(f.getName().getLocalName(), f));
388    }
389
390    @Override
391    public Map<String, Field> getSchemaFieldMap() {
392        return schemaFieldMap;
393    }
394
395    protected void fallbackOnDefaultCache() {
396        CacheService cacheService = Framework.getService(CacheService.class);
397        if (cacheService != null) {
398            if (descriptor.cacheEntryName == null) {
399                String cacheEntryName = "cache-" + getName();
400                cache.setEntryCacheName(cacheEntryName);
401                cacheService.registerCache(cacheEntryName);
402            }
403            if (descriptor.cacheEntryWithoutReferencesName == null) {
404                String cacheEntryWithoutReferencesName = "cacheWithoutReference-" + getName();
405                cache.setEntryCacheWithoutReferencesName(cacheEntryWithoutReferencesName);
406                cacheService.registerCache(cacheEntryWithoutReferencesName);
407            }
408        }
409    }
410
411    /**
412     * Consumer to load data from CSV according to the dataLoadingPolicy.
413     */
414    protected static class CSVLoaderConsumer implements Consumer<Map<String, Object>> {
415
416        protected final String dataLoadingPolicy;
417
418        protected final Session session;
419
420        protected final String schema;
421
422        public CSVLoaderConsumer(String dataLoadingPolicy, Session session, String schema) {
423            this.dataLoadingPolicy = dataLoadingPolicy;
424            this.session = session;
425            this.schema = schema;
426        }
427
428        @Override
429        public void accept(Map<String, Object> fieldMap) {
430            if (DATA_LOADING_POLICY_REJECT_DUPLICATE.equals(dataLoadingPolicy)
431                    || DATA_LOADING_POLICY_LEGACY.equals(dataLoadingPolicy)) {
432                // leverage DB constraints
433                ((BaseSession) session).createEntryWithoutReferences(fieldMap);
434            } else {
435                Object rawId = fieldMap.get(session.getIdField());
436                if (rawId == null) {
437                    throw new DirectoryException("A line is missing the entry id", SC_BAD_REQUEST);
438                }
439                String idValue = String.valueOf(rawId);
440                if (session.hasEntry(idValue)) {
441                    if (DATA_LOADING_POLICY_UPDATE_DUPLICATE.equals(dataLoadingPolicy)) {
442                        DocumentModel dm = session.getEntry(idValue);
443                        fieldMap.forEach((fieldName, value) -> dm.setProperty(schema, fieldName, value));
444                        ((BaseSession) session).updateEntryWithoutReferences(dm);
445                    } else {
446                        log.debug("Skip the entry with id: {}", idValue);
447                    }
448                } else {
449                    ((BaseSession) session).createEntryWithoutReferences(fieldMap);
450                }
451            }
452        }
453    }
454}