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