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}