001/*
002 * (C) Copyright 2017 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 *     Estelle Giuly <egiuly@nuxeo.com>
018 */
019
020package org.nuxeo.audit.storage.impl;
021
022import java.io.Serializable;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.function.Function;
031import java.util.stream.Collectors;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.core.api.CursorService;
036import org.nuxeo.ecm.core.api.DocumentModelList;
037import org.nuxeo.ecm.core.api.ScrollResult;
038import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
039import org.nuxeo.ecm.core.query.sql.model.Operator;
040import org.nuxeo.ecm.core.query.sql.model.Predicate;
041import org.nuxeo.ecm.core.query.sql.model.StringLiteral;
042import org.nuxeo.ecm.directory.Directory;
043import org.nuxeo.ecm.directory.Session;
044import org.nuxeo.ecm.directory.api.DirectoryService;
045import org.nuxeo.ecm.platform.audit.api.AuditQueryBuilder;
046import org.nuxeo.ecm.platform.audit.api.AuditStorage;
047import org.nuxeo.runtime.api.Framework;
048
049/**
050 * Audit storage implementation for an Audit directory.
051 * 
052 * @since 9.10
053 */
054public class DirectoryAuditStorage implements AuditStorage {
055
056    private static final Log log = LogFactory.getLog(DirectoryAuditStorage.class);
057
058    public static final String NAME = "DirectoryAuditStorage";
059
060    public static final String DIRECTORY_NAME = "auditStorage";
061
062    public static final String ID_COLUMN = "id";
063
064    public static final String JSON_COLUMN = "entry";
065
066    protected CursorService<Iterator<String>, String, String> cursorService = new CursorService<>(Function.identity());
067
068    protected Directory getAuditDirectory() {
069        return Framework.getService(DirectoryService.class).getDirectory(DIRECTORY_NAME);
070    }
071
072    /**
073     * Insert entries as Json in the Audit directory.
074     */
075    @Override
076    public void append(List<String> jsonEntries) {
077        try (Session session = getAuditDirectory().getSession()) {
078            for (String jsonEntry : jsonEntries) {
079                Framework.doPrivileged(() -> session.createEntry(Collections.singletonMap(JSON_COLUMN, jsonEntry)));
080            }
081        }
082    }
083
084    /**
085     * Scroll log entries in the Audit directory, given a scroll Id.
086     */
087    @Override
088    public ScrollResult<String> scroll(String scrollId) {
089        return cursorService.scroll(scrollId);
090    }
091
092    /**
093     * Scroll log entries in the Audit directory, given an audit query builder.
094     */
095    @Override
096    public ScrollResult<String> scroll(AuditQueryBuilder queryBuilder, int batchSize, int keepAlive) {
097        cursorService.checkForTimedOutScroll();
098        List<String> logEntries = queryLogs(queryBuilder);
099        String scrollId = cursorService.registerCursor(logEntries.iterator(), batchSize, keepAlive);
100        return scroll(scrollId);
101    }
102
103    /**
104     * Query log entries in the Audit directory, given an audit query builder. Does not support literals other than
105     * StringLiteral: see {@link Session#query(Map, Set, Map, boolean, int, int)}.
106     */
107    protected List<String> queryLogs(AuditQueryBuilder queryBuilder) {
108        // Get the predicates filter map from the query builder.
109        Map<String, Serializable> filter = new HashMap<>();
110        Set<String> fulltext = null;
111        MultiExpression predicate = (MultiExpression) queryBuilder.predicate();
112        @SuppressWarnings("unchecked")
113        List<Predicate> predicateList = (List<Predicate>) ((List<?>) predicate.values);
114        for (Predicate p : predicateList) {
115            String rvalue;
116            if (p.rvalue instanceof StringLiteral) {
117                rvalue = ((StringLiteral) p.rvalue).asString();
118            } else {
119                rvalue = p.rvalue.toString();
120                log.warn(String.format(
121                        "Scrolling audit logs with a query builder containing non-string literals is not supported: %s.",
122                        rvalue));
123            }
124            filter.put(p.lvalue.toString(), rvalue);
125
126            if (fulltext == null && Arrays.asList(Operator.LIKE, Operator.ILIKE).contains(p.operator)) {
127                fulltext = Collections.singleton(JSON_COLUMN);
128            }
129        }
130
131        // Get the orderBy map from the query builder.
132        Map<String, String> orderBy = queryBuilder.orders().stream().collect(
133                Collectors.toMap(o -> o.reference.name, o -> o.isDescending ? "desc" : "asc"));
134
135        // Get the limit and offset from the query builder.
136        int limit = (int) queryBuilder.limit();
137        int offset = (int) queryBuilder.offset();
138
139        // Query the Json Entries via the directory session.
140        Directory directory = getAuditDirectory();
141        try (Session session = directory.getSession()) {
142            DocumentModelList jsonEntriesDocs = session.query(filter, fulltext, orderBy, false, limit, offset);
143
144            // Build a list of Json Entries from the document model list.
145            String auditPropertyName = directory.getSchema() + ":" + JSON_COLUMN;
146            return jsonEntriesDocs.stream()
147                                  .map(doc -> doc.getPropertyValue(auditPropertyName))
148                                  .map(String::valueOf)
149                                  .collect(Collectors.toList());
150        }
151    }
152
153}