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 * $Id$ 020 */ 021package org.nuxeo.elasticsearch.audit.pageprovider; 022 023import static org.nuxeo.elasticsearch.provider.ElasticSearchNxqlPageProvider.DEFAULT_ES_MAX_RESULT_WINDOW_VALUE; 024import static org.nuxeo.elasticsearch.provider.ElasticSearchNxqlPageProvider.ES_MAX_RESULT_WINDOW_PROPERTY; 025 026import java.io.IOException; 027import java.io.Serializable; 028import java.util.ArrayList; 029import java.util.List; 030 031import org.apache.commons.lang3.StringUtils; 032import org.elasticsearch.action.search.SearchRequest; 033import org.elasticsearch.action.search.SearchResponse; 034import org.elasticsearch.search.SearchHit; 035import org.elasticsearch.search.SearchHits; 036import org.elasticsearch.search.sort.SortOrder; 037import org.nuxeo.ecm.core.api.CoreSession; 038import org.nuxeo.ecm.core.api.NuxeoException; 039import org.nuxeo.ecm.core.api.SortInfo; 040import org.nuxeo.ecm.platform.audit.api.LogEntry; 041import org.nuxeo.ecm.platform.audit.api.comment.CommentProcessorHelper; 042import org.nuxeo.ecm.platform.audit.impl.LogEntryImpl; 043import org.nuxeo.ecm.platform.audit.service.AuditBackend; 044import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService; 045import org.nuxeo.ecm.platform.query.api.AbstractPageProvider; 046import org.nuxeo.ecm.platform.query.api.PageProvider; 047import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; 048import org.nuxeo.ecm.platform.query.api.QuickFilter; 049import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition; 050import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder; 051import org.nuxeo.elasticsearch.audit.ESAuditBackend; 052import org.nuxeo.runtime.api.Framework; 053import org.nuxeo.runtime.services.config.ConfigurationService; 054 055import com.fasterxml.jackson.databind.ObjectMapper; 056 057public class ESAuditPageProvider extends AbstractPageProvider<LogEntry> implements PageProvider<LogEntry> { 058 059 private static final long serialVersionUID = 1L; 060 061 protected SearchRequest searchRequest; 062 063 public static final String CORE_SESSION_PROPERTY = "coreSession"; 064 065 public static final String UICOMMENTS_PROPERTY = "generateUIComments"; 066 067 protected static String emptyQuery = "{ \"match_all\" : { }\n }"; 068 069 protected Long maxResultWindow; 070 071 @Override 072 public String toString() { 073 buildAuditQuery(true); 074 return searchRequest.toString(); 075 } 076 077 protected CoreSession getCoreSession() { 078 Object session = getProperties().get(CORE_SESSION_PROPERTY); 079 if (session != null && session instanceof CoreSession) { 080 return (CoreSession) session; 081 } 082 return null; 083 } 084 085 protected void preprocessCommentsIfNeeded(List<LogEntry> entries) { 086 Serializable preprocess = getProperties().get(UICOMMENTS_PROPERTY); 087 088 if (preprocess != null && "true".equalsIgnoreCase(preprocess.toString())) { 089 CoreSession session = getCoreSession(); 090 if (session != null) { 091 CommentProcessorHelper cph = new CommentProcessorHelper(session); 092 cph.processComments(entries); 093 } 094 } 095 } 096 097 @Override 098 public List<LogEntry> getCurrentPage() { 099 100 buildAuditQuery(true); 101 searchRequest.source().from((int) (getCurrentPageIndex() * pageSize)); 102 searchRequest.source().size((int) getMinMaxPageSize()); 103 104 for (SortInfo sortInfo : getSortInfos()) { 105 searchRequest.source() 106 .sort(sortInfo.getSortColumn(), sortInfo.getSortAscending() ? SortOrder.ASC : SortOrder.DESC); 107 } 108 SearchResponse searchResponse = getESBackend().search(searchRequest); 109 List<LogEntry> entries = new ArrayList<>(); 110 SearchHits hits = searchResponse.getHits(); 111 112 // set total number of hits ? 113 setResultsCount(hits.getTotalHits().value); 114 ObjectMapper mapper = new ObjectMapper(); 115 for (SearchHit hit : hits) { 116 try { 117 entries.add(mapper.readValue(hit.getSourceAsString(), LogEntryImpl.class)); 118 } catch (IOException e) { 119 log.error("Error while reading Audit Entry from ES", e); 120 } 121 } 122 preprocessCommentsIfNeeded(entries); 123 124 long t0 = System.currentTimeMillis(); 125 126 CoreSession session = getCoreSession(); 127 if (session != null) { 128 // send event for statistics ! 129 fireSearchEvent(session.getPrincipal(), searchRequest.toString(), entries, System.currentTimeMillis() - t0); 130 } 131 132 return entries; 133 } 134 135 protected boolean isNonNullParam(Object[] val) { 136 if (val == null) { 137 return false; 138 } 139 for (Object v : val) { 140 if (v != null) { 141 if (v instanceof String) { 142 if (!((String) v).isEmpty()) { 143 return true; 144 } 145 } else if (v instanceof String[]) { 146 if (((String[]) v).length > 0) { 147 return true; 148 } 149 } else { 150 return true; 151 } 152 } 153 } 154 return false; 155 } 156 157 protected String getFixedPart() { 158 if (getDefinition().getWhereClause() == null) { 159 return null; 160 } else { 161 String fixedPart = getDefinition().getWhereClause().getFixedPart(); 162 if (fixedPart == null || fixedPart.isEmpty()) { 163 fixedPart = emptyQuery; 164 } 165 return fixedPart; 166 } 167 } 168 169 protected boolean allowSimplePattern() { 170 return true; 171 } 172 173 protected ESAuditBackend getESBackend() { 174 NXAuditEventsService auditEventsService = Framework.getService(NXAuditEventsService.class); 175 AuditBackend backend = auditEventsService.getBackend(); 176 if (backend instanceof ESAuditBackend) { 177 return (ESAuditBackend) backend; 178 } 179 throw new NuxeoException( 180 "Unable to use ESAuditPageProvider if auditEventsService service is not configured to run with ElasticSearch"); 181 } 182 183 protected void buildAuditQuery(boolean includeSort) { 184 PageProviderDefinition def = getDefinition(); 185 Object[] params = getParameters(); 186 List<QuickFilter> quickFilters = getQuickFilters(); 187 String quickFiltersClause = ""; 188 189 if (quickFilters != null && !quickFilters.isEmpty()) { 190 for (QuickFilter quickFilter : quickFilters) { 191 String clause = quickFilter.getClause(); 192 if (!quickFiltersClause.isEmpty() && clause != null) { 193 quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause); 194 } else { 195 quickFiltersClause = clause != null ? clause : ""; 196 } 197 } 198 } 199 200 WhereClauseDefinition whereClause = def.getWhereClause(); 201 if (whereClause == null) { 202 // Simple Pattern 203 204 if (!allowSimplePattern()) { 205 throw new UnsupportedOperationException("This page provider requires a explicit Where Clause"); 206 } 207 String originalPattern = def.getPattern(); 208 String pattern = quickFiltersClause.isEmpty() ? originalPattern 209 : StringUtils.containsIgnoreCase(originalPattern, " WHERE ") 210 ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause) 211 : originalPattern + " WHERE " + quickFiltersClause; 212 213 String baseQuery = getESBackend().expandQueryVariables(pattern, params); 214 searchRequest = getESBackend().buildQuery(baseQuery, null); 215 } else { 216 217 // Add the quick filters clauses to the fixed part 218 String fixedPart = getFixedPart(); 219 if (!StringUtils.isBlank(quickFiltersClause)) { 220 fixedPart = (!StringUtils.isBlank(fixedPart)) 221 ? NXQLQueryBuilder.appendClause(fixedPart, quickFiltersClause) 222 : quickFiltersClause; 223 } 224 225 // Where clause based on DocumentModel 226 String baseQuery = getESBackend().expandQueryVariables(fixedPart, params); 227 searchRequest = getESBackend().buildSearchQuery(baseQuery, whereClause.getPredicates(), 228 getSearchDocumentModel()); 229 } 230 } 231 232 @Override 233 public void refresh() { 234 setCurrentPageOffset(0); 235 super.refresh(); 236 } 237 238 @Override 239 public long getResultsCount() { 240 return resultsCount; 241 } 242 243 @Override 244 public List<SortInfo> getSortInfos() { 245 246 // because ContentView can reuse PageProVider without redefining columns 247 // ensure compat for ContentView configured with JPA log.* sort syntax 248 List<SortInfo> sortInfos = super.getSortInfos(); 249 for (SortInfo si : sortInfos) { 250 if (si.getSortColumn().startsWith("log.")) { 251 si.setSortColumn(si.getSortColumn().substring(4)); 252 } 253 } 254 return sortInfos; 255 } 256 257 @Override 258 public boolean isLastPageAvailable() { 259 if ((getResultsCount() + getPageSize()) <= getMaxResultWindow()) { 260 return super.isNextPageAvailable(); 261 } 262 return false; 263 } 264 265 @Override 266 public boolean isNextPageAvailable() { 267 if ((getCurrentPageOffset() + 2 * getPageSize()) <= getMaxResultWindow()) { 268 return super.isNextPageAvailable(); 269 } 270 return false; 271 } 272 273 @Override 274 public long getPageLimit() { 275 return getMaxResultWindow() / getPageSize(); 276 } 277 278 /** 279 * Returns the max result window where the PP can navigate without raising Elasticsearch 280 * QueryPhaseExecutionException. {@code from + size} must be less than or equal to this value. 281 * 282 * @since 9.2 283 */ 284 public long getMaxResultWindow() { 285 if (maxResultWindow == null) { 286 ConfigurationService cs = Framework.getService(ConfigurationService.class); 287 maxResultWindow = cs.getLong(ES_MAX_RESULT_WINDOW_PROPERTY, DEFAULT_ES_MAX_RESULT_WINDOW_VALUE); 288 } 289 return maxResultWindow; 290 } 291 292 /** 293 * Set the max result window where the PP can navigate, for testing purpose. 294 * 295 * @since 9.2 296 */ 297 public void setMaxResultWindow(long maxResultWindow) { 298 this.maxResultWindow = maxResultWindow; 299 } 300 301}