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