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    /**
159     * @since 8.4
160     */
161    @XNodeList(value = "types/type", type = String[].class, componentType = String.class)
162    public String[] types;
163
164    /**
165     * @since 8.4
166     */
167    @XNodeList(value = "deleteConstraint", type = ArrayList.class, componentType = DirectoryDeleteConstraintDescriptor.class)
168    List<DirectoryDeleteConstraintDescriptor> deleteConstraints;
169
170    /**
171     * @since 9.2
172     */
173    @XNodeList(value = "references/reference", type = ReferenceDescriptor[].class, componentType = ReferenceDescriptor.class)
174    ReferenceDescriptor[] references;
175
176    /**
177     * @since 9.2
178     */
179    @XNodeList(value = "references/inverseReference", type = InverseReferenceDescriptor[].class, componentType = InverseReferenceDescriptor.class)
180    InverseReferenceDescriptor[] inverseReferences;
181
182    @XNode("dataFile")
183    public String dataFileName;
184
185    public String getDataFileName() {
186        return dataFileName;
187    }
188
189    @XNode(value = "dataFileCharacterSeparator", trim = false)
190    public String dataFileCharacterSeparator = ",";
191
192    public char getDataFileCharacterSeparator() {
193        char sep;
194        if (StringUtils.isEmpty(dataFileCharacterSeparator)) {
195            sep = DEFAULT_DATA_FILE_CHARACTER_SEPARATOR;
196        } else {
197            sep = dataFileCharacterSeparator.charAt(0);
198            if (dataFileCharacterSeparator.length() > 1) {
199                log.warn("More than one character found for character separator, will use the first one \"" + sep
200                        + "\"");
201            }
202        }
203        return sep;
204    }
205
206    @XNode("createTablePolicy")
207    public String createTablePolicy;
208
209    public String getCreateTablePolicy() {
210        if (StringUtils.isBlank(createTablePolicy)) {
211            return CREATE_TABLE_POLICY_DEFAULT;
212        }
213        String ctp = createTablePolicy.toLowerCase();
214        if (!CREATE_TABLE_POLICIES.contains(ctp)) {
215            throw new DirectoryException("Invalid createTablePolicy: " + createTablePolicy + ", it should be one of: "
216                    + CREATE_TABLE_POLICIES);
217        }
218        return ctp;
219    }
220
221    public boolean isReadOnly() {
222        return readOnly == null ? READ_ONLY_DEFAULT : readOnly.booleanValue();
223    }
224
225    public void setReadOnly(boolean readOnly) {
226        this.readOnly = Boolean.valueOf(readOnly);
227    }
228
229    public int getCacheTimeout() {
230        return cacheTimeout == null ? CACHE_TIMEOUT_DEFAULT : cacheTimeout.intValue();
231    }
232
233    public int getCacheMaxSize() {
234        return cacheMaxSize == null ? CACHE_MAX_SIZE_DEFAULT : cacheMaxSize.intValue();
235    }
236
237    public SubstringMatchType getSubstringMatchType() {
238        if (StringUtils.isBlank(substringMatchType)) {
239            return SUBSTRING_MATCH_TYPE_DEFAULT;
240        }
241        try {
242            return SubstringMatchType.valueOf(substringMatchType);
243        } catch (IllegalArgumentException e) {
244            log.error("Unknown value for <substringMatchType>: " + substringMatchType);
245            return SUBSTRING_MATCH_TYPE_DEFAULT;
246        }
247    }
248
249    /**
250     * Sub-classes MUST OVERRIDE and use a more specific return type.
251     * <p>
252     * Usually it's bad to use clone(), and a copy-constructor is preferred, but here we want the copy method to be
253     * inheritable so clone() is appropriate.
254     * <p>
255     * {@inheritDoc}
256     */
257    @Override
258    public BaseDirectoryDescriptor clone() {
259        BaseDirectoryDescriptor clone;
260        try {
261            clone = (BaseDirectoryDescriptor) super.clone();
262        } catch (CloneNotSupportedException e) {
263            throw new AssertionError(e);
264        }
265        // basic fields are already copied by super.clone()
266        if (permissions != null) {
267            clone.permissions = new PermissionDescriptor[permissions.length];
268            for (int i = 0; i < permissions.length; i++) {
269                clone.permissions[i] = permissions[i].clone();
270            }
271        }
272        if (references != null) {
273            clone.references = Arrays.stream(references)
274                                     .map(ReferenceDescriptor::clone)
275                                     .toArray(ReferenceDescriptor[]::new);
276        }
277        if (inverseReferences != null) {
278            clone.inverseReferences = Arrays.stream(inverseReferences).map(InverseReferenceDescriptor::clone).toArray(
279                    InverseReferenceDescriptor[]::new);
280        }
281        return clone;
282    }
283
284    public void merge(BaseDirectoryDescriptor other) {
285        template = template || other.template;
286
287        if (other.parentDirectory != null) {
288            parentDirectory = other.parentDirectory;
289        }
290        if (other.schemaName != null) {
291            schemaName = other.schemaName;
292        }
293        if (other.idField != null) {
294            idField = other.idField;
295        }
296        if (other.autoincrementIdField != null) {
297            autoincrementIdField = other.autoincrementIdField;
298        }
299        if (other.tableName != null) {
300            tableName = other.tableName;
301        }
302        if (other.readOnly != null) {
303            readOnly = other.readOnly;
304        }
305        if (other.passwordField != null) {
306            passwordField = other.passwordField;
307        }
308        if (other.passwordHashAlgorithm != null) {
309            passwordHashAlgorithm = other.passwordHashAlgorithm;
310        }
311        if (other.permissions != null && other.permissions.length != 0) {
312            permissions = other.permissions;
313        }
314        if (other.cacheTimeout != null) {
315            cacheTimeout = other.cacheTimeout;
316        }
317        if (other.cacheMaxSize != null) {
318            cacheMaxSize = other.cacheMaxSize;
319        }
320        if (other.cacheEntryName != null) {
321            cacheEntryName = other.cacheEntryName;
322        }
323        if (other.cacheEntryWithoutReferencesName != null) {
324            cacheEntryWithoutReferencesName = other.cacheEntryWithoutReferencesName;
325        }
326        if (other.negativeCaching != null) {
327            negativeCaching = other.negativeCaching;
328        }
329        if (other.substringMatchType != null) {
330            substringMatchType = other.substringMatchType;
331        }
332        if (other.types != null) {
333            types = other.types;
334        }
335        if (other.deleteConstraints != null) {
336            deleteConstraints = other.deleteConstraints;
337        }
338        if (other.dataFileName != null) {
339            dataFileName = other.dataFileName;
340        }
341        if (other.dataFileCharacterSeparator != null) {
342            dataFileCharacterSeparator = other.dataFileCharacterSeparator;
343        }
344        if (other.createTablePolicy != null) {
345            createTablePolicy = other.createTablePolicy;
346        }
347        if (other.references != null && other.references.length != 0) {
348            references = other.references;
349        }
350        if (other.inverseReferences != null && other.inverseReferences.length != 0) {
351            inverseReferences = other.inverseReferences;
352        }
353    }
354
355    /**
356     * Creates a new {@link Directory} instance from this {@link BaseDirectoryDescriptor).
357     */
358    public Directory newDirectory() {
359        throw new UnsupportedOperationException("Cannot be instantiated as Directory: " + getClass().getName());
360    }
361
362    /**
363     * @since 8.4
364     */
365    public List<DirectoryDeleteConstraint> getDeleteConstraints() throws DirectoryException {
366        List<DirectoryDeleteConstraint> res = new ArrayList<>();
367        if (deleteConstraints != null) {
368            for (DirectoryDeleteConstraintDescriptor deleteConstraintDescriptor : deleteConstraints) {
369                res.add(deleteConstraintDescriptor.getDeleteConstraint());
370            }
371        }
372        return res;
373    }
374
375    /**
376     * @since 9.2
377     */
378    public ReferenceDescriptor[] getReferences() {
379        return references;
380    }
381
382    /**
383     * @since 9.2
384     */
385    public InverseReferenceDescriptor[] getInverseReferences() {
386        return inverseReferences;
387    }
388
389}