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}