1 /*
2  * This file is part of the LibreOffice project.
3  *
4  * This Source Code Form is subject to the terms of the Mozilla Public
5  * License, v. 2.0. If a copy of the MPL was not distributed with this
6  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7  *
8  * This file incorporates work covered by the following license notice:
9  *
10  *   Licensed to the Apache Software Foundation (ASF) under one or more
11  *   contributor license agreements. See the NOTICE file distributed
12  *   with this work for additional information regarding copyright
13  *   ownership. The ASF licenses this file to you under the Apache
14  *   License, Version 2.0 (the "License"); you may not use this file
15  *   except in compliance with the License. You may obtain a copy of
16  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
17  */
18 package org.libreoffice.report.pentaho.output.spreadsheet;
19 
20 import org.libreoffice.report.DataSourceFactory;
21 import org.libreoffice.report.ImageService;
22 import org.libreoffice.report.InputRepository;
23 import org.libreoffice.report.OfficeToken;
24 import org.libreoffice.report.OutputRepository;
25 import org.libreoffice.report.pentaho.OfficeNamespaces;
26 import org.libreoffice.report.pentaho.PentahoReportEngineMetaData;
27 import org.libreoffice.report.pentaho.model.OfficeMasterPage;
28 import org.libreoffice.report.pentaho.model.OfficeMasterStyles;
29 import org.libreoffice.report.pentaho.model.OfficeStyle;
30 import org.libreoffice.report.pentaho.model.OfficeStyles;
31 import org.libreoffice.report.pentaho.model.OfficeStylesCollection;
32 import org.libreoffice.report.pentaho.model.PageSection;
33 import org.libreoffice.report.pentaho.output.OfficeDocumentReportTarget;
34 import org.libreoffice.report.pentaho.output.StyleUtilities;
35 import org.libreoffice.report.pentaho.output.text.MasterPageFactory;
36 import org.libreoffice.report.pentaho.styles.LengthCalculator;
37 
38 import java.io.IOException;
39 
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 
46 import org.jfree.layouting.input.style.values.CSSNumericType;
47 import org.jfree.layouting.input.style.values.CSSNumericValue;
48 import org.jfree.layouting.util.AttributeMap;
49 import org.jfree.report.DataFlags;
50 import org.jfree.report.DataSourceException;
51 import org.jfree.report.JFreeReportInfo;
52 import org.jfree.report.ReportProcessingException;
53 import org.jfree.report.flow.ReportJob;
54 import org.jfree.report.flow.ReportStructureRoot;
55 import org.jfree.report.flow.ReportTargetUtil;
56 import org.jfree.report.structure.Element;
57 import org.jfree.report.structure.Section;
58 import org.jfree.report.util.IntegerCache;
59 
60 import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
61 import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
62 import org.pentaho.reporting.libraries.xmlns.common.AttributeList;
63 import org.pentaho.reporting.libraries.xmlns.writer.XmlWriter;
64 import org.pentaho.reporting.libraries.xmlns.writer.XmlWriterSupport;
65 
66 
67 /**
68  * Creation-Date: 03.11.2007
69  *
70  */
71 public class SpreadsheetRawReportTarget extends OfficeDocumentReportTarget
72 {
73 
74     private static final String[] FOPROPS = new String[]
75     {
76         "letter-spacing", "font-variant", "text-transform"
77     };
78     private static final String NUMBERCOLUMNSSPANNED = "number-columns-spanned";
79     private static final String[] STYLEPROPS = new String[]
80     {
81         "text-combine", "font-pitch-complex", "text-rotation-angle", "font-name", "text-blinking", "letter-kerning", "text-combine-start-char", "text-combine-end-char", "text-position", "text-scale"
82     };
83     private static final int CELL_WIDTH_FACTOR = 10000;
84     private static final String TRANSPARENT = "transparent";
85     private boolean paragraphFound = false;
86     private boolean paragraphHandled = false;
87 
88     /**
89      * This class represents a column boundary, not in width, but it's actual boundary location. One of the motivations
90      * for creating this class was to be able to record the boundaries for each incoming table while consuming as few
91      * objects/memory as possible.
92      */
93     private static class ColumnBoundary implements Comparable<ColumnBoundary>
94     {
95 
96         private final Set<Integer> tableIndices;
97         private final long boundary;
98 
ColumnBoundary(final long boundary)99         private ColumnBoundary(final long boundary)
100         {
101             this.tableIndices = new HashSet<Integer>();
102             this.boundary = boundary;
103         }
104 
addTableIndex(final int table)105         public void addTableIndex(final int table)
106         {
107             tableIndices.add(IntegerCache.getInteger(table));
108         }
109 
getBoundary()110         public float getBoundary()
111         {
112             return boundary;
113         }
114 
isContainedByTable(final int table)115         public boolean isContainedByTable(final int table)
116         {
117             final Integer index = IntegerCache.getInteger(table);
118             return tableIndices.contains(index);
119         }
120 
compareTo(final ColumnBoundary arg0)121         public int compareTo(final ColumnBoundary arg0)
122         {
123             if (arg0.equals(this))
124             {
125                 return 0;
126             }
127             if (boundary > arg0.boundary)
128             {
129                 return 1;
130             }
131             else
132             {
133                 return -1;
134             }
135         }
136 
137         @Override
equals(final Object obj)138         public boolean equals(final Object obj)
139         {
140             return obj instanceof ColumnBoundary && ((ColumnBoundary) obj).boundary == boundary;
141         }
142 
143         @Override
hashCode()144         public int hashCode()
145         {
146             assert false : "hashCode not designed";
147             return 42; // any arbitrary constant will do
148         }
149     }
150     private String tableBackgroundColor; // null means transparent ...
151     private boolean elementBoundaryCollectionPass;
152     private boolean oleHandled;
153     private final List<ColumnBoundary> columnBoundaryList;
154     private long currentRowBoundaryMarker;
155     private ColumnBoundary[] sortedBoundaryArray;
156     private ColumnBoundary[] boundariesForTableArray;
157     private int tableCounter;
158     private int columnCounter;
159     private int columnSpanCounter;
160     private int currentSpan = 0;
161     private String unitsOfMeasure;
162     final private List<AttributeMap> shapes;
163     final private List<AttributeMap> ole;
164     final private List<CSSNumericValue> rowHeights;
165 
SpreadsheetRawReportTarget(final ReportJob reportJob, final ResourceManager resourceManager, final ResourceKey baseResource, final InputRepository inputRepository, final OutputRepository outputRepository, final String target, final ImageService imageService, final DataSourceFactory dataSourceFactory)166     public SpreadsheetRawReportTarget(final ReportJob reportJob,
167             final ResourceManager resourceManager,
168             final ResourceKey baseResource,
169             final InputRepository inputRepository,
170             final OutputRepository outputRepository,
171             final String target,
172             final ImageService imageService,
173             final DataSourceFactory dataSourceFactory)
174             throws ReportProcessingException
175     {
176         super(reportJob, resourceManager, baseResource, inputRepository, outputRepository, target, imageService, dataSourceFactory);
177         columnBoundaryList = new ArrayList<ColumnBoundary>();
178         elementBoundaryCollectionPass = true;
179         rowHeights = new ArrayList<CSSNumericValue>();
180         shapes = new ArrayList<AttributeMap>();
181         ole = new ArrayList<AttributeMap>();
182         oleHandled = false;
183     }
184 
185     @Override
startOther(final AttributeMap attrs)186     public void startOther(final AttributeMap attrs) throws DataSourceException, ReportProcessingException
187     {
188         if (ReportTargetUtil.isElementOfType(JFreeReportInfo.REPORT_NAMESPACE, OfficeToken.OBJECT_OLE, attrs))
189         {
190             if (isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
191             {
192                 ole.add(attrs);
193             }
194             oleHandled = true;
195             return;
196         }
197         final String namespace = ReportTargetUtil.getNamespaceFromAttribute(attrs);
198         if (isRepeatingSection() || isFilteredNamespace(namespace))
199         {
200             return;
201         }
202 
203         final String elementType = ReportTargetUtil.getElemenTypeFromAttribute(attrs);
204         if (OfficeNamespaces.TEXT_NS.equals(namespace) && OfficeToken.P.equals(elementType) && !paragraphHandled)
205         {
206             paragraphFound = true;
207             return;
208         }
209 
210         if (OfficeNamespaces.DRAWING_NS.equals(namespace) && OfficeToken.FRAME.equals(elementType))
211         {
212             if (isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
213             {
214                 final LengthCalculator len = new LengthCalculator();
215                 for (int i = 0; i < rowHeights.size(); i++)
216                 {
217                     len.add(rowHeights.get(i));
218                 }
219 
220                 rowHeights.clear();
221                 final CSSNumericValue currentRowHeight = len.getResult();
222                 rowHeights.add(currentRowHeight);
223                 attrs.setAttribute(OfficeNamespaces.DRAWING_NS, "z-index", String.valueOf(shapes.size()));
224                 final String y = (String) attrs.getAttribute(OfficeNamespaces.SVG_NS, "y");
225                 if (y != null)
226                 {
227                     len.add(parseLength(y));
228                     final CSSNumericValue currentY = len.getResult();
229                     attrs.setAttribute(OfficeNamespaces.SVG_NS, "y", currentY.getValue() + currentY.getType().getType());
230                 }
231                 shapes.add(attrs);
232             }
233             return;
234         }
235         if (oleHandled)
236         {
237             if (isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
238             {
239                 ole.add(attrs);
240             }
241             return;
242         }
243 
244         // if this is the report namespace, write out a table definition ..
245         if (OfficeNamespaces.TABLE_NS.equals(namespace) && OfficeToken.TABLE.equals(elementType))
246         {
247             // whenever we see a new table, we increment our tableCounter
248             // this is used to keep tracked of the boundary conditions per table
249             tableCounter++;
250         }
251 
252         if (isElementBoundaryCollectionPass())
253         {
254             collectBoundaryForElement(attrs);
255         }
256         else
257         {
258             try
259             {
260                 processElement(attrs, namespace, elementType);
261             }
262             catch (IOException e)
263             {
264                 throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
265             }
266         }
267     }
268 
269     @Override
startReportSection(final AttributeMap attrs, final int role)270     protected void startReportSection(final AttributeMap attrs, final int role) throws ReportProcessingException
271     {
272         if ((role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_HEADER || role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_FOOTER) && (!PageSection.isPrintWithReportHeader(attrs) || !PageSection.isPrintWithReportFooter(attrs)))
273         {
274             startBuffering(new OfficeStylesCollection(), true);
275         }
276         else
277         {
278             super.startReportSection(attrs, role);
279         }
280     }
281 
282     @Override
endReportSection(final AttributeMap attrs, final int role)283     protected void endReportSection(final AttributeMap attrs, final int role) throws IOException, ReportProcessingException
284     {
285         if ((role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_HEADER || role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_FOOTER) && (!PageSection.isPrintWithReportHeader(attrs) || !PageSection.isPrintWithReportFooter(attrs)))
286         {
287             finishBuffering();
288         }
289         else
290         {
291             super.endReportSection(attrs, role);
292         }
293     }
294 
handleParagraph()295     private void handleParagraph()
296     {
297         if (paragraphFound)
298         {
299             try
300             {
301                 final XmlWriter xmlWriter = getXmlWriter();
302                 xmlWriter.writeTag(OfficeNamespaces.TEXT_NS, OfficeToken.P, null, XmlWriterSupport.OPEN);
303                 paragraphHandled = true;
304                 paragraphFound = false;
305             }
306             catch (IOException ex)
307             {
308                 LOGGER.error("ReportProcessing failed", ex);
309             }
310         }
311     }
312 
processElement(final AttributeMap attrs, final String namespace, final String elementType)313     private void processElement(final AttributeMap attrs, final String namespace, final String elementType)
314             throws IOException, ReportProcessingException
315     {
316         final XmlWriter xmlWriter = getXmlWriter();
317 
318         if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE, attrs))
319         {
320             // a new table means we must clear our "calculated" table boundary array cache
321             boundariesForTableArray = null;
322 
323             final String tableStyle = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
324             if (tableStyle == null)
325             {
326                 tableBackgroundColor = null;
327             }
328             else
329             {
330                 final Object raw = StyleUtilities.queryStyle(getPredefinedStylesCollection(), OfficeToken.TABLE, tableStyle,
331                         "table-properties", OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR);
332                 if (raw == null || TRANSPARENT.equals(raw))
333                 {
334                     tableBackgroundColor = null;
335                 }
336                 else
337                 {
338                     tableBackgroundColor = String.valueOf(raw);
339                 }
340             }
341             return;
342         }
343 
344         if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMN, attrs) || ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMNS, attrs))
345         {
346             return;
347         }
348 
349         // covered-table-cell elements may appear in the input from row or column spans. In the event that we hit a
350         // column-span we simply ignore these elements because we are going to adjust the span to fit the uniform table.
351         if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.COVERED_TABLE_CELL, attrs))
352         {
353             if (columnSpanCounter > 0)
354             {
355                 columnSpanCounter--;
356             }
357 
358             if (columnSpanCounter == 0)
359             {
360                 // if we weren't expecting a covered-table-cell, let's use it, it's probably from a row-span
361                 columnCounter++;
362                 final int span = getColumnSpanForCell(tableCounter, columnCounter, 1);
363                 // use the calculated span for the column in the uniform table to create any additional covered-table-cell
364                 // elements
365                 for (int i = 0; i < span; i++)
366                 {
367                     xmlWriter.writeTag(namespace, OfficeToken.COVERED_TABLE_CELL, null, XmlWriter.CLOSE);
368                 }
369             }
370             return;
371         }
372 
373         if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_ROW, attrs))
374         {
375             // a new row means our column counter gets reset
376             columnCounter = 0;
377             // Lets make sure the color of the table is ok ..
378             if (tableBackgroundColor != null)
379             {
380                 final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
381                 final OfficeStyle style = deriveStyle(OfficeToken.TABLE_ROW, styleName);
382                 Element tableRowProperties = style.getTableRowProperties();
383                 if (tableRowProperties == null)
384                 {
385                     tableRowProperties = new Section();
386                     tableRowProperties.setNamespace(OfficeNamespaces.STYLE_NS);
387                     tableRowProperties.setType("table-row-properties");
388                     tableRowProperties.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, tableBackgroundColor);
389                     style.addNode(tableRowProperties);
390                 }
391                 else
392                 {
393                     final Object oldValue = tableRowProperties.getAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR);
394                     if (oldValue == null || TRANSPARENT.equals(oldValue))
395                     {
396                         tableRowProperties.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, tableBackgroundColor);
397                     }
398                 }
399                 attrs.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, style.getStyleName());
400             }
401         }
402         else if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_CELL, attrs))
403         {
404             columnCounter++;
405             final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
406             if (styleName != null)
407             {
408                 final OfficeStyle cellStyle = getPredefinedStylesCollection().getStyle(OfficeToken.TABLE_CELL, styleName);
409                 if (cellStyle != null)
410                 {
411                     final Section textProperties = (Section) cellStyle.getTextProperties();
412                     if (textProperties != null)
413                     {
414                         for (String i : FOPROPS)
415                         {
416                             textProperties.setAttribute(OfficeNamespaces.FO_NS, i, null);
417                         }
418                         textProperties.setAttribute(OfficeNamespaces.TEXT_NS, "display", null);
419                         for (String i : STYLEPROPS)
420                         {
421                             textProperties.setAttribute(OfficeNamespaces.STYLE_NS, i, null);
422                         }
423                     }
424                     final Section props = (Section) cellStyle.getTableCellProperties();
425                     if (props != null)
426                     {
427                         final Object raw = props.getAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR);
428                         if (TRANSPARENT.equals(raw))
429                         {
430                             props.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, null);
431                         }
432                     }
433                 }
434                 attrs.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, styleName);
435             }
436 
437             final String numColSpanStr = (String) attrs.getAttribute(namespace, NUMBERCOLUMNSSPANNED);
438             int initialColumnSpan = columnSpanCounter = 1;
439             if (numColSpanStr != null)
440             {
441                 initialColumnSpan = Integer.parseInt(numColSpanStr);
442                 columnSpanCounter = initialColumnSpan;
443             }
444             final int span = getColumnSpanForCell(tableCounter, columnCounter, initialColumnSpan);
445             if (initialColumnSpan > 1)
446             {
447                 // add the initial column span to our column counter index (subtract 1, since it is counted by default)
448                 columnCounter += initialColumnSpan - 1;
449             }
450 
451             // there's no point to create number-columns-spanned attributes if we only span 1 column
452             if (span > 1)
453             {
454                 attrs.setAttribute(namespace, NUMBERCOLUMNSSPANNED, "" + span);
455                 currentSpan = span;
456             }
457             // we must also generate "covered-table-cell" elements for each column spanned
458             // but we'll do this in the endElement, after we close this OfficeToken.TABLE_CELL
459         }
460 
461         // All styles have to be processed or you will lose the paragraph-styles and inline text-styles.
462         performStyleProcessing(attrs);
463 
464         final AttributeList attrList = buildAttributeList(attrs);
465         xmlWriter.writeTag(namespace, elementType, attrList, XmlWriter.OPEN);
466     }
467 
collectBoundaryForElement(final AttributeMap attrs)468     private void collectBoundaryForElement(final AttributeMap attrs)
469     {
470         if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMNS, attrs))
471         {
472             // A table row resets the column counter.
473             resetCurrentRowBoundaryMarker();
474         }
475         else if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMN, attrs))
476         {
477             final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
478             if (styleName == null)
479             {
480                 // This should not happen, but if it does, we will ignore that cell.
481                 return;
482             }
483 
484             final OfficeStyle style = getPredefinedStylesCollection().getStyle(OfficeToken.TABLE_COLUMN, styleName);
485             if (style == null)
486             {
487                 // Now this is very bad. It means that there is no style defined with the given name.
488                 return;
489             }
490 
491             final Element tableColumnProperties = style.getTableColumnProperties();
492             String widthStr = (String) tableColumnProperties.getAttribute("column-width");
493             widthStr = widthStr.substring(0, widthStr.indexOf(getUnitsOfMeasure(widthStr)));
494             final float val = Float.parseFloat(widthStr) * CELL_WIDTH_FACTOR;
495             addColumnWidthToRowBoundaryMarker((long) val);
496             ColumnBoundary currentRowBoundary = new ColumnBoundary(getCurrentRowBoundaryMarker());
497             final List<ColumnBoundary> columnBoundaryList_ = getColumnBoundaryList();
498             final int idx = columnBoundaryList_.indexOf(currentRowBoundary);
499             if (idx == -1)
500             {
501                 columnBoundaryList_.add(currentRowBoundary);
502             }
503             else
504             {
505                 currentRowBoundary = columnBoundaryList_.get(idx);
506             }
507             currentRowBoundary.addTableIndex(tableCounter);
508         }
509     }
510 
getUnitsOfMeasure(final String str)511     private String getUnitsOfMeasure(final String str)
512     {
513         if (unitsOfMeasure == null || "".equals(unitsOfMeasure))
514         {
515             if (str == null || "".equals(str))
516             {
517                 unitsOfMeasure = "cm";
518                 return unitsOfMeasure;
519             }
520 
521             // build units of measure, set it
522             int i = str.length() - 1;
523             for (; i >= 0; i--)
524             {
525                 final char c = str.charAt(i);
526                 if (Character.isDigit(c) || c == '.' || c == ',')
527                 {
528                     break;
529                 }
530             }
531             unitsOfMeasure = str.substring(i + 1);
532         }
533         return unitsOfMeasure;
534     }
535 
createTableShapes()536     private void createTableShapes() throws ReportProcessingException
537     {
538         if (!shapes.isEmpty())
539         {
540             try
541             {
542                 final XmlWriter xmlWriter = getXmlWriter();
543                 // at this point we need to generate the table-columns section based on our boundary table
544                 // <table:shapes>
545                 // <draw:frame />
546 
547                 // </table:shapes>
548                 xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.SHAPES, null, XmlWriterSupport.OPEN);
549 
550 
551                 for (int i = 0; i < shapes.size(); i++)
552                 {
553                     final AttributeMap attrs = shapes.get(i);
554                     final AttributeList attrList = buildAttributeList(attrs);
555                     attrList.removeAttribute(OfficeNamespaces.DRAWING_NS, OfficeToken.STYLE_NAME);
556                     xmlWriter.writeTag(OfficeNamespaces.DRAWING_NS, OfficeToken.FRAME, attrList, XmlWriterSupport.OPEN);
557                     startChartProcessing(ole.get(i));
558 
559                     xmlWriter.writeCloseTag();
560                 }
561                 xmlWriter.writeCloseTag();
562             }
563             catch (IOException e)
564             {
565                 throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
566             }
567         }
568     }
569 
createTableColumns()570     private void createTableColumns() throws ReportProcessingException
571     {
572         try
573         {
574             final XmlWriter xmlWriter = getXmlWriter();
575             // at this point we need to generate the table-columns section based on our boundary table
576             // <table-columns>
577             // <table-column style-name="coX"/>
578 
579             // </table-columns>
580             // the first boundary is '0' which is a placeholder so we will ignore it
581             xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMNS, null, XmlWriterSupport.OPEN);
582 
583             // blow away current column styles
584             // start processing at i=1 because we added a boundary for "0" which is virtual
585             final ColumnBoundary[] cba = getSortedColumnBoundaryArray();
586             for (int i = 1; i < cba.length; i++)
587             {
588                 final ColumnBoundary cb = cba[i];
589                 float columnWidth = cb.getBoundary();
590                 if (i > 1)
591                 {
592                     columnWidth -= cba[i - 1].getBoundary();
593                 }
594                 columnWidth = columnWidth / CELL_WIDTH_FACTOR;
595                 final OfficeStyle style = deriveStyle(OfficeToken.TABLE_COLUMN, ("co" + i + "_"));
596                 final Section tableColumnProperties = new Section();
597                 tableColumnProperties.setType("table-column-properties");
598                 tableColumnProperties.setNamespace(style.getNamespace());
599                 final String width = String.format("%f", columnWidth);
600                 tableColumnProperties.setAttribute(style.getNamespace(),
601                         "column-width", width + getUnitsOfMeasure(null));
602                 style.addNode(tableColumnProperties);
603 
604                 final AttributeList myAttrList = new AttributeList();
605                 myAttrList.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, style.getStyleName());
606                 xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMN, myAttrList, XmlWriterSupport.CLOSE);
607             }
608             xmlWriter.writeCloseTag();
609         }
610         catch (IOException e)
611         {
612             throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
613         }
614     }
615 
616     @Override
endOther(final AttributeMap attrs)617     protected void endOther(final AttributeMap attrs) throws DataSourceException, ReportProcessingException
618     {
619         if (ReportTargetUtil.isElementOfType(JFreeReportInfo.REPORT_NAMESPACE, OfficeToken.OBJECT_OLE, attrs) || oleHandled)
620         {
621             oleHandled = false;
622             return;
623         }
624 
625         if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_ROW, attrs) && isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
626         {
627             final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
628             rowHeights.add(computeRowHeight(styleName));
629         }
630 
631         if (isRepeatingSection() || isElementBoundaryCollectionPass())
632         {
633             return;
634         }
635 
636         final String namespace = ReportTargetUtil.getNamespaceFromAttribute(attrs);
637         if (isFilteredNamespace(namespace))
638         {
639             return;
640         }
641         final String elementType = ReportTargetUtil.getElemenTypeFromAttribute(attrs);
642         if (OfficeNamespaces.DRAWING_NS.equals(namespace) && OfficeToken.FRAME.equals(elementType))
643         {
644             return;
645         }
646 
647         // if this is the report namespace, write out a table definition ..
648         if (OfficeNamespaces.TABLE_NS.equals(namespace) && (OfficeToken.TABLE.equals(elementType) || OfficeToken.COVERED_TABLE_CELL.equals(elementType) || OfficeToken.TABLE_COLUMN.equals(elementType) || OfficeToken.TABLE_COLUMNS.equals(elementType)))
649         {
650             return;
651         }
652 
653         if (!paragraphHandled && OfficeNamespaces.TEXT_NS.equals(namespace) && OfficeToken.P.equals(elementType))
654         {
655             if (!paragraphHandled)
656             {
657                 return;
658             }
659 
660             paragraphHandled = false;
661         }
662         try
663         {
664             final XmlWriter xmlWriter = getXmlWriter();
665             xmlWriter.writeCloseTag();
666             // table-cell elements may have a number-columns-spanned attribute which indicates how many
667             // 'covered-table-cell' elements we need to generate
668             generateCoveredTableCells(attrs);
669         }
670         catch (IOException e)
671         {
672             throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
673         }
674     }
675 
generateCoveredTableCells(final AttributeMap attrs)676     private void generateCoveredTableCells(final AttributeMap attrs) throws IOException
677     {
678         if (!ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_CELL, attrs))
679         {
680             return;
681         }
682 
683         // do this after we close the tag
684         final XmlWriter xmlWriter = getXmlWriter();
685         final int span = currentSpan;
686         currentSpan = 0;
687         for (int i = 1; i < span; i++)
688         {
689             xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.COVERED_TABLE_CELL, null, XmlWriter.CLOSE);
690         }
691     }
692 
getExportDescriptor()693     public String getExportDescriptor()
694     {
695         return "raw/" + PentahoReportEngineMetaData.OPENDOCUMENT_SPREADSHEET;
696     }
697 
698 
699     @Override
processText(final String text)700     public void processText(final String text) throws DataSourceException, ReportProcessingException
701     {
702         if (!(isRepeatingSection() || isElementBoundaryCollectionPass()))
703         {
704             handleParagraph();
705             super.processText(text);
706         }
707     }
708 
709     @Override
processContent(final DataFlags value)710     public void processContent(final DataFlags value) throws DataSourceException, ReportProcessingException
711     {
712         if (!(isRepeatingSection() || isElementBoundaryCollectionPass()))
713         {
714             handleParagraph();
715             super.processContent(value);
716         }
717     }
718 
getStartContent()719     private String getStartContent()
720     {
721         return "spreadsheet";
722     }
723 
724     @Override
startContent(final AttributeMap attrs)725     protected void startContent(final AttributeMap attrs) throws IOException, DataSourceException,
726             ReportProcessingException
727     {
728         if (!isElementBoundaryCollectionPass())
729         {
730             final XmlWriter xmlWriter = getXmlWriter();
731             xmlWriter.writeTag(OfficeNamespaces.OFFICE_NS, getStartContent(), null, XmlWriterSupport.OPEN);
732 
733             writeNullDate();
734 
735             final AttributeMap tableAttributes = new AttributeMap();
736             tableAttributes.setAttribute(JFreeReportInfo.REPORT_NAMESPACE, Element.NAMESPACE_ATTRIBUTE, OfficeNamespaces.TABLE_NS);
737             tableAttributes.setAttribute(JFreeReportInfo.REPORT_NAMESPACE, Element.TYPE_ATTRIBUTE, OfficeToken.TABLE);
738             tableAttributes.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, generateInitialTableStyle());
739             tableAttributes.setAttribute(OfficeNamespaces.TABLE_NS, "name", "Report");
740 
741             performStyleProcessing(tableAttributes);
742 
743             xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE, buildAttributeList(tableAttributes), XmlWriterSupport.OPEN);
744             createTableShapes();
745             createTableColumns();
746         }
747     }
748 
generateInitialTableStyle()749     private String generateInitialTableStyle() throws ReportProcessingException
750     {
751         final OfficeStylesCollection predefStyles = getPredefinedStylesCollection();
752         final OfficeStyles commonStyles = predefStyles.getAutomaticStyles();
753         if (!commonStyles.containsStyle(OfficeToken.TABLE, "Initial_Table"))
754         {
755             final String masterPageName = createMasterPage();
756 
757             final OfficeStyle tableStyle = new OfficeStyle();
758             tableStyle.setStyleFamily(OfficeToken.TABLE);
759             tableStyle.setStyleName("Initial_Table");
760             tableStyle.setAttribute(OfficeNamespaces.STYLE_NS, "master-page-name", masterPageName);
761             final Element tableProperties = produceFirstChild(tableStyle, OfficeNamespaces.STYLE_NS, "table-properties");
762             tableProperties.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, TRANSPARENT);
763             commonStyles.addStyle(tableStyle);
764         }
765         return "Initial_Table";
766     }
767 
createMasterPage()768     private String createMasterPage() throws ReportProcessingException
769     {
770         final OfficeStylesCollection predefStyles = getPredefinedStylesCollection();
771         final MasterPageFactory masterPageFactory = new MasterPageFactory(predefStyles.getMasterStyles());
772         final OfficeMasterPage masterPage;
773         if (!masterPageFactory.containsMasterPage("Standard", null, null))
774         {
775             masterPage = masterPageFactory.createMasterPage("Standard", null, null);
776 
777             final CSSNumericValue zeroLength = CSSNumericValue.createValue(CSSNumericType.CM, 0);
778             final String pageLayoutTemplate = masterPage.getPageLayout();
779             if (pageLayoutTemplate == null)
780             {
781                 // there is no pagelayout. Create one ..
782                 final String derivedLayout = masterPageFactory.createPageStyle(getGlobalStylesCollection().getAutomaticStyles(), zeroLength, zeroLength);
783                 masterPage.setPageLayout(derivedLayout);
784             }
785             else
786             {
787                 final String derivedLayout = masterPageFactory.derivePageStyle(pageLayoutTemplate,
788                         getPredefinedStylesCollection().getAutomaticStyles(),
789                         getGlobalStylesCollection().getAutomaticStyles(), zeroLength, zeroLength);
790                 masterPage.setPageLayout(derivedLayout);
791             }
792 
793             final OfficeStylesCollection officeStylesCollection = getGlobalStylesCollection();
794             final OfficeMasterStyles officeMasterStyles = officeStylesCollection.getMasterStyles();
795             officeMasterStyles.addMasterPage(masterPage);
796         }
797         else
798         {
799             masterPage = masterPageFactory.getMasterPage("Standard", null, null);
800         }
801         return masterPage.getStyleName();
802     }
803 
804     @Override
endContent(final AttributeMap attrs)805     protected void endContent(final AttributeMap attrs) throws IOException, DataSourceException,
806             ReportProcessingException
807     {
808         // todo
809         if (!isElementBoundaryCollectionPass())
810         {
811             final XmlWriter xmlWriter = getXmlWriter();
812             xmlWriter.writeCloseTag();
813             xmlWriter.writeCloseTag();
814         }
815     }
816 
817     @Override
endReport(final ReportStructureRoot report)818     public void endReport(final ReportStructureRoot report) throws DataSourceException, ReportProcessingException
819     {
820         super.endReport(report);
821         setElementBoundaryCollectionPass(false);
822         resetTableCounter();
823         columnCounter = 0;
824         copyMeta();
825     }
826 
isElementBoundaryCollectionPass()827     private boolean isElementBoundaryCollectionPass()
828     {
829         return elementBoundaryCollectionPass;
830     }
831 
setElementBoundaryCollectionPass(final boolean elementBoundaryCollectionPass)832     private void setElementBoundaryCollectionPass(final boolean elementBoundaryCollectionPass)
833     {
834         this.elementBoundaryCollectionPass = elementBoundaryCollectionPass;
835     }
836 
getSortedColumnBoundaryArray()837     private ColumnBoundary[] getSortedColumnBoundaryArray()
838     {
839         if (sortedBoundaryArray == null)
840         {
841             getColumnBoundaryList().add(new ColumnBoundary(0));
842             sortedBoundaryArray = getColumnBoundaryList().toArray(new ColumnBoundary[getColumnBoundaryList().size()]);
843             Arrays.sort(sortedBoundaryArray);
844         }
845         return sortedBoundaryArray;
846     }
847 
getColumnBoundaryList()848     private List<ColumnBoundary> getColumnBoundaryList()
849     {
850         return columnBoundaryList;
851     }
852 
addColumnWidthToRowBoundaryMarker(final long width)853     private void addColumnWidthToRowBoundaryMarker(final long width)
854     {
855         currentRowBoundaryMarker += width;
856     }
857 
getCurrentRowBoundaryMarker()858     private long getCurrentRowBoundaryMarker()
859     {
860         return currentRowBoundaryMarker;
861     }
862 
resetTableCounter()863     private void resetTableCounter()
864     {
865         tableCounter = 0;
866     }
867 
resetCurrentRowBoundaryMarker()868     private void resetCurrentRowBoundaryMarker()
869     {
870         currentRowBoundaryMarker = 0;
871     }
872 
getBoundariesForTable(final int table)873     private ColumnBoundary[] getBoundariesForTable(final int table)
874     {
875         if (boundariesForTableArray == null)
876         {
877             final List<ColumnBoundary> boundariesForTable = new ArrayList<ColumnBoundary>();
878             final List<ColumnBoundary> boundaryList = getColumnBoundaryList();
879             for (int i = 0; i < boundaryList.size(); i++)
880             {
881                 final ColumnBoundary b = boundaryList.get(i);
882                 if (b.isContainedByTable(table))
883                 {
884                     boundariesForTable.add(b);
885                 }
886             }
887             boundariesForTableArray = boundariesForTable.toArray(new ColumnBoundary[boundariesForTable.size()]);
888             Arrays.sort(boundariesForTableArray);
889         }
890         return boundariesForTableArray;
891     }
892 
getColumnSpanForCell(final int table, final int col, final int initialColumnSpan)893     private int getColumnSpanForCell(final int table, final int col, final int initialColumnSpan)
894     {
895         final ColumnBoundary[] globalBoundaries = getSortedColumnBoundaryArray();
896         final ColumnBoundary[] tableBoundaries = getBoundariesForTable(table);
897         // how many column boundaries in the globalBoundaries list fall between the currentRowWidth and the next boundary
898         // for the current row
899 
900         float cellBoundary = tableBoundaries[col - 1].getBoundary();
901         float cellWidth = tableBoundaries[col - 1].getBoundary();
902 
903         if (col > 1)
904         {
905             cellWidth = cellWidth - tableBoundaries[col - 2].getBoundary();
906         }
907 
908         if (initialColumnSpan > 1)
909         {
910             // ok we've got some additional spanning specified on the input
911             final int index = (col - 1) + (initialColumnSpan - 1);
912             cellWidth += tableBoundaries[index].getBoundary() - tableBoundaries[col - 1].getBoundary();
913             cellBoundary = tableBoundaries[index].getBoundary();
914         }
915 
916         int beginBoundaryIndex = 0;
917         int endBoundaryIndex = globalBoundaries.length - 1;
918         for (int i = 0; i < globalBoundaries.length; i++)
919         {
920             // find beginning boundary
921             if (globalBoundaries[i].getBoundary() <= cellBoundary - cellWidth)
922             {
923                 beginBoundaryIndex = i;
924             }
925             if (globalBoundaries[i].getBoundary() <= cellBoundary)
926             {
927                 endBoundaryIndex = i;
928             }
929         }
930         final int span = endBoundaryIndex - beginBoundaryIndex;
931         // span will be zero for the first column, so we adjust it to 1
932         if (span == 0)
933         {
934             return 1;
935         }
936         return span;
937     }
938 
939     @Override
getTargetMimeType()940     protected String getTargetMimeType()
941     {
942         return "application/vnd.oasis.opendocument.spreadsheet";
943     }
944 }
945