001/*
002 * (C) Copyright 2013 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 *     Martin Pernollet
018 */
019
020package org.nuxeo.ecm.platform.groups.audit.service.acl;
021
022import java.util.Collection;
023import java.util.HashSet;
024import java.util.Set;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.apache.poi.hssf.util.HSSFColor;
029import org.apache.poi.ss.usermodel.CellStyle;
030import org.apache.poi.ss.usermodel.Font;
031import org.apache.poi.ss.usermodel.HorizontalAlignment;
032import org.apache.poi.ss.usermodel.Sheet;
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
036import org.nuxeo.ecm.platform.groups.audit.service.acl.ReportLayoutSettings.SpanMode;
037import org.nuxeo.ecm.platform.groups.audit.service.acl.data.DataProcessor;
038import org.nuxeo.ecm.platform.groups.audit.service.acl.data.DataProcessor.ProcessorStatus;
039import org.nuxeo.ecm.platform.groups.audit.service.acl.data.DataProcessorPaginated;
040import org.nuxeo.ecm.platform.groups.audit.service.acl.data.DocumentSummary;
041import org.nuxeo.ecm.platform.groups.audit.service.acl.data.IDataProcessor;
042import org.nuxeo.ecm.platform.groups.audit.service.acl.excel.AclNameShortner;
043import org.nuxeo.ecm.platform.groups.audit.service.acl.excel.ByteColor;
044import org.nuxeo.ecm.platform.groups.audit.service.acl.excel.ExcelBuilder;
045import org.nuxeo.ecm.platform.groups.audit.service.acl.excel.ExcelBuilder.Type;
046import org.nuxeo.ecm.platform.groups.audit.service.acl.excel.ExcelBuilderMultiSheet;
047import org.nuxeo.ecm.platform.groups.audit.service.acl.excel.IExcelBuilder;
048import org.nuxeo.ecm.platform.groups.audit.service.acl.filter.AcceptsAllContent;
049import org.nuxeo.ecm.platform.groups.audit.service.acl.filter.IContentFilter;
050import org.nuxeo.ecm.platform.groups.audit.service.acl.utils.MessageAccessor;
051
052import com.google.common.collect.Multimap;
053
054/**
055 * A builder works in three phases:
056 * <ul>
057 * <li>Fetch documents, possibly using paging.
058 * <li>Extract a document summary for each document.
059 * <li>Render documents' summary:
060 * <ul>
061 * <li>Render header and define column layout
062 * <li>Render file tree and define row layout
063 * <li>Render ACL matrix
064 * </ul>
065 * </ul>
066 * One can apply a {@link IContentFilter} to ignore some users/groups. This report builder uses one column per user, and
067 * write the list of existing ACL in one cell, by using "," as separator character. A denying ACL is indicated by !S,
068 * where S is the short name given to the ACL, as stated by the {@link AclNameShortner}.
069 *
070 * @author Martin Pernollet <mpernollet@nuxeo.com>
071 */
072public class AclExcelLayoutBuilder implements IAclExcelLayoutBuilder {
073    protected static Log log = LogFactory.getLog(AclExcelLayoutBuilder.class);
074
075    protected static final String PROPERTY_MAIN_SHEET_NAME = "message.acl.audit.xl.mainsheet";
076
077    protected static final String PROPERTY_LEGEND_SHEET_NAME = "message.acl.audit.xl.legend";
078
079    protected static final String PROPERTY_LEGEND_LOCK_INHERITANCE = "message.acl.audit.xl.legend.lockInheritance";
080
081    protected static final String PROPERTY_LEGEND_PERM_DENIED = "message.acl.audit.xl.legend.denied";
082
083    protected IExcelBuilder excel = new ExcelBuilder();
084
085    protected static final int CELL_WIDTH_UNIT = 256;
086
087    public static final int STATUS_ROW = 0;
088
089    public static final int STATUS_COL = 0;
090
091    /* layout */
092    protected ReportLayoutSettings layoutSettings;
093
094    protected ReportLayout layout;
095
096    protected int treeLineCursor = 0;
097
098    protected CellStyle userHeaderStyle;
099
100    protected CellStyle aclHeaderStyle;
101
102    protected CellStyle lockInheritanceStyle;
103
104    protected CellStyle grayTextStyle;
105
106    protected int mainSheetId;
107
108    protected int legendSheetId;
109
110    protected String mainSheetName;
111
112    protected String legendSheetName;
113
114    protected String legendLockInheritance = "Permission inheritance locked";
115
116    protected String legendPermissionDenied = "Permission denied";
117
118    public static ReportLayoutSettings defaultLayout() {
119        ReportLayoutSettings layout = new ReportLayoutSettings();
120        layout.userHeaderHeight = 1000;
121        layout.userHeaderRotation = 45;
122        layout.fileTreeColumnWidth = 2; // in number of char
123        layout.aclColumnWidth = 4;
124        layout.defaultRowHeight = 100;
125        layout.splitPaneX = 500;
126        layout.splitPaneY = 1500;
127        layout.freezePaneRowSplit = 1;
128        layout.treeLineCursorRowStart = 1;
129        layout.spanMode = SpanMode.COLUMN_OVERFLOW_ON_NEXT_SHEETS;
130        layout.zoomRatioDenominator = 2;
131        layout.zoomRatioNumerator = 1;
132        layout.showFullPath = false;
133
134        // data fetch setting
135        layout.pageSize = 1000;
136
137        return layout;
138    }
139
140    /* tools */
141    protected IContentFilter filter;
142
143    protected AclNameShortner shortner;
144
145    protected IDataProcessor data;
146
147    public AclExcelLayoutBuilder() {
148        this(defaultLayout());
149    }
150
151    public AclExcelLayoutBuilder(IContentFilter filter) {
152        this(defaultLayout(), filter);
153    }
154
155    public AclExcelLayoutBuilder(ReportLayoutSettings layout) {
156        this(layout, null);
157    }
158
159    public AclExcelLayoutBuilder(ReportLayoutSettings layout, IContentFilter filter) {
160        this.layoutSettings = layout;
161
162        if (SpanMode.NONE.equals(layout.spanMode))
163            excel = new ExcelBuilder(Type.XLS, "Permissions"); // missing context, no I18N
164        else if (SpanMode.COLUMN_OVERFLOW_ON_NEXT_SHEETS.equals(layout.spanMode)) {
165            excel = new ExcelBuilderMultiSheet(Type.XLS, "Permissions"); // missing context, no I18N
166            ((ExcelBuilderMultiSheet) excel).setMultiSheetColumns(true);
167        } else
168            throw new IllegalArgumentException("layout span mode unknown: " + layout.spanMode);
169
170        if (filter == null)
171            this.filter = new AcceptsAllContent();
172        else
173            this.filter = filter;
174
175        if (layoutSettings.pageSize > 0)
176            this.data = new DataProcessorPaginated(this.filter, layoutSettings.pageSize);
177        else
178            this.data = new DataProcessor(this.filter);
179
180        this.shortner = new AclNameShortner();
181        this.layout = new ReportLayout();
182    }
183
184    @Override
185    public void renderAudit(CoreSession session) {
186        renderAudit(session, session.getRootDocument(), true);
187    }
188
189    @Override
190    public void renderAudit(CoreSession session, final DocumentModel doc) {
191        renderAudit(session, doc, true);
192    }
193
194    @Override
195    public void renderAudit(CoreSession session, final DocumentModel doc, boolean unrestricted) {
196        renderAudit(session, doc, unrestricted, 0);
197    }
198
199    @Override
200    public void renderAudit(CoreSession session, final DocumentModel doc, boolean unrestricted, final int timeout)
201            {
202        if (!unrestricted) {
203            analyzeAndRender(session, doc, timeout);
204        } else {
205            UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(session) {
206                @Override
207                public void run() {
208                    analyzeAndRender(session, doc, timeout);
209                }
210            };
211            runner.runUnrestricted();
212        }
213    }
214
215    protected void analyzeAndRender(CoreSession session, final DocumentModel doc, int timeout) {
216        log.debug("start processing data");
217        data.analyze(session, doc, timeout);
218
219        configure(session);
220        render(data);
221    }
222
223    /* EXCEL RENDERING */
224
225    protected void configure(CoreSession session) {
226        // mainSheetName = MessageAccessor.get(session, PROPERTY_MAIN_SHEET_NAME);
227        legendSheetName = MessageAccessor.get(session, PROPERTY_LEGEND_SHEET_NAME);
228        legendLockInheritance = MessageAccessor.get(session, PROPERTY_LEGEND_LOCK_INHERITANCE);
229        legendPermissionDenied = MessageAccessor.get(session, PROPERTY_LEGEND_PERM_DENIED);
230    }
231
232    protected void render(IDataProcessor data) {
233        int minDepth = data.getDocumentTreeMinDepth();
234        int maxDepth = data.getDocumentTreeMaxDepth();
235        int colStart = maxDepth + (layoutSettings.showFullPath ? 1 : 0);
236
237        mainSheetId = excel.getCurrentSheetId();
238        legendSheetId = excel.newSheet(excel.getCurrentSheetId() + 1, legendSheetName);
239
240        renderInit();
241        renderHeader(colStart, data.getUserAndGroups(), data.getPermissions());
242        renderFileTreeAndAclMatrix(data.getAllDocuments(), minDepth, maxDepth);
243        formatFileTreeCellLayout(maxDepth, minDepth, colStart);
244        renderLegend(data.getStatus(), data.getInformation());
245        renderFinal();
246    }
247
248    /** Initialize layout data model and pre-built cell styles */
249    protected void renderInit() {
250        layout.reset();
251
252        userHeaderStyle = excel.newCellStyle();
253        userHeaderStyle.setFont(excel.getBoldFont());
254        userHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
255        if (layoutSettings.userHeaderRotation != 0)
256            userHeaderStyle.setRotation((short) layoutSettings.userHeaderRotation);
257
258        aclHeaderStyle = excel.newCellStyle();
259        aclHeaderStyle.setFont(excel.newFont(layoutSettings.aclHeaderFontSize));
260        aclHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
261        if (layoutSettings.aclHeaderRotation != 0)
262            aclHeaderStyle.setRotation((short) layoutSettings.aclHeaderRotation);
263
264        lockInheritanceStyle = excel.newColoredCellStyle(ByteColor.BLUE);
265
266        grayTextStyle = excel.newCellStyle();
267        Font f = excel.newFont();
268        f.setColor(HSSFColor.HSSFColorPredefined.GREY_50_PERCENT.getIndex());
269        grayTextStyle.setFont(f);
270        // grayTextStyle.set
271    }
272
273    /** Perform various general tasks, such as setting the current sheet zoom. */
274    protected void renderFinal() {
275        for (Sheet s : excel.getAllSheets()) {
276            s.setZoom(layoutSettings.zoomRatioNumerator * layoutSettings.zoomRatioDenominator);
277        }
278    }
279
280    /* HEADER RENDERING */
281
282    /**
283     * Write users and groups on the first row. Memorize the user (or group) column which can later be retrieved with
284     * getColumn(user)
285     */
286    protected void renderHeader(int tableStartColumn, Set<String> userOrGroups, Set<String> permission) {
287        renderHeaderUsers(tableStartColumn, userOrGroups);
288    }
289
290    protected void renderHeaderUsers(int tableStartColumn, Set<String> userOrGroups) {
291        int column = tableStartColumn;
292        for (String userOrGroup : userOrGroups) {
293            excel.setCell(0, column, userOrGroup, userHeaderStyle);
294            layout.setUserColumn(column, userOrGroup);
295            column++;
296        }
297        excel.setRowHeight(0, layoutSettings.userHeaderHeight);
298    }
299
300    /* FILE TREE AND MATRIX CONTENT RENDERING */
301
302    protected void renderFileTreeAndAclMatrix(Collection<DocumentSummary> analyses, int minDepth, int maxDepth)
303            {
304        treeLineCursor = layoutSettings.treeLineCursorRowStart;
305
306        for (DocumentSummary summary : analyses) {
307            renderFilename(summary.getTitle(), summary.getDepth() - minDepth, summary.isAclLockInheritance());
308
309            if (layoutSettings.showFullPath)
310                excel.setCell(treeLineCursor, maxDepth - minDepth + 1, summary.getPath());
311
312            if (summary.getAclInheritedByUser() != null)
313                renderAcl(summary.getAclByUser(), summary.getAclInheritedByUser());
314            else
315                renderAcl(summary.getAclByUser());
316            treeLineCursor++;
317        }
318    }
319
320    protected void renderFilename(String title, int depth, boolean lockInheritance) {
321        // draw title
322        excel.setCell(treeLineCursor, depth, title);
323
324        // draw ace inheritance locker
325        if (depth > 0 && lockInheritance) {
326            excel.setCell(treeLineCursor, depth - 1, "", lockInheritanceStyle);
327        }
328    }
329
330    /** Render a row with all ACL of a given input file. */
331    protected void renderAcl(Multimap<String, Pair<String, Boolean>> userAcls) {
332        renderAcl(userAcls, (CellStyle) null);
333    }
334
335    protected void renderAcl(Multimap<String, Pair<String, Boolean>> userAcls, CellStyle style) {
336        for (String user : userAcls.keySet()) {
337            int column = layout.getUserColumn(user);
338            String info = formatAcl(userAcls.get(user));
339            excel.setCell(treeLineCursor, column, info, style);
340        }
341    }
342
343    /**
344     * Render local AND inherited ACL.
345     * <ul>
346     * <li>Local acl only are rendered with default font.
347     * <li>Inherited acl only are rendered with gray font.
348     * <li>Mixed acl (local and inherited) are rendered with default font.
349     * </ul>
350     */
351    protected void renderAcl(Multimap<String, Pair<String, Boolean>> localAcls,
352            Multimap<String, Pair<String, Boolean>> inheritedAcls) {
353        Set<String> users = new HashSet<>();
354        users.addAll(localAcls.keySet());
355        users.addAll(inheritedAcls.keySet());
356
357        for (String user : users) {
358            int column = layout.getUserColumn(user);
359            String localAclsString = formatAcl(localAcls.get(user));
360            String inheritedAclsString = formatAcl(inheritedAcls.get(user));
361
362            if ("".equals(localAclsString) && "".equals(inheritedAclsString)) {
363            } else if (!"".equals(localAclsString) && !"".equals(inheritedAclsString)) {
364                String info = localAclsString + "," + inheritedAclsString;
365                excel.setCell(treeLineCursor, column, info);
366            } else if (!"".equals(localAclsString) && "".equals(inheritedAclsString)) {
367                String info = localAclsString;
368                excel.setCell(treeLineCursor, column, info);
369            } else if ("".equals(localAclsString) && !"".equals(inheritedAclsString)) {
370                String info = inheritedAclsString;
371                excel.setCell(treeLineCursor, column, info, grayTextStyle);
372            }
373        }
374    }
375
376    protected void renderLegend(ProcessorStatus status, String message) {
377        ((ExcelBuilderMultiSheet) excel).setMultiSheetColumns(false);
378
379        excel.setCurrentSheetId(legendSheetId);
380
381        int row = STATUS_ROW;
382        int col = STATUS_COL;
383        int off = renderLegendErrorMessage(row, col, status, message);
384        off = renderLegendAcl(off + 1, 0);
385        off++;
386        excel.setCell(off, col, "", lockInheritanceStyle);
387        excel.setCell(off, col + 1, legendLockInheritance);
388        off++;
389    }
390
391    protected int renderLegendErrorMessage(int row, int col, ProcessorStatus status, String message) {
392        if (!ProcessorStatus.SUCCESS.equals(status)) {
393            excel.setCell(row++, col, "Status: " + status);
394            if (message != null && !"".equals(message))
395                excel.setCell(row++, col, "Message: " + message);
396        }
397        return row;
398    }
399
400    protected int renderLegendAcl(int row, int col) {
401        excel.setCell(row++, col, "ACL meaning");
402        for (String shortName : shortner.getShortNames()) {
403            String fullName = shortner.getFullName(shortName);
404            excel.setCell(row, col, shortName);
405            excel.setCell(row, col + 1, fullName);
406            row++;
407        }
408        return row;
409    }
410
411    /* ACL TEXT FORMATTER FOR MATRIX */
412
413    /**
414     * Renders all ACE separated by a , Each ACE name is formated using {@link formatAce(Pair<String, Boolean> ace)}
415     *
416     * @return
417     */
418    protected String formatAcl(Collection<Pair<String, Boolean>> acls) {
419        StringBuilder sb = new StringBuilder();
420        int k = 0;
421        for (Pair<String, Boolean> ace : acls) {
422            sb.append(formatAce(ace));
423            if ((++k) < acls.size())
424                sb.append(",");
425        }
426        return sb.toString();
427    }
428
429    protected String formatAce(Pair<String, Boolean> ace) {
430        if (ace.b)
431            return formatPermission(ace.a);
432        else
433            return "!" + formatPermission(ace.a);
434    }
435
436    protected String formatPermission(String permission) {
437        return shortner.getShortName(permission);
438    }
439
440    /* CELL FORMATTER */
441
442    /**
443     * Set column of size of each file tree column, and apply a freeze pan to fix the tree columns and header rows.
444     */
445    protected void formatFileTreeCellLayout(int maxDepth, int minDepth, int colStart) {
446        int realMax = maxDepth - minDepth;
447        for (int i = 0; i < realMax; i++) {
448            excel.setColumnWidth(i, (int) (layoutSettings.fileTreeColumnWidth * CELL_WIDTH_UNIT));
449        }
450        excel.setColumnWidthAuto(realMax);
451        excel.setFreezePane(colStart, layoutSettings.freezePaneRowSplit);
452    }
453
454    /* */
455
456    /** {@inheritDoc} */
457    @Override
458    public IExcelBuilder getExcel() {
459        return excel;
460    }
461}