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 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.directory;
021
022import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
023
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027import java.util.Locale;
028
029import org.apache.commons.lang3.StringUtils;
030import org.apache.logging.log4j.LogManager;
031import org.apache.logging.log4j.Logger;
032import org.nuxeo.common.xmap.annotation.XNode;
033import org.nuxeo.common.xmap.annotation.XNodeList;
034import org.nuxeo.common.xmap.annotation.XObject;
035import org.nuxeo.ecm.directory.api.DirectoryDeleteConstraint;
036
037/**
038 * Basic directory descriptor, containing the basic fields used by all directories.
039 *
040 * @since 8.2
041 */
042@XObject(value = "directory")
043public class BaseDirectoryDescriptor implements Cloneable {
044
045    private static final Logger log = LogManager.getLogger(BaseDirectoryDescriptor.class);
046
047    /**
048     * How directory semi-"fulltext" searches are matched with a query string.
049     * <p>
050     * Used for SQL and LDAP directories.
051     *
052     * @since 8.2
053     */
054    public enum SubstringMatchType {
055        /** Matches initial substring. */
056        subinitial,
057        /** Matches final substring. */
058        subfinal,
059        /** Matches any substring. */
060        subany
061    }
062
063    public static final boolean AUTO_INCREMENT_ID_FIELD_DEFAULT = false;
064
065    public static final int CACHE_TIMEOUT_DEFAULT = 0;
066
067    public static final int CACHE_MAX_SIZE_DEFAULT = 0;
068
069    public static final boolean READ_ONLY_DEFAULT = false;
070
071    public static final SubstringMatchType SUBSTRING_MATCH_TYPE_DEFAULT = SubstringMatchType.subinitial;
072
073    public static final char DEFAULT_DATA_FILE_CHARACTER_SEPARATOR = ',';
074
075    /**
076     * Doesn't create or modify the table in any way.
077     */
078    public static final String CREATE_TABLE_POLICY_NEVER = "never";
079
080    /**
081     * Always recreates the table from scratch and loads the CSV data.
082     */
083    public static final String CREATE_TABLE_POLICY_ALWAYS = "always";
084
085    /**
086     * If the table doesn't exist then creates it and loads the CSV data. If the table already exists, only adds missing
087     * columns (with null values).
088     */
089    public static final String CREATE_TABLE_POLICY_ON_MISSING_COLUMNS = "on_missing_columns";
090
091    public static final String CREATE_TABLE_POLICY_DEFAULT = CREATE_TABLE_POLICY_NEVER;
092
093    public static final List<String> CREATE_TABLE_POLICIES = Arrays.asList(CREATE_TABLE_POLICY_NEVER,
094            CREATE_TABLE_POLICY_ALWAYS, CREATE_TABLE_POLICY_ON_MISSING_COLUMNS);
095
096    /**
097     * Load the CSV file if the table has just been created (behavior before introducing data loading feature).
098     *
099     * @since 11.1
100     */
101    public static final String DATA_LOADING_POLICY_LEGACY = "legacy";
102
103    /**
104     * Doesn't load the CSV data.
105     *
106     * @since 11.1
107     */
108    public static final String DATA_LOADING_POLICY_NEVER_LOAD = "never_load";
109
110    /**
111     * Duplicate lines in data load from CSV are ignored.
112     *
113     * @since 11.1
114     */
115    public static final String DATA_LOADING_POLICY_SKIP_DUPLICATE = "skip_duplicate";
116
117    /**
118     * If the data loaded from CSV already exist throw an exception.
119     *
120     * @since 11.1
121     */
122    public static final String DATA_LOADING_POLICY_REJECT_DUPLICATE = "reject_duplicate";
123
124    /**
125     * Load CSV data and update duplicate lines.
126     *
127     * @since 11.1
128     */
129    public static final String DATA_LOADING_POLICY_UPDATE_DUPLICATE = "update_duplicate";
130
131    /**
132     * Policy used to deal with duplicates when loading data to a directory.
133     *
134     * @since 11.1
135     */
136    public static final List<String> DATA_LOADING_POLICIES = List.of(DATA_LOADING_POLICY_LEGACY,
137            DATA_LOADING_POLICY_NEVER_LOAD, DATA_LOADING_POLICY_SKIP_DUPLICATE, DATA_LOADING_POLICY_REJECT_DUPLICATE,
138            DATA_LOADING_POLICY_UPDATE_DUPLICATE);
139
140    @XNode("@name")
141    public String name;
142
143    @XNode("@remove")
144    public boolean remove;
145
146    @XNode("@template")
147    public boolean template;
148
149    @XNode("@extends")
150    public String extendz;
151
152    @XNode("parentDirectory")
153    public String parentDirectory;
154
155    @XNode("schema")
156    public String schemaName;
157
158    @XNode("idField")
159    public String idField;
160
161    @XNode("autoincrementIdField")
162    public Boolean autoincrementIdField;
163
164    public boolean isAutoincrementIdField() {
165        return autoincrementIdField == null ? AUTO_INCREMENT_ID_FIELD_DEFAULT : autoincrementIdField.booleanValue();
166    }
167
168    public void setAutoincrementIdField(boolean autoincrementIdField) {
169        this.autoincrementIdField = Boolean.valueOf(autoincrementIdField);
170    }
171
172    @XNode("table")
173    public String tableName;
174
175    @XNode("readOnly")
176    public Boolean readOnly;
177
178    @XNode("passwordField")
179    public String passwordField;
180
181    @XNode("passwordHashAlgorithm")
182    public String passwordHashAlgorithm;
183
184    @XNodeList(value = "permissions/permission", type = PermissionDescriptor[].class, componentType = PermissionDescriptor.class)
185    public PermissionDescriptor[] permissions;
186
187    @XNode("cacheTimeout")
188    public Integer cacheTimeout;
189
190    @XNode("cacheMaxSize")
191    public Integer cacheMaxSize;
192
193    @XNode("cacheEntryName")
194    public String cacheEntryName;
195
196    @XNode("cacheEntryWithoutReferencesName")
197    public String cacheEntryWithoutReferencesName;
198
199    @XNode("negativeCaching")
200    public Boolean negativeCaching;
201
202    @XNode("substringMatchType")
203    public String substringMatchType;
204
205    @XNode("computeMultiTenantId")
206    protected boolean computeMultiTenantId = true;
207
208    /**
209     * @since 8.4
210     */
211    @XNodeList(value = "types/type", type = String[].class, componentType = String.class)
212    public String[] types;
213
214    /**
215     * @since 8.4
216     */
217    @XNodeList(value = "deleteConstraint", type = ArrayList.class, componentType = DirectoryDeleteConstraintDescriptor.class)
218    List<DirectoryDeleteConstraintDescriptor> deleteConstraints;
219
220    /**
221     * @since 9.2
222     */
223    @XNodeList(value = "references/reference", type = ReferenceDescriptor[].class, componentType = ReferenceDescriptor.class)
224    ReferenceDescriptor[] references;
225
226    /**
227     * @since 9.2
228     */
229    @XNodeList(value = "references/inverseReference", type = InverseReferenceDescriptor[].class, componentType = InverseReferenceDescriptor.class)
230    InverseReferenceDescriptor[] inverseReferences;
231
232    @XNode("dataFile")
233    public String dataFileName;
234
235    public String getDataFileName() {
236        return dataFileName;
237    }
238
239    @XNode(value = "dataFileCharacterSeparator", trim = false)
240    public String dataFileCharacterSeparator = ",";
241
242    public char getDataFileCharacterSeparator() {
243        char sep;
244        if (StringUtils.isEmpty(dataFileCharacterSeparator)) {
245            sep = DEFAULT_DATA_FILE_CHARACTER_SEPARATOR;
246        } else {
247            sep = dataFileCharacterSeparator.charAt(0);
248            if (dataFileCharacterSeparator.length() > 1) {
249                log.warn("More than one character found for character separator, will use the first one \"{}\"", sep);
250            }
251        }
252        return sep;
253    }
254
255    @XNode("createTablePolicy")
256    public String createTablePolicy;
257
258    public String getCreateTablePolicy() {
259        if (StringUtils.isBlank(createTablePolicy)) {
260            return CREATE_TABLE_POLICY_DEFAULT;
261        }
262        String ctp = createTablePolicy.toLowerCase(Locale.ENGLISH);
263        if (!CREATE_TABLE_POLICIES.contains(ctp)) {
264            throw new DirectoryException("Invalid createTablePolicy: " + createTablePolicy + ", it should be one of: "
265                    + CREATE_TABLE_POLICIES);
266        }
267        return ctp;
268    }
269
270    /**
271     * @since 11.1
272     */
273    @XNode("dataLoadingPolicy")
274    public String dataLoadingPolicy;
275
276    /**
277     * Returns the dataLoadingPolicy; default is {@link #DATA_LOADING_POLICY_NEVER_LOAD}.
278     *
279     * @since 11.1
280     */
281    public String getDataLoadingPolicy() {
282        if (StringUtils.isBlank(dataLoadingPolicy)) {
283            return DATA_LOADING_POLICY_LEGACY;
284        }
285        String dlp = dataLoadingPolicy.toLowerCase(Locale.ENGLISH);
286        checkDataLoadingPolicy(dlp);
287        return dlp;
288    }
289
290    protected static void checkDataLoadingPolicy(String dataLoadingPolicy) {
291        if (dataLoadingPolicy == null || !DATA_LOADING_POLICIES.contains(dataLoadingPolicy)) {
292            throw new DirectoryException("Invalid dataLoadingPolicy: " + dataLoadingPolicy + ", it should be one of: "
293                    + DATA_LOADING_POLICIES, SC_BAD_REQUEST);
294        }
295    }
296
297    public boolean isReadOnly() {
298        return readOnly == null ? READ_ONLY_DEFAULT : readOnly.booleanValue();
299    }
300
301    public void setReadOnly(boolean readOnly) {
302        this.readOnly = Boolean.valueOf(readOnly);
303    }
304
305    public int getCacheTimeout() {
306        return cacheTimeout == null ? CACHE_TIMEOUT_DEFAULT : cacheTimeout.intValue();
307    }
308
309    public int getCacheMaxSize() {
310        return cacheMaxSize == null ? CACHE_MAX_SIZE_DEFAULT : cacheMaxSize.intValue();
311    }
312
313    public SubstringMatchType getSubstringMatchType() {
314        if (StringUtils.isBlank(substringMatchType)) {
315            return SUBSTRING_MATCH_TYPE_DEFAULT;
316        }
317        try {
318            return SubstringMatchType.valueOf(substringMatchType);
319        } catch (IllegalArgumentException e) {
320            log.error("Unknown value for <substringMatchType>: {}", substringMatchType);
321            return SUBSTRING_MATCH_TYPE_DEFAULT;
322        }
323    }
324
325    /**
326     * Sub-classes MUST OVERRIDE and use a more specific return type.
327     * <p>
328     * Usually it's bad to use clone(), and a copy-constructor is preferred, but here we want the copy method to be
329     * inheritable so clone() is appropriate.
330     * <p>
331     * {@inheritDoc}
332     */
333    @Override
334    public BaseDirectoryDescriptor clone() {
335        BaseDirectoryDescriptor clone;
336        try {
337            clone = (BaseDirectoryDescriptor) super.clone();
338        } catch (CloneNotSupportedException e) {
339            throw new AssertionError(e);
340        }
341        // basic fields are already copied by super.clone()
342        if (permissions != null) {
343            clone.permissions = new PermissionDescriptor[permissions.length];
344            for (int i = 0; i < permissions.length; i++) {
345                clone.permissions[i] = permissions[i].clone();
346            }
347        }
348        if (references != null) {
349            clone.references = Arrays.stream(references)
350                                     .map(ReferenceDescriptor::clone)
351                                     .toArray(ReferenceDescriptor[]::new);
352        }
353        if (inverseReferences != null) {
354            clone.inverseReferences = Arrays.stream(inverseReferences)
355                                            .map(InverseReferenceDescriptor::clone)
356                                            .toArray(InverseReferenceDescriptor[]::new);
357        }
358        return clone;
359    }
360
361    public void merge(BaseDirectoryDescriptor other) {
362        template = template || other.template;
363
364        if (other.parentDirectory != null) {
365            parentDirectory = other.parentDirectory;
366        }
367        if (other.schemaName != null) {
368            schemaName = other.schemaName;
369        }
370        if (other.idField != null) {
371            idField = other.idField;
372        }
373        if (other.autoincrementIdField != null) {
374            autoincrementIdField = other.autoincrementIdField;
375        }
376        if (other.tableName != null) {
377            tableName = other.tableName;
378        }
379        if (other.readOnly != null) {
380            readOnly = other.readOnly;
381        }
382        if (other.passwordField != null) {
383            passwordField = other.passwordField;
384        }
385        if (other.passwordHashAlgorithm != null) {
386            passwordHashAlgorithm = other.passwordHashAlgorithm;
387        }
388        if (other.permissions != null && other.permissions.length != 0) {
389            permissions = other.permissions;
390        }
391        if (other.cacheTimeout != null) {
392            cacheTimeout = other.cacheTimeout;
393        }
394        if (other.cacheMaxSize != null) {
395            cacheMaxSize = other.cacheMaxSize;
396        }
397        if (other.cacheEntryName != null) {
398            cacheEntryName = other.cacheEntryName;
399        }
400        if (other.cacheEntryWithoutReferencesName != null) {
401            cacheEntryWithoutReferencesName = other.cacheEntryWithoutReferencesName;
402        }
403        if (other.negativeCaching != null) {
404            negativeCaching = other.negativeCaching;
405        }
406        if (other.substringMatchType != null) {
407            substringMatchType = other.substringMatchType;
408        }
409        if (other.types != null) {
410            types = other.types;
411        }
412        if (other.deleteConstraints != null) {
413            deleteConstraints = other.deleteConstraints;
414        }
415        if (other.dataFileName != null) {
416            dataFileName = other.dataFileName;
417        }
418        if (other.dataFileCharacterSeparator != null) {
419            dataFileCharacterSeparator = other.dataFileCharacterSeparator;
420        }
421        if (other.createTablePolicy != null) {
422            createTablePolicy = other.createTablePolicy;
423        }
424        if (other.dataLoadingPolicy != null) {
425            dataLoadingPolicy = other.dataLoadingPolicy;
426        }
427        if (other.references != null && other.references.length != 0) {
428            references = other.references;
429        }
430        if (other.inverseReferences != null && other.inverseReferences.length != 0) {
431            inverseReferences = other.inverseReferences;
432        }
433        computeMultiTenantId = other.computeMultiTenantId;
434    }
435
436    /**
437     * Creates a new {@link Directory} instance from this {@link BaseDirectoryDescriptor}.
438     */
439    public Directory newDirectory() {
440        throw new UnsupportedOperationException("Cannot be instantiated as Directory: " + getClass().getName());
441    }
442
443    /**
444     * @since 8.4
445     */
446    public List<DirectoryDeleteConstraint> getDeleteConstraints() {
447        List<DirectoryDeleteConstraint> res = new ArrayList<>();
448        if (deleteConstraints != null) {
449            for (DirectoryDeleteConstraintDescriptor deleteConstraintDescriptor : deleteConstraints) {
450                res.add(deleteConstraintDescriptor.getDeleteConstraint());
451            }
452        }
453        return res;
454    }
455
456    /**
457     * @since 9.2
458     */
459    public ReferenceDescriptor[] getReferences() {
460        return references;
461    }
462
463    /**
464     * @since 9.2
465     */
466    public InverseReferenceDescriptor[] getInverseReferences() {
467        return inverseReferences;
468    }
469
470    /**
471     * Returns {@code true} if a multi tenant id should be computed for this directory, if the directory has support for
472     * multi tenancy, {@code false} otherwise.
473     *
474     * @since 10.1
475     */
476    public boolean isComputeMultiTenantId() {
477        return computeMultiTenantId;
478    }
479
480}