View Javadoc
1   /*
2    * Metis: Java Data Framework
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.metis.viewer;
18  
19  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
20  import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
21  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
22  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
23  import io.github.tonywasher.joceanus.metis.data.MetisDataDelta;
24  import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
25  import io.github.tonywasher.joceanus.metis.exc.MetisIOException;
26  import org.w3c.dom.Document;
27  import org.w3c.dom.Element;
28  
29  import javax.xml.XMLConstants;
30  import javax.xml.parsers.DocumentBuilder;
31  import javax.xml.parsers.DocumentBuilderFactory;
32  import javax.xml.transform.Transformer;
33  import javax.xml.transform.TransformerException;
34  import javax.xml.transform.TransformerFactory;
35  import javax.xml.transform.dom.DOMSource;
36  import javax.xml.transform.stream.StreamResult;
37  import java.io.StringWriter;
38  
39  /**
40   * Data Viewer Builder.
41   */
42  public class MetisViewerBuilder {
43      /**
44       * Logger.
45       */
46      private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(MetisViewerBuilder.class);
47  
48      /**
49       * Wrap for hex string.
50       */
51      private static final int WRAP_HEXSTRING = 60;
52  
53      /**
54       * The HTML element.
55       */
56      private static final String ELEMENT_HTML = "html";
57  
58      /**
59       * The body element.
60       */
61      private static final String ELEMENT_BODY = "body";
62  
63      /**
64       * The title element.
65       */
66      private static final String ELEMENT_TITLE = "h2";
67  
68      /**
69       * The link element.
70       */
71      private static final String ELEMENT_LINK = "a";
72  
73      /**
74       * The table element.
75       */
76      private static final String ELEMENT_TABLE = "table";
77  
78      /**
79       * The tableHead element.
80       */
81      private static final String ELEMENT_THEAD = "thead";
82  
83      /**
84       * The tableBody element.
85       */
86      private static final String ELEMENT_TBODY = "tbody";
87  
88      /**
89       * The tableRow element.
90       */
91      private static final String ELEMENT_TROW = "tr";
92  
93      /**
94       * The tableHdr element.
95       */
96      private static final String ELEMENT_THDR = "th";
97  
98      /**
99       * The tableCell element.
100      */
101     private static final String ELEMENT_TCELL = "td";
102 
103     /**
104      * The class attribute.
105      */
106     private static final String ATTR_CLASS = "class";
107 
108     /**
109      * The hRef attribute.
110      */
111     private static final String ATTR_HREF = "href";
112 
113     /**
114      * Name of table class.
115      */
116     private static final String CLASS_VIEWER = "-metis-viewer";
117 
118     /**
119      * Name of odd table row class.
120      */
121     private static final String CLASS_ODDROW = "-metis-oddrow";
122 
123     /**
124      * Name of even table row class.
125      */
126     private static final String CLASS_EVENROW = "-metis-evenrow";
127 
128     /**
129      * Name of changed cell class.
130      */
131     private static final String CLASS_CHANGED = "-metis-changed";
132 
133     /**
134      * Name of security changed cell class.
135      */
136     private static final String CLASS_SECCHANGED = "-metis-security";
137 
138     /**
139      * The data formatter.
140      */
141     private final OceanusDataFormatter theFormatter;
142 
143     /**
144      * Transformer.
145      */
146     private final Transformer theXformer;
147 
148     /**
149      * The document.
150      */
151     private final Document theDocument;
152 
153     /**
154      * The document body.
155      */
156     private final Element theBody;
157 
158     /**
159      * The page.
160      */
161     private MetisViewerPage thePage;
162 
163     /**
164      * The table Header row.
165      */
166     private Element theTblHdr;
167 
168     /**
169      * The table body.
170      */
171     private Element theTblBody;
172 
173     /**
174      * The current table row.
175      */
176     private Element theTblRow;
177 
178     /**
179      * The number of rows.
180      */
181     private int theNumRows;
182 
183     /**
184      * Constructor.
185      *
186      * @param pFormatter the data formatter
187      * @throws OceanusException on error
188      */
189     protected MetisViewerBuilder(final OceanusDataFormatter pFormatter) throws OceanusException {
190         /* Protect against exceptions */
191         try {
192             /* Store parameters */
193             theFormatter = pFormatter;
194 
195             /* Create the document builder */
196             final DocumentBuilderFactory myDocFactory = DocumentBuilderFactory.newInstance();
197             myDocFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
198             myDocFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
199             myDocFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
200 
201             /* Create the document builder */
202             final DocumentBuilder myBuilder = myDocFactory.newDocumentBuilder();
203 
204             /* Create the transformer */
205             final TransformerFactory myXformFactory = TransformerFactory.newInstance();
206             myXformFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
207             myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
208             myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
209             theXformer = myXformFactory.newTransformer();
210 
211             /* Create the document */
212             theDocument = myBuilder.newDocument();
213 
214             /* Create the standard structure */
215             final Element myHtml = theDocument.createElement(ELEMENT_HTML);
216             theDocument.appendChild(myHtml);
217             theBody = theDocument.createElement(ELEMENT_BODY);
218             myHtml.appendChild(theBody);
219 
220         } catch (Exception e) {
221             throw new MetisIOException("Failed to create", e);
222         }
223     }
224 
225     /**
226      * Format document.
227      */
228     protected void formatDocument() {
229         /* protect against exceptions */
230         try {
231             /* Format the XML and write to stream */
232             final StringWriter myWriter = new StringWriter();
233             theXformer.transform(new DOMSource(theDocument), new StreamResult(myWriter));
234 
235             /* Convert result to string */
236             thePage.setHtml(myWriter.toString());
237 
238         } catch (TransformerException e) {
239             LOGGER.error("Failed to format document", e);
240         }
241     }
242 
243     /**
244      * Clear an element.
245      *
246      * @param pElement the element to clear
247      */
248     private static void clearElement(final Element pElement) {
249         /* Clear the existing elements */
250         while (pElement.hasChildNodes()) {
251             pElement.removeChild(pElement.getFirstChild());
252         }
253     }
254 
255     /**
256      * Reset the document.
257      *
258      * @param pPage the associated page
259      */
260     protected void resetDocument(final MetisViewerPage pPage) {
261         /* Reset the page */
262         thePage = pPage;
263         thePage.resetPage();
264 
265         /* Reset the document */
266         clearElement(theBody);
267     }
268 
269     /**
270      * Make title.
271      *
272      * @param pTitle the title
273      */
274     protected void newTitle(final String pTitle) {
275         /* Create the title */
276         final Element myTitle = theDocument.createElement(ELEMENT_TITLE);
277         theBody.appendChild(myTitle);
278         myTitle.setTextContent(pTitle);
279     }
280 
281     /**
282      * Make new table.
283      */
284     protected void newTable() {
285         /* Create the table */
286         final Element myTable = theDocument.createElement(ELEMENT_TABLE);
287         myTable.setAttribute(ATTR_CLASS, CLASS_VIEWER);
288         theBody.appendChild(myTable);
289         final Element myHead = theDocument.createElement(ELEMENT_THEAD);
290         myTable.appendChild(myHead);
291         theTblHdr = theDocument.createElement(ELEMENT_TROW);
292         myHead.appendChild(theTblHdr);
293         theTblBody = theDocument.createElement(ELEMENT_TBODY);
294         myTable.appendChild(theTblBody);
295 
296         /* Set counters */
297         theTblRow = null;
298         theNumRows = 0;
299     }
300 
301     /**
302      * Make new title cell.
303      *
304      * @param pTitle the title
305      */
306     protected void newTitleCell(final String pTitle) {
307         /* Create the title cell */
308         final Element myCell = theDocument.createElement(ELEMENT_THDR);
309         theTblHdr.appendChild(myCell);
310         myCell.setTextContent(pTitle);
311     }
312 
313     /**
314      * Make new table row.
315      */
316     protected void newTableRow() {
317         /* Create the table row */
318         theTblRow = theDocument.createElement(ELEMENT_TROW);
319         theTblBody.appendChild(theTblRow);
320 
321         /* Set correct class */
322         theTblRow.setAttribute(ATTR_CLASS, (theNumRows % 2 == 0)
323                 ? CLASS_EVENROW
324                 : CLASS_ODDROW);
325         theNumRows++;
326     }
327 
328     /**
329      * Make new data cell.
330      *
331      * @param pData the data
332      */
333     protected void newDataCell(final Object pData) {
334         newDataCell(pData, false);
335     }
336 
337     /**
338      * Make new data cell.
339      *
340      * @param pData    the data
341      * @param pChanged is this a changed field true/false
342      */
343     protected void newDataCell(final Object pData,
344                                final boolean pChanged) {
345         /* Create the data cell */
346         final Element myCell = theDocument.createElement(ELEMENT_TCELL);
347         theTblRow.appendChild(myCell);
348 
349         /* Determine the text */
350         final String myText = formatValue(pData);
351 
352         /* If the Object is a data delta */
353         if (pData instanceof MetisDataDelta myDelta) {
354             /* Access the difference */
355             final MetisDataDifference myDiff = myDelta.getDifference();
356 
357             /* If there is a difference */
358             if (!myDiff.isIdentical()) {
359                 myCell.setAttribute(ATTR_CLASS, myDiff.isValueChanged()
360                         ? CLASS_CHANGED
361                         : CLASS_SECCHANGED);
362             }
363         } else if (pChanged) {
364             myCell.setAttribute(ATTR_CLASS, CLASS_CHANGED);
365         }
366 
367         /* If the object is link-able */
368         if (MetisViewerPage.isLinkable(pData)) {
369             /* Create the link */
370             final Element myLink = theDocument.createElement(ELEMENT_LINK);
371             myCell.appendChild(myLink);
372             myLink.setAttribute(ATTR_HREF, thePage.newLink(pData));
373             myLink.setTextContent(myText);
374 
375             /* else just record the formatted text */
376         } else {
377             myCell.setTextContent(myText);
378         }
379     }
380 
381     /**
382      * Format a value.
383      *
384      * @param pValue the value to format
385      * @return the formatted value
386      */
387     private String formatValue(final Object pValue) {
388         /* Format the value */
389         String myFormat = theFormatter.formatObject(pValue);
390 
391         /* Perform special formatting for a long byte[] */
392         if (needsWrapping(pValue)
393                 && (myFormat.length() > WRAP_HEXSTRING)) {
394             final StringBuilder myBuffer = new StringBuilder(myFormat.length() << 1);
395 
396             /* Format the buffer */
397             myBuffer.append(myFormat);
398 
399             /* Insert new lines */
400             int iCount = myFormat.length()
401                     / WRAP_HEXSTRING;
402             while (iCount > 0) {
403                 myBuffer.insert(WRAP_HEXSTRING
404                         * iCount--, '\n');
405             }
406 
407             /* Obtain new format */
408             myFormat = myBuffer.toString();
409         }
410 
411         /* Return the formatted value */
412         return myFormat;
413     }
414 
415     /**
416      * does the object format need wrapping?
417      *
418      * @param pObject the object
419      * @return true/false
420      */
421     private static boolean needsWrapping(final Object pObject) {
422         /* Determine whether we need wrapping */
423         Object myObject = pObject;
424         if (myObject instanceof MetisDataDelta) {
425             myObject = ((MetisDataDelta) pObject).getObject();
426         }
427         return myObject instanceof byte[];
428     }
429 }