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 * 019 */ 020 021package org.nuxeo.ecm.directory; 022 023import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; 024import static org.apache.commons.lang3.StringUtils.isBlank; 025import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_LEGACY; 026import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_NEVER_LOAD; 027import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_REJECT_DUPLICATE; 028import static org.nuxeo.ecm.directory.BaseDirectoryDescriptor.DATA_LOADING_POLICY_UPDATE_DUPLICATE; 029 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.Collection; 033import java.util.HashMap; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.function.Consumer; 038 039import org.apache.commons.lang3.StringUtils; 040import org.apache.logging.log4j.LogManager; 041import org.apache.logging.log4j.Logger; 042import org.nuxeo.ecm.core.api.Blob; 043import org.nuxeo.ecm.core.api.DocumentModel; 044import org.nuxeo.ecm.core.api.DocumentModelComparator; 045import org.nuxeo.ecm.core.cache.CacheService; 046import org.nuxeo.ecm.core.query.sql.model.OrderByExpr; 047import org.nuxeo.ecm.core.query.sql.model.OrderByList; 048import org.nuxeo.ecm.core.schema.SchemaManager; 049import org.nuxeo.ecm.core.schema.types.Field; 050import org.nuxeo.ecm.core.schema.types.Schema; 051import org.nuxeo.ecm.directory.api.DirectoryDeleteConstraint; 052import org.nuxeo.runtime.api.Framework; 053import org.nuxeo.runtime.metrics.MetricsService; 054import org.nuxeo.runtime.transaction.TransactionHelper; 055 056import io.dropwizard.metrics5.Counter; 057import io.dropwizard.metrics5.MetricName; 058import io.dropwizard.metrics5.MetricRegistry; 059import io.dropwizard.metrics5.SharedMetricRegistries; 060 061public abstract class AbstractDirectory implements Directory { 062 063 private static final Logger log = LogManager.getLogger(AbstractDirectory.class); 064 065 public static final String TENANT_ID_FIELD = "tenantId"; 066 067 public final BaseDirectoryDescriptor descriptor; 068 069 protected DirectoryFieldMapper fieldMapper; 070 071 protected final Map<String, List<Reference>> references = new HashMap<>(); 072 073 // simple cache system for entry lookups, disabled by default 074 protected final DirectoryCache cache; 075 076 // @since 5.7 077 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 078 079 protected final Counter sessionCount; 080 081 protected final Counter sessionMaxCount; 082 083 protected Map<String, Field> schemaFieldMap; 084 085 protected List<String> types = new ArrayList<>(); 086 087 protected Class<? extends Reference> referenceClass; 088 089 protected AbstractDirectory(BaseDirectoryDescriptor descriptor, Class<? extends Reference> referenceClass) { 090 this.referenceClass = referenceClass; 091 this.descriptor = descriptor; 092 // is the directory visible in the ui 093 if (descriptor.types != null) { 094 this.types = Arrays.asList(descriptor.types); 095 } 096 if (!descriptor.template && doSanityChecks()) { 097 if (StringUtils.isEmpty(descriptor.idField)) { 098 throw new DirectoryException("idField configuration is missing for directory: " + getName()); 099 } 100 if (StringUtils.isEmpty(descriptor.schemaName)) { 101 throw new DirectoryException("schema configuration is missing for directory " + getName()); 102 } 103 } 104 105 sessionCount = registry.counter(MetricName.build("nuxeo", "directories", "directory", "sessions", "active") 106 .tagged("directory", getName())); 107 sessionMaxCount = registry.counter(MetricName.build("nuxeo", "directories", "directory", "sessions", "max") 108 .tagged("directory", getName())); 109 110 // add references 111 addReferences(); 112 addInverseReferences(); 113 114 // cache parameterization 115 cache = new DirectoryCache(getName()); 116 cache.setEntryCacheName(descriptor.cacheEntryName); 117 cache.setEntryCacheWithoutReferencesName(descriptor.cacheEntryWithoutReferencesName); 118 cache.setNegativeCaching(descriptor.negativeCaching); 119 120 } 121 122 protected boolean doSanityChecks() { 123 return true; 124 } 125 126 @Override 127 public void initialize() { 128 initSchemaFieldMap(); 129 } 130 131 /** 132 * @deprecated since 11.1, use {@link #loadDataOnInit(boolean)} instead 133 */ 134 @Deprecated(since = "11.1") 135 protected void loadData() { 136 loadDataOnInit(false); 137 } 138 139 protected void loadDataOnInit(boolean tableExists) { 140 String dataFileName = descriptor.getDataFileName(); 141 if (isBlank(dataFileName)) { 142 return; 143 } 144 String dataLoadingPolicy = descriptor.getDataLoadingPolicy(); 145 if (tableExists 146 && (DATA_LOADING_POLICY_LEGACY.equals(dataLoadingPolicy) || descriptor.isAutoincrementIdField())) { 147 log.debug("Don't load directory: {} on init as table already exists", this::getName); 148 return; 149 } 150 if (DATA_LOADING_POLICY_NEVER_LOAD.equals(dataLoadingPolicy)) { 151 log.debug("Don't load directory: {} on init due to never load policy", this::getName); 152 return; 153 } 154 Blob blob = DirectoryCSVLoader.createBlob(dataFileName); 155 log.info("Load directory: {} with dataLoadingPolicy: {} and file: {}", getName(), dataLoadingPolicy, 156 dataFileName); 157 TransactionHelper.runInTransaction(() -> Framework.doPrivileged(() -> loadFromCSV(blob, dataLoadingPolicy))); 158 } 159 160 @Override 161 public void loadFromCSV(Blob dataBlob, String dataLoadingPolicy) { 162 if (dataBlob == null) { 163 throw new DirectoryException("dataBlob must not be null", SC_BAD_REQUEST); 164 } 165 BaseDirectoryDescriptor.checkDataLoadingPolicy(dataLoadingPolicy); 166 try (Session session = getSession()) { 167 String schemaName = getSchema(); 168 Schema schema = Framework.getService(SchemaManager.class).getSchema(schemaName); 169 Consumer<Map<String, Object>> loader = new CSVLoaderConsumer(dataLoadingPolicy, session, schemaName); 170 DirectoryCSVLoader.loadData(dataBlob, descriptor.getDataFileCharacterSeparator(), schema, loader); 171 invalidateCaches(); 172 } 173 } 174 175 @Override 176 public void initializeReferences() { 177 // nothing, but may be subclassed 178 } 179 180 @Override 181 public void initializeInverseReferences() { 182 for (Reference reference : getReferences()) { 183 if (reference instanceof InverseReference) { 184 ((InverseReference) reference).initialize(); 185 } 186 } 187 } 188 189 @Override 190 public String getName() { 191 return descriptor.name; 192 } 193 194 @Override 195 public String getSchema() { 196 return descriptor.schemaName; 197 } 198 199 @Override 200 public String getParentDirectory() { 201 return descriptor.parentDirectory; 202 } 203 204 @Override 205 public String getIdField() { 206 return descriptor.idField; 207 } 208 209 @Override 210 public String getPasswordField() { 211 return descriptor.passwordField; 212 } 213 214 @Override 215 public boolean isReadOnly() { 216 return descriptor.isReadOnly(); 217 } 218 219 public void setReadOnly(boolean readOnly) { 220 descriptor.setReadOnly(readOnly); 221 } 222 223 @Override 224 public void invalidateCaches() { 225 cache.invalidateAll(); 226 for (Reference ref : getReferences()) { 227 Directory targetDir = ref.getTargetDirectory(); 228 if (targetDir != null) { 229 targetDir.invalidateDirectoryCache(); 230 } 231 } 232 } 233 234 public DirectoryFieldMapper getFieldMapper() { 235 if (fieldMapper == null) { 236 fieldMapper = new DirectoryFieldMapper(); 237 } 238 return fieldMapper; 239 } 240 241 @Deprecated 242 @Override 243 public Reference getReference(String referenceFieldName) { 244 List<Reference> refs = getReferences(referenceFieldName); 245 if (refs == null || refs.isEmpty()) { 246 return null; 247 } else if (refs.size() == 1) { 248 return refs.get(0); 249 } else { 250 throw new DirectoryException( 251 "Unexpected multiple references for " + referenceFieldName + " in directory " + getName()); 252 } 253 } 254 255 @Override 256 public List<Reference> getReferences(String referenceFieldName) { 257 return references.get(referenceFieldName); 258 } 259 260 public boolean isReference(String referenceFieldName) { 261 return references.containsKey(referenceFieldName); 262 } 263 264 public void addReference(Reference reference) { 265 reference.setSourceDirectoryName(getName()); 266 String fieldName = reference.getFieldName(); 267 references.computeIfAbsent(fieldName, k -> new ArrayList<>(1)).add(reference); 268 } 269 270 protected void addReferences() { 271 ReferenceDescriptor[] descs = descriptor.getReferences(); 272 if (descs != null) { 273 Arrays.stream(descs).map(this::newReference).forEach(this::addReference); 274 } 275 } 276 277 protected Reference newReference(ReferenceDescriptor desc) { 278 try { 279 return referenceClass.getDeclaredConstructor(ReferenceDescriptor.class).newInstance(desc); 280 } catch (ReflectiveOperationException e) { 281 throw new DirectoryException( 282 "An error occurred while instantiating reference class " + referenceClass.getName(), e); 283 } 284 } 285 286 protected void addInverseReferences() { 287 InverseReferenceDescriptor[] descs = descriptor.getInverseReferences(); 288 if (descs != null) { 289 Arrays.stream(descs).map(InverseReference::new).forEach(this::addReference); 290 } 291 } 292 293 @Override 294 public Collection<Reference> getReferences() { 295 List<Reference> allRefs = new ArrayList<>(2); 296 for (List<Reference> refs : references.values()) { 297 allRefs.addAll(refs); 298 } 299 return allRefs; 300 } 301 302 /** 303 * Helper method to order entries. 304 * 305 * @param entries the list of entries. 306 * @param orderBy an ordered map of field name -@gt; "asc" or "desc". 307 */ 308 public void orderEntries(List<DocumentModel> entries, Map<String, String> orderBy) { 309 entries.sort(new DocumentModelComparator(getSchema(), orderBy)); 310 } 311 312 /** 313 * Helper to create an old-style ordering map. 314 * 315 * @since 10.3 316 */ 317 public static Map<String, String> makeOrderBy(OrderByList orders) { 318 Map<String, String> orderBy = new HashMap<>(); 319 for (OrderByExpr ob : orders) { 320 String ascOrDesc = ob.isDescending ? "desc" : DocumentModelComparator.ORDER_ASC; 321 orderBy.put(ob.reference.name, ascOrDesc); 322 } 323 return orderBy; 324 } 325 326 @Override 327 public DirectoryCache getCache() { 328 return cache; 329 } 330 331 public void removeSession(Session session) { 332 sessionCount.dec(); 333 } 334 335 public void addSession(Session session) { 336 sessionCount.inc(); 337 if (sessionCount.getCount() > sessionMaxCount.getCount()) { 338 sessionMaxCount.inc(); 339 } 340 } 341 342 @Override 343 public void invalidateDirectoryCache() { 344 getCache().invalidateAll(); 345 } 346 347 @Override 348 public boolean isMultiTenant() { 349 return false; 350 } 351 352 @Override 353 public void shutdown() { 354 sessionCount.dec(sessionCount.getCount()); 355 sessionMaxCount.dec(sessionMaxCount.getCount()); 356 } 357 358 /** 359 * since @8.4 360 */ 361 @Override 362 public List<String> getTypes() { 363 return types; 364 } 365 366 /** 367 * @since 8.4 368 */ 369 @Override 370 public List<DirectoryDeleteConstraint> getDirectoryDeleteConstraints() { 371 return descriptor.getDeleteConstraints(); 372 } 373 374 /* 375 * Initializes schemaFieldMap. Note that this cannot be called from the Directory constructor because the 376 * SchemaManager initialization itself requires access to directories (and therefore their construction) for fields 377 * having entry resolvers. So an infinite recursion must be avoided. 378 */ 379 protected void initSchemaFieldMap() { 380 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 381 Schema schema = schemaManager.getSchema(getSchema()); 382 if (schema == null) { 383 throw new DirectoryException( 384 "Invalid configuration for directory: " + getName() + ", no such schema: " + getSchema()); 385 } 386 schemaFieldMap = new LinkedHashMap<>(); 387 schema.getFields().forEach(f -> schemaFieldMap.put(f.getName().getLocalName(), f)); 388 } 389 390 @Override 391 public Map<String, Field> getSchemaFieldMap() { 392 return schemaFieldMap; 393 } 394 395 protected void fallbackOnDefaultCache() { 396 CacheService cacheService = Framework.getService(CacheService.class); 397 if (cacheService != null) { 398 if (descriptor.cacheEntryName == null) { 399 String cacheEntryName = "cache-" + getName(); 400 cache.setEntryCacheName(cacheEntryName); 401 cacheService.registerCache(cacheEntryName); 402 } 403 if (descriptor.cacheEntryWithoutReferencesName == null) { 404 String cacheEntryWithoutReferencesName = "cacheWithoutReference-" + getName(); 405 cache.setEntryCacheWithoutReferencesName(cacheEntryWithoutReferencesName); 406 cacheService.registerCache(cacheEntryWithoutReferencesName); 407 } 408 } 409 } 410 411 /** 412 * Consumer to load data from CSV according to the dataLoadingPolicy. 413 */ 414 protected static class CSVLoaderConsumer implements Consumer<Map<String, Object>> { 415 416 protected final String dataLoadingPolicy; 417 418 protected final Session session; 419 420 protected final String schema; 421 422 public CSVLoaderConsumer(String dataLoadingPolicy, Session session, String schema) { 423 this.dataLoadingPolicy = dataLoadingPolicy; 424 this.session = session; 425 this.schema = schema; 426 } 427 428 @Override 429 public void accept(Map<String, Object> fieldMap) { 430 if (DATA_LOADING_POLICY_REJECT_DUPLICATE.equals(dataLoadingPolicy) 431 || DATA_LOADING_POLICY_LEGACY.equals(dataLoadingPolicy)) { 432 // leverage DB constraints 433 ((BaseSession) session).createEntryWithoutReferences(fieldMap); 434 } else { 435 Object rawId = fieldMap.get(session.getIdField()); 436 if (rawId == null) { 437 throw new DirectoryException("A line is missing the entry id", SC_BAD_REQUEST); 438 } 439 String idValue = String.valueOf(rawId); 440 if (session.hasEntry(idValue)) { 441 if (DATA_LOADING_POLICY_UPDATE_DUPLICATE.equals(dataLoadingPolicy)) { 442 DocumentModel dm = session.getEntry(idValue); 443 fieldMap.forEach((fieldName, value) -> dm.setProperty(schema, fieldName, value)); 444 ((BaseSession) session).updateEntryWithoutReferences(dm); 445 } else { 446 log.debug("Skip the entry with id: {}", idValue); 447 } 448 } else { 449 ((BaseSession) session).createEntryWithoutReferences(fieldMap); 450 } 451 } 452 } 453 } 454}