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 <[email protected]> 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}