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