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}