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