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