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