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.metis.data.MetisDataDelta;
20  import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
21  import io.github.tonywasher.joceanus.metis.exc.MetisIOException;
22  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
23  import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
24  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
25  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
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      * Blank link indication.
140      */
141     private static final String BLANK_LINK = "<Blank>";
142 
143     /**
144      * The data formatter.
145      */
146     private final OceanusDataFormatter theFormatter;
147 
148     /**
149      * Transformer.
150      */
151     private final Transformer theXformer;
152 
153     /**
154      * The document.
155      */
156     private final Document theDocument;
157 
158     /**
159      * The document body.
160      */
161     private final Element theBody;
162 
163     /**
164      * The page.
165      */
166     private MetisViewerPage thePage;
167 
168     /**
169      * The table Header row.
170      */
171     private Element theTblHdr;
172 
173     /**
174      * The table body.
175      */
176     private Element theTblBody;
177 
178     /**
179      * The current table row.
180      */
181     private Element theTblRow;
182 
183     /**
184      * The number of rows.
185      */
186     private int theNumRows;
187 
188     /**
189      * Constructor.
190      *
191      * @param pFormatter the data formatter
192      * @throws OceanusException on error
193      */
194     protected MetisViewerBuilder(final OceanusDataFormatter pFormatter) throws OceanusException {
195         /* Protect against exceptions */
196         try {
197             /* Store parameters */
198             theFormatter = pFormatter;
199 
200             /* Create the document builder */
201             final DocumentBuilderFactory myDocFactory = DocumentBuilderFactory.newInstance();
202             myDocFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
203             myDocFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
204             myDocFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
205 
206             /* Create the document builder */
207             final DocumentBuilder myBuilder = myDocFactory.newDocumentBuilder();
208 
209             /* Create the transformer */
210             final TransformerFactory myXformFactory = TransformerFactory.newInstance();
211             myXformFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
212             myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
213             myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
214             theXformer = myXformFactory.newTransformer();
215 
216             /* Create the document */
217             theDocument = myBuilder.newDocument();
218 
219             /* Create the standard structure */
220             final Element myHtml = theDocument.createElement(ELEMENT_HTML);
221             theDocument.appendChild(myHtml);
222             theBody = theDocument.createElement(ELEMENT_BODY);
223             myHtml.appendChild(theBody);
224 
225         } catch (Exception e) {
226             throw new MetisIOException("Failed to create", e);
227         }
228     }
229 
230     /**
231      * Format document.
232      */
233     protected void formatDocument() {
234         /* protect against exceptions */
235         try {
236             /* Format the XML and write to stream */
237             final StringWriter myWriter = new StringWriter();
238             theXformer.transform(new DOMSource(theDocument), new StreamResult(myWriter));
239 
240             /* Convert result to string */
241             thePage.setHtml(myWriter.toString());
242 
243         } catch (TransformerException e) {
244             LOGGER.error("Failed to format document", e);
245         }
246     }
247 
248     /**
249      * Clear an element.
250      *
251      * @param pElement the element to clear
252      */
253     private static void clearElement(final Element pElement) {
254         /* Clear the existing elements */
255         while (pElement.hasChildNodes()) {
256             pElement.removeChild(pElement.getFirstChild());
257         }
258     }
259 
260     /**
261      * Reset the document.
262      *
263      * @param pPage the associated page
264      */
265     protected void resetDocument(final MetisViewerPage pPage) {
266         /* Reset the page */
267         thePage = pPage;
268         thePage.resetPage();
269 
270         /* Reset the document */
271         clearElement(theBody);
272     }
273 
274     /**
275      * Make title.
276      *
277      * @param pTitle the title
278      */
279     protected void newTitle(final String pTitle) {
280         /* Create the title */
281         final Element myTitle = theDocument.createElement(ELEMENT_TITLE);
282         theBody.appendChild(myTitle);
283         myTitle.setTextContent(pTitle);
284     }
285 
286     /**
287      * Make new table.
288      */
289     protected void newTable() {
290         /* Create the table */
291         final Element myTable = theDocument.createElement(ELEMENT_TABLE);
292         myTable.setAttribute(ATTR_CLASS, CLASS_VIEWER);
293         theBody.appendChild(myTable);
294         final Element myHead = theDocument.createElement(ELEMENT_THEAD);
295         myTable.appendChild(myHead);
296         theTblHdr = theDocument.createElement(ELEMENT_TROW);
297         myHead.appendChild(theTblHdr);
298         theTblBody = theDocument.createElement(ELEMENT_TBODY);
299         myTable.appendChild(theTblBody);
300 
301         /* Set counters */
302         theTblRow = null;
303         theNumRows = 0;
304     }
305 
306     /**
307      * Make new title cell.
308      *
309      * @param pTitle the title
310      */
311     protected void newTitleCell(final String pTitle) {
312         /* Create the title cell */
313         final Element myCell = theDocument.createElement(ELEMENT_THDR);
314         theTblHdr.appendChild(myCell);
315         myCell.setTextContent(pTitle);
316     }
317 
318     /**
319      * Make new table row.
320      */
321     protected void newTableRow() {
322         /* Create the table row */
323         theTblRow = theDocument.createElement(ELEMENT_TROW);
324         theTblBody.appendChild(theTblRow);
325 
326         /* Set correct class */
327         theTblRow.setAttribute(ATTR_CLASS, (theNumRows % 2 == 0)
328                 ? CLASS_EVENROW
329                 : CLASS_ODDROW);
330         theNumRows++;
331     }
332 
333     /**
334      * Make new data cell.
335      *
336      * @param pData the data
337      */
338     protected void newDataCell(final Object pData) {
339         newDataCell(pData, false);
340     }
341 
342     /**
343      * Make new data cell.
344      *
345      * @param pData    the data
346      * @param pChanged is this a changed field true/false
347      */
348     protected void newDataCell(final Object pData,
349                                final boolean pChanged) {
350         /* Create the data cell */
351         final Element myCell = theDocument.createElement(ELEMENT_TCELL);
352         theTblRow.appendChild(myCell);
353 
354         /* Determine the text */
355         String myText = formatValue(pData);
356 
357         /* If the Object is a data delta */
358         if (pData instanceof MetisDataDelta myDelta) {
359             /* Access the difference */
360             final MetisDataDifference myDiff = myDelta.getDifference();
361 
362             /* If there is a difference */
363             if (!myDiff.isIdentical()) {
364                 myCell.setAttribute(ATTR_CLASS, myDiff.isValueChanged()
365                         ? CLASS_CHANGED
366                         : CLASS_SECCHANGED);
367             }
368         } else if (pChanged) {
369             myCell.setAttribute(ATTR_CLASS, CLASS_CHANGED);
370         }
371 
372         /* If the object is link-able */
373         if (MetisViewerPage.isLinkable(pData)) {
374             /* Create the link */
375             final Element myLink = theDocument.createElement(ELEMENT_LINK);
376             myCell.appendChild(myLink);
377             myLink.setAttribute(ATTR_HREF, thePage.newLink(pData));
378             myText = myText.isEmpty() ? BLANK_LINK : myText;
379             myLink.setTextContent(myText);
380 
381             /* else just record the formatted text */
382         } else {
383             myCell.setTextContent(myText);
384         }
385     }
386 
387     /**
388      * Format a value.
389      *
390      * @param pValue the value to format
391      * @return the formatted value
392      */
393     private String formatValue(final Object pValue) {
394         /* Format the value */
395         String myFormat = theFormatter.formatObject(pValue);
396 
397         /* Perform special formatting for a long byte[] */
398         if (needsWrapping(pValue)
399                 && (myFormat.length() > WRAP_HEXSTRING)) {
400             final StringBuilder myBuffer = new StringBuilder(myFormat.length() << 1);
401 
402             /* Format the buffer */
403             myBuffer.append(myFormat);
404 
405             /* Insert new lines */
406             int iCount = myFormat.length()
407                     / WRAP_HEXSTRING;
408             while (iCount > 0) {
409                 myBuffer.insert(WRAP_HEXSTRING
410                         * iCount--, '\n');
411             }
412 
413             /* Obtain new format */
414             myFormat = myBuffer.toString();
415         }
416 
417         /* Return the formatted value */
418         return myFormat;
419     }
420 
421     /**
422      * does the object format need wrapping?
423      *
424      * @param pObject the object
425      * @return true/false
426      */
427     private static boolean needsWrapping(final Object pObject) {
428         /* Determine whether we need wrapping */
429         Object myObject = pObject;
430         if (myObject instanceof MetisDataDelta) {
431             myObject = ((MetisDataDelta) pObject).getObject();
432         }
433         return myObject instanceof byte[];
434     }
435 }