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}