001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (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.lang.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)
279                                     .map(InverseReferenceDescriptor::clone)
280                                     .toArray(InverseReferenceDescriptor[]::new);
281        }
282        return clone;
283    }
284
285    public void merge(BaseDirectoryDescriptor other) {
286        template = template || other.template;
287
288        if (other.parentDirectory != null) {
289            parentDirectory = other.parentDirectory;
290        }
291        if (other.schemaName != null) {
292            schemaName = other.schemaName;
293        }
294        if (other.idField != null) {
295            idField = other.idField;
296        }
297        if (other.autoincrementIdField != null) {
298            autoincrementIdField = other.autoincrementIdField;
299        }
300        if (other.tableName != null) {
301            tableName = other.tableName;
302        }
303        if (other.readOnly != null) {
304            readOnly = other.readOnly;
305        }
306        if (other.passwordField != null) {
307            passwordField = other.passwordField;
308        }
309        if (other.passwordHashAlgorithm != null) {
310            passwordHashAlgorithm = other.passwordHashAlgorithm;
311        }
312        if (other.permissions != null && other.permissions.length != 0) {
313            permissions = other.permissions;
314        }
315        if (other.cacheTimeout != null) {
316            cacheTimeout = other.cacheTimeout;
317        }
318        if (other.cacheMaxSize != null) {
319            cacheMaxSize = other.cacheMaxSize;
320        }
321        if (other.cacheEntryName != null) {
322            cacheEntryName = other.cacheEntryName;
323        }
324        if (other.cacheEntryWithoutReferencesName != null) {
325            cacheEntryWithoutReferencesName = other.cacheEntryWithoutReferencesName;
326        }
327        if (other.negativeCaching != null) {
328            negativeCaching = other.negativeCaching;
329        }
330        if (other.substringMatchType != null) {
331            substringMatchType = other.substringMatchType;
332        }
333        if (other.types != null) {
334            types = other.types;
335        }
336        if (other.deleteConstraints != null) {
337            deleteConstraints = other.deleteConstraints;
338        }
339        if (other.dataFileName != null) {
340            dataFileName = other.dataFileName;
341        }
342        if (other.dataFileCharacterSeparator != null) {
343            dataFileCharacterSeparator = other.dataFileCharacterSeparator;
344        }
345        if (other.createTablePolicy != null) {
346            createTablePolicy = other.createTablePolicy;
347        }
348        if (other.references != null && other.references.length != 0) {
349            references = other.references;
350        }
351        if (other.inverseReferences != null && other.inverseReferences.length != 0) {
352            inverseReferences = other.inverseReferences;
353        }
354    }
355
356    /**
357     * Creates a new {@link Directory} instance from this {@link BaseDirectoryDescriptor).
358     */
359    public Directory newDirectory() {
360        throw new UnsupportedOperationException("Cannot be instantiated as Directory: " + getClass().getName());
361    }
362
363    /**
364     * @since 8.4
365     */
366    public List<DirectoryDeleteConstraint> getDeleteConstraints() throws DirectoryException {
367        List<DirectoryDeleteConstraint> res = new ArrayList<>();
368        if (deleteConstraints != null) {
369            for (DirectoryDeleteConstraintDescriptor deleteConstraintDescriptor : deleteConstraints) {
370                res.add(deleteConstraintDescriptor.getDeleteConstraint());
371            }
372        }
373        return res;
374    }
375
376    /**
377     * @since 9.2
378     */
379    public ReferenceDescriptor[] getReferences() {
380        return references;
381    }
382
383    /**
384     * @since 9.2
385     */
386    public InverseReferenceDescriptor[] getInverseReferences() {
387        return inverseReferences;
388    }
389
390}