View Javadoc
1   /*
2    * Prometheus: Application 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.prometheus.data;
18  
19  import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
20  import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory.GordianFactoryLock;
21  import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipFactory;
22  import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipFileContents;
23  import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipFileEntry;
24  import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipLock;
25  import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipReadFile;
26  import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipWriteFile;
27  import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
28  import io.github.tonywasher.joceanus.metis.field.MetisFieldItem.MetisFieldSetDef;
29  import io.github.tonywasher.joceanus.metis.list.MetisListKey;
30  import io.github.tonywasher.joceanus.metis.toolkit.MetisToolkit;
31  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
32  import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
33  import io.github.tonywasher.joceanus.oceanus.profile.OceanusProfile;
34  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataValues.PrometheusGroupedItem;
35  import io.github.tonywasher.joceanus.prometheus.exc.PrometheusDataException;
36  import io.github.tonywasher.joceanus.prometheus.exc.PrometheusIOException;
37  import io.github.tonywasher.joceanus.prometheus.exc.PrometheusSecurityException;
38  import io.github.tonywasher.joceanus.prometheus.security.PrometheusSecurityPasswordManager;
39  import io.github.tonywasher.joceanus.tethys.api.thread.TethysUIThreadStatusReport;
40  import org.w3c.dom.Document;
41  import org.w3c.dom.Element;
42  import org.w3c.dom.Node;
43  import org.xml.sax.SAXException;
44  
45  import javax.xml.XMLConstants;
46  import javax.xml.parsers.DocumentBuilder;
47  import javax.xml.parsers.DocumentBuilderFactory;
48  import javax.xml.parsers.ParserConfigurationException;
49  import javax.xml.transform.Transformer;
50  import javax.xml.transform.TransformerConfigurationException;
51  import javax.xml.transform.TransformerException;
52  import javax.xml.transform.TransformerFactory;
53  import javax.xml.transform.dom.DOMSource;
54  import javax.xml.transform.stream.StreamResult;
55  import java.io.File;
56  import java.io.FileInputStream;
57  import java.io.FileOutputStream;
58  import java.io.IOException;
59  import java.io.InputStream;
60  import java.io.OutputStream;
61  import java.util.Iterator;
62  
63  /**
64   * Formatter/Parser class for DataValues.
65   */
66  public class PrometheusDataValuesFormatter {
67      /**
68       * Entry suffix.
69       */
70      private static final String SUFFIX_ENTRY = ".xml";
71  
72      /**
73       * Error text.
74       */
75      private static final String ERROR_BACKUP = "Failed to create backup XML";
76  
77      /**
78       * The report.
79       */
80      private final TethysUIThreadStatusReport theReport;
81  
82      /**
83       * The password manager.
84       */
85      private final PrometheusSecurityPasswordManager thePasswordMgr;
86  
87      /**
88       * The document builder.
89       */
90      private final DocumentBuilder theBuilder;
91  
92      /**
93       * The transformer.
94       */
95      private final Transformer theXformer;
96  
97      /**
98       * The Data version.
99       */
100     private Integer theVersion;
101 
102     /**
103      * Constructor.
104      *
105      * @param pReport      the report
106      * @param pPasswordMgr the password manager
107      * @throws PrometheusIOException on error
108      */
109     public PrometheusDataValuesFormatter(final TethysUIThreadStatusReport pReport,
110                                          final PrometheusSecurityPasswordManager pPasswordMgr) throws PrometheusIOException {
111         /* Store values */
112         theReport = pReport;
113         thePasswordMgr = pPasswordMgr;
114 
115         /* protect against exceptions */
116         try {
117             /* Create a Document builder */
118             final DocumentBuilderFactory myFactory = DocumentBuilderFactory.newInstance();
119             myFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
120             myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
121             myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
122             theBuilder = myFactory.newDocumentBuilder();
123 
124             /* Create the transformer */
125             final TransformerFactory myXformFactory = TransformerFactory.newInstance();
126             myXformFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
127             myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
128             myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
129             theXformer = myXformFactory.newTransformer();
130 
131         } catch (ParserConfigurationException | TransformerConfigurationException e) {
132             throw new PrometheusIOException("Failed to initialise parser", e);
133         }
134     }
135 
136     /**
137      * Create a Backup ZipFile.
138      *
139      * @param pData Data to write out
140      * @param pFile the backup file to write to
141      * @throws OceanusException on error
142      */
143     public void createBackup(final PrometheusDataSet pData,
144                              final File pFile) throws OceanusException {
145         boolean writeFailed = false;
146         try {
147             createBackup(pData, new FileOutputStream(pFile));
148         } catch (IOException
149                  | OceanusException e) {
150             writeFailed = true;
151             throw new PrometheusIOException(ERROR_BACKUP, e);
152         } finally {
153             /* Try to delete the file if required */
154             if (writeFailed) {
155                 MetisToolkit.cleanUpFile(pFile);
156             }
157         }
158     }
159 
160     /**
161      * Create a Backup ZipFile.
162      *
163      * @param pData      Data to write out
164      * @param pZipStream the output stream
165      * @throws OceanusException on error
166      */
167     public void createBackup(final PrometheusDataSet pData,
168                              final OutputStream pZipStream) throws OceanusException {
169         /* Obtain the active profile */
170         final OceanusProfile myTask = theReport.getActiveTask();
171         final OceanusProfile myStage = myTask.startTask("Writing");
172 
173         /* Protect agains exceptions */
174         try {
175             /* Create a similar security control */
176             final PrometheusSecurityPasswordManager myPasswordMgr = pData.getPasswordMgr();
177             final GordianFactoryLock myBase = pData.getFactoryLock();
178             final GordianFactoryLock myLock = myPasswordMgr.similarFactoryLock(myBase);
179             final GordianZipFactory myZips = myPasswordMgr.getSecurityFactory().getZipFactory();
180             final GordianZipLock myZipLock = myZips.zipLock(myLock);
181 
182             /* Access the data version */
183             theVersion = pData.getControl().getDataVersion();
184 
185             /* Declare the number of stages */
186             theReport.setNumStages(pData.getListMap().size());
187 
188             /* Protect the workbook access */
189             try (GordianZipWriteFile myZipFile = myZips.createZipFile(myZipLock, pZipStream)) {
190                 /* Loop through the data lists */
191                 final Iterator<PrometheusDataList<?>> myIterator = pData.iterator();
192                 while (myIterator.hasNext()) {
193                     final PrometheusDataList<?> myList = myIterator.next();
194 
195                     /* Declare the new stage */
196                     theReport.setNewStage(myList.listName());
197 
198                     /* If this list should be written */
199                     if (myList.includeDataXML()) {
200                         /* Write the list details */
201                         myStage.startTask(myList.listName());
202                         writeXMLListToFile(myList, myZipFile, true);
203                     }
204                 }
205 
206                 /* Complete the task */
207                 myStage.end();
208 
209             } catch (IOException
210                      | OceanusException e) {
211                 throw new PrometheusIOException(ERROR_BACKUP, e);
212             }
213         } catch (GordianException e) {
214             throw new PrometheusSecurityException(e);
215         }
216     }
217 
218     /**
219      * Create a Backup ZipFile.
220      *
221      * @param pData Data to write out
222      * @param pFile the backup file to write to
223      * @throws OceanusException on error
224      */
225     public void createExtract(final PrometheusDataSet pData,
226                               final File pFile) throws OceanusException {
227         boolean writeFailed = false;
228         try {
229             createExtract(pData, new FileOutputStream(pFile));
230         } catch (IOException
231                  | OceanusException e) {
232             writeFailed = true;
233             throw new PrometheusIOException(ERROR_BACKUP, e);
234         } finally {
235             /* Try to delete the file if required */
236             if (writeFailed) {
237                 MetisToolkit.cleanUpFile(pFile);
238             }
239         }
240     }
241 
242     /**
243      * Create an Extract ZipFile.
244      *
245      * @param pData      Data to write out
246      * @param pZipStream the output stream
247      * @throws OceanusException on error
248      */
249     public void createExtract(final PrometheusDataSet pData,
250                               final OutputStream pZipStream) throws OceanusException {
251         /* Obtain the active profile */
252         final OceanusProfile myTask = theReport.getActiveTask();
253         final OceanusProfile myStage = myTask.startTask("Writing");
254 
255         /* Access the data version */
256         theVersion = pData.getControl().getDataVersion();
257 
258         /* Declare the number of stages */
259         theReport.setNumStages(pData.getListMap().size());
260         final GordianZipFactory myZips = thePasswordMgr.getSecurityFactory().getZipFactory();
261 
262         /* Protect the workbook access */
263         try (GordianZipWriteFile myZipFile = myZips.createZipFile(pZipStream)) {
264             /* Loop through the data lists */
265             final Iterator<PrometheusDataList<?>> myIterator = pData.iterator();
266             while (myIterator.hasNext()) {
267                 final PrometheusDataList<?> myList = myIterator.next();
268 
269                 /* Declare the new stage */
270                 theReport.setNewStage(myList.listName());
271 
272                 /* If this list should be written */
273                 if (myList.includeDataXML()) {
274                     /* Write the list details */
275                     myStage.startTask(myList.listName());
276                     writeXMLListToFile(myList, myZipFile, false);
277                 }
278             }
279 
280             /* Complete the task */
281             myStage.end();
282 
283         } catch (IOException
284                  | OceanusException e) {
285             throw new PrometheusIOException("Failed to create extract XML", e);
286         }
287     }
288 
289     /**
290      * Write XML list to file.
291      *
292      * @param pList     the data list
293      * @param pZipFile  the output zipFile
294      * @param pStoreIds do we include IDs in XML
295      * @throws OceanusException on error
296      */
297     private void writeXMLListToFile(final PrometheusDataList<?> pList,
298                                     final GordianZipWriteFile pZipFile,
299                                     final boolean pStoreIds) throws OceanusException {
300         /* Access the list name */
301         final String myName = pList.listName() + SUFFIX_ENTRY;
302 
303         /* Protect the workbook access */
304         try (OutputStream myStream = pZipFile.createOutputStream(new File(myName), true)) {
305             /* Create a new document */
306             final Document myDocument = theBuilder.newDocument();
307 
308             /* Populate the document from the list */
309             populateXML(myDocument, pList, pStoreIds);
310 
311             /* Format the XML and write to stream */
312             theXformer.transform(new DOMSource(myDocument), new StreamResult(myStream));
313 
314         } catch (GordianException
315                  | TransformerException
316                  | IOException e) {
317             throw new PrometheusIOException("Failed to transform XML", e);
318         }
319     }
320 
321     /**
322      * Create XML for a list.
323      *
324      * @param pDocument the document to hold the list.
325      * @param pList     the data list
326      * @param pStoreIds do we include IDs in XML
327      * @throws OceanusException on error
328      */
329     private void populateXML(final Document pDocument,
330                              final PrometheusDataList<?> pList,
331                              final boolean pStoreIds) throws OceanusException {
332         /* Create an element for the item */
333         final Element myElement = pDocument.createElement(pList.listName());
334         pDocument.appendChild(myElement);
335 
336         /* Access the Data formatter */
337         final OceanusDataFormatter myFormatter = pList.getDataSet().getDataFormatter();
338 
339         /* Declare the number of steps */
340         final int myTotal = pList.size();
341         theReport.setNumSteps(myTotal);
342 
343         /* Set the list type and size */
344         myElement.setAttribute(PrometheusDataValues.ATTR_TYPE, pList.getItemType().getItemName());
345         myElement.setAttribute(PrometheusDataValues.ATTR_SIZE, Integer.toString(myTotal));
346         myElement.setAttribute(PrometheusDataValues.ATTR_VERS, Integer.toString(theVersion));
347 
348         /* Iterate through the list */
349         final Iterator<?> myIterator = pList.iterator();
350         while (myIterator.hasNext()) {
351             final Object myObject = myIterator.next();
352 
353             /* Ignore if not a DataItem */
354             if (!(myObject instanceof PrometheusDataItem myItem)) {
355                 continue;
356             }
357 
358             /* Skip over child items */
359             if (myItem instanceof PrometheusGroupedItem myGrouped
360                     && myGrouped.isChild()) {
361                 continue;
362             }
363 
364             /* Create DataValues for item */
365             final PrometheusDataValues myValues = new PrometheusDataValues(myItem);
366 
367             /* Add the child to the list */
368             final Element myChild = myValues.createXML(pDocument, myFormatter, pStoreIds);
369             myElement.appendChild(myChild);
370 
371             /* Report the progress */
372             theReport.setNextStep();
373         }
374     }
375 
376     /**
377      * Load a ZipFile.
378      *
379      * @param pData DataSet to load into
380      * @param pFile the file to load
381      * @throws OceanusException on error
382      */
383     public void loadZipFile(final PrometheusDataSet pData,
384                             final File pFile) throws OceanusException {
385         try {
386             loadZipFile(pData, new FileInputStream(pFile), pFile.getName());
387         } catch (IOException e) {
388             throw new PrometheusIOException("Failed to access ZipFile", e);
389         }
390     }
391 
392     /**
393      * Load a ZipFile.
394      *
395      * @param pData     DataSet to load into
396      * @param pInStream the input stream
397      * @param pName     the file to load
398      * @throws OceanusException on error
399      */
400     public void loadZipFile(final PrometheusDataSet pData,
401                             final InputStream pInStream,
402                             final String pName) throws OceanusException {
403         /* Protect against exceptions */
404         try {
405             /* Obtain the active profile */
406             final OceanusProfile myTask = theReport.getActiveTask();
407             final OceanusProfile myStage = myTask.startTask("Loading");
408             myStage.startTask("Parsing");
409 
410             /* Access the zip file */
411             final GordianZipFactory myZips = thePasswordMgr.getSecurityFactory().getZipFactory();
412             final GordianZipReadFile myZipFile = myZips.openZipFile(pInStream);
413 
414             /* Obtain the hash bytes from the file */
415             final GordianZipLock myLock = myZipFile.getLock();
416 
417             /* If this is a secure ZipFile */
418             if (myLock != null) {
419                 /* Resolve the lock */
420                 thePasswordMgr.resolveZipLock(myLock, pName);
421             }
422 
423             /* Parse the Zip File */
424             parseZipFile(myStage, pData, myZipFile);
425 
426             /* Complete the task */
427             myStage.end();
428 
429         } catch (GordianException e) {
430             throw new PrometheusSecurityException(e);
431         }
432     }
433 
434     /**
435      * Parse a ZipFile.
436      *
437      * @param pProfile the active profile
438      * @param pData    DataSet to load into
439      * @param pZipFile the file to parse
440      * @throws OceanusException on error
441      */
442     private void parseZipFile(final OceanusProfile pProfile,
443                               final PrometheusDataSet pData,
444                               final GordianZipReadFile pZipFile) throws OceanusException {
445         /* Start new stage */
446         final OceanusProfile myStage = pProfile.startTask("Loading");
447 
448         /* Declare the number of stages */
449         theReport.setNumStages(pData.getListMap().size());
450 
451         /* Loop through the data lists */
452         final Iterator<PrometheusDataList<?>> myIterator = pData.iterator();
453         while (myIterator.hasNext()) {
454             final PrometheusDataList<?> myList = myIterator.next();
455 
456             /* Declare the new stage */
457             theReport.setNewStage(myList.listName());
458 
459             /* If this list should be read */
460             if (myList.includeDataXML()) {
461                 /* Write the list details */
462                 myStage.startTask(myList.listName());
463                 readXMLListFromFile(myList, pZipFile);
464             }
465 
466             /* postProcessList after load */
467             myList.postProcessOnLoad();
468         }
469 
470         /* Create the control data */
471         pData.getControlData().addNewControl(theVersion);
472 
473         /* Complete the task */
474         myStage.end();
475     }
476 
477     /**
478      * Read XML list from file.
479      *
480      * @param pList    the data list
481      * @param pZipFile the input zipFile
482      * @throws OceanusException on error
483      */
484     private void readXMLListFromFile(final PrometheusDataList<?> pList,
485                                      final GordianZipReadFile pZipFile) throws OceanusException {
486         /* Protect against exceptions */
487         try {
488             /* Access the list name */
489             final String myName = pList.listName() + SUFFIX_ENTRY;
490 
491             /* Locate the correct entry */
492             final GordianZipFileContents myContents = pZipFile.getContents();
493             final GordianZipFileEntry myEntry = myContents.findFileEntry(myName);
494             if (myEntry == null) {
495                 throw new PrometheusDataException("List not found " + myName);
496             }
497 
498             /* Protect the workbook access */
499             try (InputStream myStream = pZipFile.createInputStream(myEntry)) {
500                 /* Read the document from the stream and parse it */
501                 final Document myDocument = theBuilder.parse(myStream);
502 
503                 /* Populate the list from the document */
504                 parseXMLDocument(myDocument, pList);
505 
506             } catch (IOException
507                      | SAXException e) {
508                 throw new PrometheusIOException("Failed to parse XML", e);
509             }
510         } catch (GordianException e) {
511             throw new PrometheusSecurityException(e);
512         }
513     }
514 
515     /**
516      * parse an XML document into DataValues.
517      *
518      * @param pDocument the document that holds the list.
519      * @param pList     the data list
520      * @throws OceanusException on error
521      */
522     private void parseXMLDocument(final Document pDocument,
523                                   final PrometheusDataList<?> pList) throws OceanusException {
524         /* Access the parent element */
525         final Element myElement = pDocument.getDocumentElement();
526         final MetisListKey myItemType = pList.getItemType();
527 
528         /* Check that the document name and dataType are correct */
529         if (!MetisDataDifference.isEqual(myElement.getNodeName(), pList.listName())
530                 || !MetisDataDifference.isEqual(myElement.getAttribute(PrometheusDataValues.ATTR_TYPE), myItemType.getItemName())) {
531             throw new PrometheusDataException("Invalid list type");
532         }
533 
534         /* If this is the first Data version */
535         final Integer myVersion = Integer.valueOf(myElement.getAttribute(PrometheusDataValues.ATTR_VERS));
536         if (theVersion == null) {
537             theVersion = myVersion;
538         } else if (!theVersion.equals(myVersion)) {
539             throw new PrometheusDataException("Inconsistent data version");
540         }
541 
542         /* Access field types for list */
543         final MetisFieldSetDef myFields = pList.getItemFields();
544 
545         /* Access the Data formatter */
546         final OceanusDataFormatter myFormatter = pList.getDataSet().getDataFormatter();
547 
548         /* Declare the number of steps */
549         final int myTotal = getListCount(myFormatter, myElement);
550         theReport.setNumSteps(myTotal);
551 
552         /* Loop through the children */
553         for (Node myChild = myElement.getFirstChild(); myChild != null; myChild = myChild.getNextSibling()) {
554             /* Ignore non-elements */
555             if (!(myChild instanceof Element)) {
556                 continue;
557             }
558 
559             /* Access as Element */
560             final Element myItem = (Element) myChild;
561 
562             /* Create DataArguments for item */
563             final PrometheusDataValues myValues = new PrometheusDataValues(myItem, myFields);
564 
565             /* Add the child to the list */
566             pList.addValuesItem(myValues);
567 
568             /* Report the progress */
569             theReport.setNextStep();
570         }
571     }
572 
573     /**
574      * Obtain count attribute.
575      *
576      * @param pFormatter the formatter.
577      * @param pElement   the element that holds the count.
578      * @return the list count
579      * @throws OceanusException on error
580      */
581     private static Integer getListCount(final OceanusDataFormatter pFormatter,
582                                         final Element pElement) throws OceanusException {
583         try {
584             /* Access the list count */
585             final String mySize = pElement.getAttribute(PrometheusDataValues.ATTR_SIZE);
586             return pFormatter.parseValue(mySize, Integer.class);
587         } catch (NumberFormatException e) {
588             throw new PrometheusDataException("Invalid list count", e);
589         }
590     }
591 }