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