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