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 * Florent Guillaume 018 * 019 * $Id: MemoryDirectorySession.java 30374 2008-02-20 16:31:28Z gracinet $ 020 */ 021 022package org.nuxeo.ecm.directory.memory; 023 024import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; 025 026import java.io.Serializable; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Map.Entry; 033import java.util.Set; 034 035import org.nuxeo.ecm.core.api.DataModel; 036import org.nuxeo.ecm.core.api.DocumentModel; 037import org.nuxeo.ecm.core.api.DocumentModelList; 038import org.nuxeo.ecm.core.api.NuxeoException; 039import org.nuxeo.ecm.core.api.PropertyException; 040import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 041import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 042import org.nuxeo.ecm.core.api.security.SecurityConstants; 043import org.nuxeo.ecm.core.query.sql.model.OrderByList; 044import org.nuxeo.ecm.core.query.sql.model.Predicate; 045import org.nuxeo.ecm.core.query.sql.model.QueryBuilder; 046import org.nuxeo.ecm.directory.AbstractDirectory; 047import org.nuxeo.ecm.directory.BaseSession; 048import org.nuxeo.ecm.directory.DirectoryException; 049 050/** 051 * Trivial in-memory implementation of a Directory to use in unit tests. 052 * 053 * @author Florent Guillaume 054 */ 055public class MemoryDirectorySession extends BaseSession { 056 057 protected final Map<String, Map<String, Object>> data; 058 059 protected final String passwordField; 060 061 protected NuxeoException closeStackTrace; 062 063 public MemoryDirectorySession(MemoryDirectory directory) { 064 super(directory, null); 065 data = directory.data; 066 passwordField = getPasswordField(); 067 } 068 069 /** To be implemented with a more specific type. */ 070 @Override 071 public MemoryDirectory getDirectory() { 072 return (MemoryDirectory) directory; 073 } 074 075 @Override 076 public boolean authenticate(String username, String password) { 077 checkClose(); 078 Map<String, Object> map = data.get(username); 079 if (map == null) { 080 return false; 081 } 082 String expected = (String) map.get(passwordField); 083 if (expected == null) { 084 return false; 085 } 086 return expected.equals(password); 087 } 088 089 @Override 090 public void close() { 091 NuxeoException exc = new NuxeoException("debug close stack trace"); 092 if (closeStackTrace == null) { 093 closeStackTrace = exc; 094 } else { 095 closeStackTrace.addSuppressed(exc); 096 } 097 getDirectory().removeSession(this); 098 } 099 100 protected void checkClose() { 101 if (closeStackTrace != null) { 102 NuxeoException exc = new NuxeoException("Session is closed"); 103 exc.addSuppressed(closeStackTrace); 104 throw exc; 105 } 106 } 107 108 public void commit() { 109 } 110 111 public void rollback() { 112 throw new RuntimeException("Not implemented"); 113 } 114 115 @Override 116 public DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) { 117 checkClose(); 118 // find id 119 Object rawId = fieldMap.get(getIdField()); 120 if (rawId == null) { 121 throw new DirectoryException("Missing id"); 122 } 123 String id = String.valueOf(rawId); 124 Map<String, Object> map = data.get(id); 125 if (map != null) { 126 throw new DirectoryException( 127 String.format("Entry with id %s already exists in directory %s", id, directory.getName()), 128 SC_CONFLICT); 129 } 130 map = new HashMap<>(); 131 data.put(id, map); 132 // put fields in map 133 for (Entry<String, Object> e : fieldMap.entrySet()) { 134 String fieldName = e.getKey(); 135 if (!getDirectory().schemaSet.contains(fieldName)) { 136 continue; 137 } 138 map.put(fieldName, e.getValue()); 139 } 140 return getEntry(id); 141 } 142 143 @Override 144 protected List<String> updateEntryWithoutReferences(DocumentModel docModel) { 145 checkClose(); 146 String id = docModel.getId(); 147 DataModel dataModel = docModel.getDataModel(directory.getSchema()); 148 149 Map<String, Object> map = data.get(id); 150 if (map == null) { 151 throw new DirectoryException("UpdateEntry failed: entry '" + id + "' not found"); 152 } 153 154 for (String fieldName : getDirectory().schemaSet) { 155 try { 156 if (!dataModel.isDirty(fieldName) || fieldName.equals(getIdField())) { 157 continue; 158 } 159 } catch (PropertyNotFoundException e) { 160 continue; 161 } 162 // TODO references 163 map.put(fieldName, dataModel.getData(fieldName)); 164 } 165 dataModel.getDirtyFields().clear(); 166 return new ArrayList<>(); 167 } 168 169 @Override 170 protected void deleteEntryWithoutReferences(String id) { 171 checkClose(); 172 checkDeleteConstraints(id); 173 data.remove(id); 174 } 175 176 @Override 177 public DocumentModel createEntry(Map<String, Object> fieldMap) { 178 checkClose(); 179 checkPermission(SecurityConstants.WRITE); 180 return createEntryWithoutReferences(fieldMap); 181 } 182 183 @Override 184 public void updateEntry(DocumentModel docModel) { 185 checkClose(); 186 checkPermission(SecurityConstants.WRITE); 187 updateEntryWithoutReferences(docModel); 188 } 189 190 @Override 191 public void deleteEntry(String id) { 192 checkClose(); 193 checkPermission(SecurityConstants.WRITE); 194 deleteEntryWithoutReferences(id); 195 } 196 197 @Override 198 public DocumentModel getEntry(String id, boolean fetchReferences) { 199 checkClose(); 200 // XXX no references here 201 Map<String, Object> map = data.get(id); 202 if (map == null) { 203 return null; 204 } 205 if (passwordField != null && map.get(passwordField) != null) { 206 map = new HashMap<>(map); 207 map.remove(passwordField); 208 } 209 try { 210 return createEntryModel(null, directory.getSchema(), id, map, isReadOnly()); 211 } catch (PropertyException e) { 212 throw new DirectoryException(e); 213 } 214 } 215 216 @Override 217 public DocumentModelList getEntries() { 218 checkClose(); 219 DocumentModelList list = new DocumentModelListImpl(); 220 for (String id : data.keySet()) { 221 list.add(getEntry(id)); 222 } 223 return list; 224 } 225 226 // given our storage model this doesn't even make sense, as id field is 227 // unique 228 @Override 229 public void deleteEntry(String id, Map<String, String> map) { 230 checkClose(); 231 throw new DirectoryException("Not implemented"); 232 } 233 234 @Override 235 public void deleteEntry(DocumentModel docModel) { 236 checkClose(); 237 deleteEntry(docModel.getId()); 238 } 239 240 @Override 241 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy, 242 boolean fetchReferences, int limit, int offset) { 243 checkClose(); 244 DocumentModelList results = new DocumentModelListImpl(); 245 // canonicalize filter 246 Map<String, Object> filt = new HashMap<>(); 247 for (Entry<String, Serializable> e : filter.entrySet()) { 248 String fieldName = e.getKey(); 249 if (!getDirectory().schemaSet.contains(fieldName)) { 250 continue; 251 } 252 filt.put(fieldName, e.getValue()); 253 } 254 // do the search 255 data_loop: for (Entry<String, Map<String, Object>> datae : data.entrySet()) { 256 String id = datae.getKey(); 257 Map<String, Object> map = datae.getValue(); 258 for (Entry<String, Object> e : filt.entrySet()) { 259 String fieldName = e.getKey(); 260 Object expected = e.getValue(); 261 Object value = map.get(fieldName); 262 if (value == null) { 263 if (expected != null) { 264 continue data_loop; 265 } 266 } else { 267 if (fulltext != null && fulltext.contains(fieldName)) { 268 if (!value.toString().toLowerCase().startsWith(expected.toString().toLowerCase())) { 269 continue data_loop; 270 } 271 } else { 272 if (!value.equals(expected)) { 273 continue data_loop; 274 } 275 } 276 } 277 } 278 // this entry matches 279 results.add(getEntry(id)); 280 } 281 // order entries 282 if (orderBy != null && !orderBy.isEmpty()) { 283 getDirectory().orderEntries(results, orderBy); 284 } 285 return applyQueryLimits(results, limit, offset); 286 } 287 288 @Override 289 public DocumentModelList query(QueryBuilder queryBuilder, boolean fetchReferences) { 290 checkClose(); 291 if (!hasPermission(SecurityConstants.READ)) { 292 return new DocumentModelListImpl(); 293 } 294 if (FieldDetector.hasField(queryBuilder.predicate(), passwordField)) { 295 throw new DirectoryException("Cannot filter on password"); 296 } 297 DocumentModelList results = new DocumentModelListImpl(); 298 Predicate expression = queryBuilder.predicate(); 299 OrderByList orders = queryBuilder.orders(); 300 int limit = Math.max(0, (int) queryBuilder.limit()); 301 int offset = Math.max(0, (int) queryBuilder.offset()); 302 boolean countTotal = queryBuilder.countTotal(); 303 304 // do the search 305 MemoryDirectoryExpressionEvaluator evaluator = new MemoryDirectoryExpressionEvaluator(getDirectory()); 306 for (Entry<String, Map<String, Object>> datae : data.entrySet()) { 307 if (evaluator.matchesEntry(expression, datae.getValue())) { 308 results.add(getEntry(datae.getKey())); 309 } 310 } 311 // order entries 312 if (!orders.isEmpty()) { 313 getDirectory().orderEntries(results, AbstractDirectory.makeOrderBy(orders)); 314 } 315 results = applyQueryLimits(results, limit, offset); 316 if ((limit != 0 || offset != 0) && !countTotal) { 317 // compat with other directories 318 ((DocumentModelListImpl) results).setTotalSize(-2); 319 } 320 return results; 321 } 322 323 @Override 324 public List<String> queryIds(QueryBuilder queryBuilder) { 325 checkClose(); 326 if (!hasPermission(SecurityConstants.READ)) { 327 return Collections.emptyList(); 328 } 329 if (FieldDetector.hasField(queryBuilder.predicate(), passwordField)) { 330 throw new DirectoryException("Cannot filter on password"); 331 } 332 DocumentModelList entries = new DocumentModelListImpl(); // needed if we have ordering 333 List<String> ids = new ArrayList<>(); 334 Predicate expression = queryBuilder.predicate(); 335 OrderByList orders = queryBuilder.orders(); 336 boolean order = !orders.isEmpty(); 337 int limit = Math.max(0, (int) queryBuilder.limit()); 338 int offset = Math.max(0, (int) queryBuilder.offset()); 339 340 // do the search 341 MemoryDirectoryExpressionEvaluator evaluator = new MemoryDirectoryExpressionEvaluator(getDirectory()); 342 for (Entry<String, Map<String, Object>> datae : data.entrySet()) { 343 if (evaluator.matchesEntry(expression, datae.getValue())) { 344 String id = datae.getKey(); 345 if (order) { 346 entries.add(getEntry(id)); 347 } else { 348 ids.add(id); 349 } 350 } 351 } 352 // order entries if needed 353 if (order) { 354 getDirectory().orderEntries(entries, AbstractDirectory.makeOrderBy(orders)); 355 entries.forEach(doc -> ids.add(doc.getId())); 356 } 357 // apply query limits 358 return applyQueryLimits(ids, limit, offset); 359 } 360 361 @Override 362 public DocumentModel createEntry(DocumentModel entry) { 363 checkClose(); 364 Map<String, Object> fieldMap = entry.getProperties(directory.getSchema()); 365 return createEntry(fieldMap); 366 } 367 368 @Override 369 public boolean hasEntry(String id) { 370 checkClose(); 371 return data.containsKey(id); 372 } 373 374}