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