001/* 002 * (C) Copyright 2017-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 * Kevin Leturc 018 */ 019package org.nuxeo.mongodb.audit.pageprovider; 020 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Calendar; 024import java.util.List; 025 026import org.apache.commons.lang3.StringUtils; 027import org.apache.logging.log4j.LogManager; 028import org.apache.logging.log4j.Logger; 029import org.bson.BsonDocument; 030import org.bson.Document; 031import org.bson.conversions.Bson; 032import org.nuxeo.ecm.core.api.CoreSession; 033import org.nuxeo.ecm.core.api.DocumentModel; 034import org.nuxeo.ecm.core.api.NuxeoException; 035import org.nuxeo.ecm.core.api.SortInfo; 036import org.nuxeo.ecm.platform.audit.api.LogEntry; 037import org.nuxeo.ecm.platform.audit.api.comment.CommentProcessorHelper; 038import org.nuxeo.ecm.platform.audit.service.AuditBackend; 039import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService; 040import org.nuxeo.ecm.platform.query.api.AbstractPageProvider; 041import org.nuxeo.ecm.platform.query.api.PageProvider; 042import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; 043import org.nuxeo.ecm.platform.query.api.PredicateDefinition; 044import org.nuxeo.ecm.platform.query.api.PredicateFieldDefinition; 045import org.nuxeo.ecm.platform.query.api.QuickFilter; 046import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition; 047import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder; 048import org.nuxeo.mongodb.audit.MongoDBAuditBackend; 049import org.nuxeo.mongodb.audit.MongoDBAuditEntryReader; 050import org.nuxeo.runtime.api.Framework; 051import org.nuxeo.runtime.mongodb.MongoDBSerializationHelper; 052 053import com.mongodb.client.FindIterable; 054import com.mongodb.client.MongoCollection; 055import com.mongodb.client.model.Filters; 056import com.mongodb.client.model.Sorts; 057 058/** 059 * @since 9.1 060 */ 061public class MongoDBAuditPageProvider extends AbstractPageProvider<LogEntry> implements PageProvider<LogEntry> { 062 063 private static final long serialVersionUID = 1L; 064 065 private static final Logger log = LogManager.getLogger(MongoDBAuditPageProvider.class); 066 067 private static final String EMPTY_QUERY = "{}"; 068 069 public static final String CORE_SESSION_PROPERTY = "coreSession"; 070 071 public static final String UICOMMENTS_PROPERTY = "generateUIComments"; 072 073 @Override 074 public String toString() { 075 return buildAuditFilter().toString(); 076 } 077 078 protected CoreSession getCoreSession() { 079 Object session = getProperties().get(CORE_SESSION_PROPERTY); 080 if (session instanceof CoreSession) { 081 return (CoreSession) session; 082 } 083 return null; 084 } 085 086 protected void preprocessCommentsIfNeeded(List<LogEntry> entries) { 087 Serializable preprocess = getProperties().get(UICOMMENTS_PROPERTY); 088 089 if (preprocess != null && "true".equalsIgnoreCase(preprocess.toString())) { 090 CoreSession session = getCoreSession(); 091 if (session != null) { 092 CommentProcessorHelper cph = new CommentProcessorHelper(session); 093 cph.processComments(entries); 094 } 095 } 096 } 097 098 @Override 099 public List<LogEntry> getCurrentPage() { 100 long t0 = System.currentTimeMillis(); 101 102 Bson filter = buildAuditFilter(); 103 104 List<SortInfo> sortInfos = getSortInfos(); 105 List<Bson> sorts = new ArrayList<>(sortInfos.size()); 106 for (SortInfo sortInfo : sortInfos) { 107 String sortColumn = sortInfo.getSortColumn(); 108 if ("id".equals(sortColumn)) { 109 sortColumn = MongoDBSerializationHelper.MONGODB_ID; 110 } 111 if (sortInfo.getSortAscending()) { 112 sorts.add(Sorts.ascending(sortColumn)); 113 } else { 114 sorts.add(Sorts.descending(sortColumn)); 115 } 116 } 117 MongoCollection<Document> auditCollection = getMongoDBBackend().getAuditCollection(); 118 FindIterable<Document> response = auditCollection.find(filter) 119 .sort(Sorts.orderBy(sorts)) 120 .skip((int) (getCurrentPageIndex() * pageSize)) 121 .limit((int) getMinMaxPageSize()); 122 List<LogEntry> entries = new ArrayList<>(); 123 124 // set total number of results 125 setResultsCount(auditCollection.countDocuments(filter)); 126 127 for (Document document : response) { 128 entries.add(MongoDBAuditEntryReader.read(document)); 129 } 130 preprocessCommentsIfNeeded(entries); 131 132 CoreSession session = getCoreSession(); 133 if (session != null) { 134 // send event for statistics ! 135 fireSearchEvent(session.getPrincipal(), filter.toString(), entries, System.currentTimeMillis() - t0); 136 } 137 138 return entries; 139 } 140 141 protected String getFixedPart() { 142 WhereClauseDefinition whereClause = getDefinition().getWhereClause(); 143 if (whereClause == null) { 144 return null; 145 } else { 146 String fixedPart = whereClause.getFixedPart(); 147 if (fixedPart == null || fixedPart.isEmpty()) { 148 fixedPart = EMPTY_QUERY; 149 } 150 return fixedPart; 151 } 152 } 153 154 protected boolean allowSimplePattern() { 155 return true; 156 } 157 158 protected MongoDBAuditBackend getMongoDBBackend() { 159 NXAuditEventsService auditEventsService = Framework.getService(NXAuditEventsService.class); 160 AuditBackend backend = auditEventsService.getBackend(); 161 if (backend instanceof MongoDBAuditBackend) { 162 return (MongoDBAuditBackend) backend; 163 } 164 throw new NuxeoException( 165 "Unable to use MongoDBAuditPageProvider if auditEventsService service is not configured to run with MongoDB"); 166 } 167 168 protected Bson buildAuditFilter() { 169 PageProviderDefinition def = getDefinition(); 170 Object[] params = getParameters(); 171 List<QuickFilter> quickFilters = getQuickFilters(); 172 String quickFiltersClause = ""; 173 174 if (quickFilters != null && !quickFilters.isEmpty()) { 175 for (QuickFilter quickFilter : quickFilters) { 176 String clause = quickFilter.getClause(); 177 if (!quickFiltersClause.isEmpty() && clause != null) { 178 quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause); 179 } else { 180 quickFiltersClause = clause != null ? clause : ""; 181 } 182 } 183 } 184 185 Bson filter; 186 WhereClauseDefinition whereClause = def.getWhereClause(); 187 MongoDBAuditBackend mongoDBBackend = getMongoDBBackend(); 188 if (whereClause == null) { 189 // Simple Pattern 190 if (!allowSimplePattern()) { 191 throw new UnsupportedOperationException("This page provider requires a explicit Where Clause"); 192 } 193 String originalPattern = def.getPattern(); 194 String pattern = quickFiltersClause.isEmpty() ? originalPattern 195 : StringUtils.containsIgnoreCase(originalPattern, " WHERE ") 196 ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause) 197 : originalPattern + " WHERE " + quickFiltersClause; 198 199 String baseQuery = mongoDBBackend.expandQueryVariables(pattern, params); 200 filter = mongoDBBackend.buildFilter(baseQuery, null); 201 } else { 202 203 // Add the quick filters clauses to the fixed part 204 String fixedPart = getFixedPart(); 205 if (StringUtils.isNotBlank(quickFiltersClause)) { 206 fixedPart = StringUtils.isNotBlank(fixedPart) 207 ? NXQLQueryBuilder.appendClause(fixedPart, quickFiltersClause) 208 : quickFiltersClause; 209 } 210 211 // Where clause based on DocumentModel 212 String baseQuery = mongoDBBackend.expandQueryVariables(fixedPart, params); 213 filter = buildSearchFilter(baseQuery, whereClause.getPredicates(), getSearchDocumentModel()); 214 } 215 return filter; 216 } 217 218 @Override 219 public void refresh() { 220 setCurrentPageOffset(0); 221 super.refresh(); 222 } 223 224 @Override 225 public long getResultsCount() { 226 return resultsCount; 227 } 228 229 @Override 230 public List<SortInfo> getSortInfos() { 231 // because ContentView can reuse PageProvider without redefining columns 232 // ensure compat for ContentView configured with JPA log.* sort syntax 233 List<SortInfo> sortInfos = super.getSortInfos(); 234 for (SortInfo si : sortInfos) { 235 if (si.getSortColumn().startsWith("log.")) { 236 si.setSortColumn(si.getSortColumn().substring(4)); 237 } 238 } 239 return sortInfos; 240 } 241 242 private Bson buildFilter(PredicateDefinition[] predicates, DocumentModel searchDocumentModel) { 243 if (searchDocumentModel == null) { 244 return new Document(); 245 } 246 247 List<Bson> filters = new ArrayList<>(); 248 249 for (PredicateDefinition predicate : predicates) { 250 251 // extract data from DocumentModel 252 PredicateFieldDefinition[] fieldDef = predicate.getValues(); 253 Object[] val = new Object[fieldDef.length]; 254 for (int fidx = 0; fidx < fieldDef.length; fidx++) { 255 Object value; 256 if (fieldDef[fidx].getXpath() != null) { 257 value = searchDocumentModel.getPropertyValue(fieldDef[fidx].getXpath()); 258 } else { 259 value = searchDocumentModel.getProperty(fieldDef[fidx].getSchema(), fieldDef[fidx].getName()); 260 } 261 // Convert Calendar objects 262 if (value instanceof Calendar) { 263 value = ((Calendar) value).getTime(); 264 } 265 val[fidx] = value; 266 } 267 268 if (!isNonNullParam(val)) { 269 // skip predicate where all values are null 270 continue; 271 } 272 273 String op = predicate.getOperator(); 274 Object firstValue = val[0]; 275 String predicateParameter = predicate.getParameter(); 276 if (op.equalsIgnoreCase("IN")) { 277 278 String[] values; 279 if (firstValue instanceof Iterable<?>) { 280 List<String> l = new ArrayList<>(); 281 Iterable<?> vals = (Iterable<?>) firstValue; 282 283 for (Object v : vals) { 284 if (v != null) { 285 l.add(v.toString()); 286 } 287 } 288 values = l.toArray(new String[l.size()]); 289 } else if (firstValue instanceof Object[]) { 290 values = (String[]) firstValue; 291 } else { 292 throw new NuxeoException("IN operand required a list or an array as parameter"); 293 } 294 filters.add(Filters.in(predicateParameter, values)); 295 } else if (op.equalsIgnoreCase("BETWEEN")) { 296 filters.add(Filters.gt(predicateParameter, firstValue)); 297 if (val.length > 1) { 298 filters.add(Filters.lt(predicateParameter, val[1])); 299 } 300 } else if (">".equals(op)) { 301 filters.add(Filters.gt(predicateParameter, firstValue)); 302 } else if (">=".equals(op)) { 303 filters.add(Filters.gte(predicateParameter, firstValue)); 304 } else if ("<".equals(op)) { 305 filters.add(Filters.lt(predicateParameter, firstValue)); 306 } else if ("<=".equals(op)) { 307 filters.add(Filters.lte(predicateParameter, firstValue)); 308 } else { 309 filters.add(Filters.eq(predicateParameter, firstValue)); 310 } 311 } 312 313 if (filters.isEmpty()) { 314 return new Document(); 315 } else { 316 return Filters.and(filters); 317 } 318 } 319 320 private Bson buildSearchFilter(String fixedPart, PredicateDefinition[] predicates, 321 DocumentModel searchDocumentModel) { 322 try { 323 Document fixedFilter = Document.parse(fixedPart); 324 BsonDocument filter = buildFilter(predicates, searchDocumentModel).toBsonDocument(BsonDocument.class, 325 getMongoDBBackend().getAuditCollection().getCodecRegistry()); 326 if (fixedFilter.isEmpty()) { 327 return filter; 328 } else if (filter.isEmpty()) { 329 return fixedFilter; 330 } else { 331 return Filters.and(fixedFilter, filter); 332 } 333 } catch (RuntimeException e) { // NOSONAR 334 // catch everything to detect what's going wrong, see NXP-29101 335 log.error("Error during search filter construction, fixedPart: {}, predicates: {}, searchDocumentModel: {}", 336 fixedPart, predicates, searchDocumentModel); 337 throw new NuxeoException("Unable to build search filter", e); 338 } 339 } 340 341 private boolean isNonNullParam(Object[] val) { 342 if (val == null) { 343 return false; 344 } 345 for (Object v : val) { 346 if (v instanceof String) { 347 if (!((String) v).isEmpty()) { 348 return true; 349 } 350 } else if (v instanceof String[]) { 351 if (((String[]) v).length > 0) { 352 return true; 353 } 354 } else if (v != null) { 355 return true; 356 } 357 } 358 return false; 359 } 360 361}