package org.autoplot.state; import java.io.FileNotFoundException; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.parsers.ParserConfigurationException; import java.beans.XMLDecoder; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PushbackInputStream; import java.lang.reflect.InvocationTargetException; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Formatter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.TransformerConfigurationException; import org.das2.util.AboutUtil; import org.autoplot.RenderType; import org.autoplot.dom.Application; import org.autoplot.dom.DomNode; import org.autoplot.dom.PlotElement; import org.w3c.dom.DOMException; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSException; import org.w3c.dom.ls.LSOutput; import org.w3c.dom.ls.LSSerializer; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamSource; import javax.xml.transform.TransformerException; import org.jdesktop.beansbinding.AutoBinding; import org.jdesktop.beansbinding.BeanProperty; import org.jdesktop.beansbinding.Binding; import org.jdesktop.beansbinding.Bindings; import org.autoplot.dom.Axis; import org.autoplot.dom.BindingModel; import org.autoplot.dom.Canvas; import org.autoplot.dom.DomUtil; import org.autoplot.dom.Plot; import org.autoplot.dom.Row; import org.das2.qstream.SerializeDelegate; import org.das2.qstream.SerializeRegistry; /** * Class for serializing and deserializing application configuration. * @author jbf */ public class StatePersistence { private static final Logger logger= org.das2.util.LoggerManager.getLogger("autoplot.dom.vap"); private StatePersistence() { } /** * return the current DOM version, describing the scheme of the DOM. * @return */ public static AbstractVapScheme currentScheme() { return new Vap1_08Scheme(); } public static void saveState( File f, Object state ) throws IOException { saveState( new FileOutputStream(f), state, "" ); } /** * Save the Object (DOM application) to a file. * @param f the file target where the state is saved * @param state the Object to be saved, in Autoplot's case it's the DOM. * @param sscheme empty string or the name of a scheme to target, such as "1.06" * @throws IOException */ public static void saveState( File f, Object state, String sscheme ) throws IOException { saveState( new FileOutputStream(f), state, sscheme ); } /** * Save the Object (DOM application) to a file. * @param out the output stream. * @param state the Object to be saved, in Autoplot's case it's the DOM. * @param sscheme empty string or the name of a scheme to target, such as "1.06" * @throws IOException */ public static void saveState( OutputStream out, Object state, String sscheme ) throws IOException { Document document=null; try { document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); } catch (ParserConfigurationException ex) { logger.log(Level.SEVERE, ex.getMessage(), ex); throw new RuntimeException(ex); } VapScheme currentScheme= new Vap1_09Scheme(); VapScheme scheme; switch (sscheme) { case "": scheme= new Vap1_08Scheme(); break; case "1.09": scheme= new Vap1_09Scheme(); break; case "1.08": scheme= new Vap1_08Scheme(); break; case "1.07": scheme= new Vap1_07Scheme(); break; case "1.06": scheme= new Vap1_06Scheme(); break; default: throw new IllegalArgumentException("output scheme not supported: "+sscheme); } if ( scheme.getId().equals( "1.08" ) && currentScheme.getId().equals("1.09") ) { boolean removedBindings= false; Application app= (Application)state; List newbms= new ArrayList( Arrays.asList( app.getBindings() ) ); for ( int i=app.getBindings().length-1; i>=0; i-- ) { if ( app.getBindings(i).getSrcProperty().equals("scale") ||app.getBindings(i).getDstProperty().equals("scale") ) { newbms.remove(i); removedBindings= true; } } if ( removedBindings ) { logger.warning("removing all bindings to scale to support old versions"); app.setBindings(newbms.toArray( new BindingModel[newbms.size()] ) ); } } Element element = SerializeUtil.getDomElement( document, (DomNode)state, scheme, true ); Element vap= document.createElement("vap"); vap.appendChild(element); vap.setAttribute( "domVersion", scheme.getId() ); vap.setAttribute( "appVersionTag", AboutUtil.getReleaseTag() ); document.appendChild(vap); if ( !scheme.getId().equals( currentScheme.getId() ) ) { try { //doConvert( document, scheme.getId(), currentScheme.getId() ); logger.log(Level.INFO, "converting from vap version {0} to {1}", new Object[]{currentScheme.getId(), scheme.getId()}); doConvert( document, currentScheme.getId(), scheme.getId() ); } catch ( TransformerException ex ) { logger.log( Level.WARNING, ex.getMessage(), ex ); IOException result= new IOException("Unable to export to version "+sscheme,ex ); throw result; } } writeDocument( out, document); } /** * write the document out to the file, hiding the details of the serializer. * @param out outputStream * @param document XML document object * @throws LSException * @throws DOMException * @throws FileNotFoundException */ public static void writeDocument( OutputStream out, Document document) throws IOException { DOMImplementation impl = document.getImplementation(); DOMImplementationLS ls = (DOMImplementationLS) impl.getFeature("LS", "3.0"); LSSerializer serializer = ls.createLSSerializer(); LSOutput output = ls.createLSOutput(); output.setEncoding("UTF-8"); output.setByteStream(out); try { if (serializer.getDomConfig().canSetParameter("format-pretty-print", Boolean.TRUE)) { serializer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); } } catch (Error e2) { // Ed's nice trick for finding the implementation //String name = serializer.getClass().getSimpleName(); //java.net.URL u = serializer.getClass().getResource(name + ".class"); //System.err.println(u); logger.log( Level.WARNING, e2.getMessage(), e2 ); } serializer.write(document, output); out.close(); } /** * write the document out to the file, hiding the details of the serializer. * @param f the file * @param document the XML document model. * @throws LSException * @throws DOMException * @throws FileNotFoundException */ public static void writeDocument(File f, Document document) throws FileNotFoundException, IOException { OutputStream out = new FileOutputStream( f ); writeDocument( out, document ); } /** * return the first child, if any, with the given tag name. * @param parent the node of the tree. * @param tagName the node to look for. * @return return the child or null if no such child exists. */ public static Element getChildElement( Element parent, String tagName ) { NodeList nl= parent.getChildNodes(); for ( int i=0; i macros= new LinkedHashMap<>(); String pwd= "file:"+f.getParent()+"/"; macros.put("PWD", pwd ); result= restoreState( in, macros ); } finally { in.close(); } return result; } private static Document doConvert( Document document, String domVersion, String currentVersion ) throws TransformerConfigurationException, TransformerException { double srcVersion= Double.parseDouble(domVersion); double dstVersion= Double.parseDouble(currentVersion); if (srcVersion > dstVersion) { // downgrade future versions. This is experimental, but slightly // better than not allowing use. This is intended to smooth // transitions to new autoplot versions. Future vap files // that use future features will not load properly. for ( double s=srcVersion; s>dstVersion; s=s-0.01 ) { Source src = new DOMSource( document ); DOMResult res = new DOMResult( ); String fname= String.format( Locale.US, "Vap_%4.2f_to_%4.2f", s, s-0.01 ); fname= fname.replaceAll("\\.","_") + ".xsl"; InputStream xsl = StatePersistence.class.getResourceAsStream(fname); if ( xsl==null ) { throw new RuntimeException("Unable to find "+fname+"."); } TransformerFactory factory = TransformerFactory.newInstance(); Transformer tr = factory.newTransformer( new StreamSource(xsl) ); tr.transform(src, res); document= ((Document)res.getNode()); } } return document; } private static Application readLegacyFile( Document document ) throws IOException { Application state; importLegacyVap(document.getDocumentElement()); ByteArrayOutputStream baos= new ByteArrayOutputStream(10000); //throw new RuntimeException("It is no longer possible to convert old " + // "files at runtime. Contact Autoplot group at Google groups " + // "for conversion help." ); logger.info("importing legacy vap file v0.99. "); logger.info("These must be rewritten to new vap format, support will be dropped."); DOMImplementation impl = document.getImplementation(); DOMImplementationLS ls = (DOMImplementationLS) impl.getFeature("LS", "3.0"); LSSerializer serializer = ls.createLSSerializer(); LSOutput output = ls.createLSOutput(); output.setEncoding("UTF-8"); output.setByteStream(baos); serializer.write(document, output); baos.close(); XMLDecoder decode= new XMLDecoder( new ByteArrayInputStream( baos.toByteArray() ) ); state= (Application) decode.readObject(); Application app= (Application)state; for ( PlotElement p: app.getPlotElements() ) { if ( p.getRenderType()==null ) { p.setRenderTypeAutomatically( RenderType.series ); } } for ( Plot p: app.getPlots() ) { p.getZaxis().setVisible(false); List pes= DomUtil.getPlotElementsFor(app, p); for ( PlotElement pe: pes ) { RenderType rt= pe.getRenderType(); if ( rt==RenderType.spectrogram || rt==RenderType.nnSpectrogram || rt==RenderType.colorScatter ) { p.getZaxis().setVisible(true); } } } return state; } /** * we need to way to implement bindings, since we may mutate the state * before syncing to it. This makes the state more valid and avoids * bugs like * https://sourceforge.net/p/autoplot/bugs/362/ * @param state the application model */ private static void doBindings( Application state ) { for ( BindingModel m: state.getBindings() ) { Object src= DomUtil.getElementById( state, m.getSrcId() ); Object dst= DomUtil.getElementById( state, m.getDstId() ); Binding binding = Bindings.createAutoBinding( AutoBinding.UpdateStrategy.READ_WRITE, src, BeanProperty.create(m.getSrcProperty()), dst, BeanProperty.create(m.getDstProperty() ) ); binding.bind(); } } /** * fix the state to make it valid, to the extent that this is possible. * For example, old vap files didn't specify rows, so we add rows to make * it. Note the mechanism used to save old states doesn't allow for importing, * since it's tied to classes in the running JRE. It would be non-trivial * to implement this. So we do this for now. * * @param state the application model * @see DomUtil#deleteDuplicateIds(org.autoplot.dom.Application) */ private static void makeValid( Application state ) { if ( state.getController()!=null ) throw new IllegalArgumentException("state must not have controller"); // check to see if rows need to be made Canvas c= state.getCanvases(0); if ( c.getMarginRow().getId().equals("") ) c.getMarginRow().setId("marginRow_0"); if ( c.getMarginColumn().getId().equals("") ) c.getMarginColumn().setId("marginColumn_0"); if ( state.getPlots(0).getRowId().equals("") ) { int n= state.getPlots().length; Row[] rows= new Row[n]; for ( int i=0; i property_value pairs to apply to the * DOM after it's loaded. * @return the DOM. * @throws IOException * @see #restoreState(java.io.InputStream) which just restores the state. */ public static Application restoreState(InputStream in, LinkedHashMap deltas) throws IOException { Application state = (Application) StatePersistence.restoreState(in); List problems= DomUtil.checkUniqueIdsAndReferences( state, new ArrayList<>() ); if ( problems.size()>0 ) { for ( String s: problems ) { logger.warning(s); } } makeValid( state ); if (deltas != null) { doBindings( state ); for (Map.Entry e : deltas.entrySet()) { logger.log(Level.FINEST, "applying to vap {0}={1}", new Object[]{e.getKey(), e.getValue()}); String node = e.getKey(); String sval = e.getValue(); // BeanProperty prop = BeanProperty.create(node); // if (!prop.isWriteable(state)) { // logger.warning("the node " + node + " of " + state + " is not writable"); // continue; // } // Class c = prop.getWriteType(state); if ( Character.isUpperCase( node.charAt(0) ) ) { DomUtil.applyMacro( state, "%{"+node+"}", sval ); } else { Class c; try { c = DomUtil.getPropertyType(state, node); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { logger.log(Level.SEVERE, ex.getMessage(), ex); continue; } SerializeDelegate sd = SerializeRegistry.getDelegate(c); if (sd == null) { System.err.println("unable to find serialize delegate for " + c.getCanonicalName()); continue; } Object val; try { if ( sval.length()>1 && sval.startsWith("'") && sval.endsWith("'") ) { sval= sval.substring(1,sval.length()-1); } val = sd.parse(sd.typeId(c), sval); // prop.setValue(state, val); DomUtil.setPropertyValue(state, node, val); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { logger.log(Level.SEVERE, ex.getMessage(), ex); } catch (ParseException ex) { IOException ioex= new IOException( ex.getMessage() ); throw ioex; //logger.log(Level.SEVERE, ex.getMessage(), ex); } } } } return state; } /** * restore the XML on the inputStream, possibly promoting it to a modern version. * @param in, an input stream that starts with the xml. This will be left open. * @return the Application object. * @throws IOException * @throws IllegalArgumentException if the stream is not a .vap. * @see #restoreState(java.io.InputStream, java.util.LinkedHashMap) see restoreState which has macros like "PWD" */ public static Object restoreState( InputStream in ) throws IOException { PushbackInputStream pbin= new PushbackInputStream(in,10); int available= pbin.available(); logger.log(Level.FINER, "available bytes in stream: {0}", available); if ( available<5 ) { throw new IllegalArgumentException("expected to find file that contained at least 5 characters"); } else { byte[] five= new byte[5]; int bytesRead= pbin.read(five); while ( bytesRead<5 ) { bytesRead+= pbin.read(five,bytesRead,5-bytesRead); } String magic= new String( five ); if ( !( magic.equals(" dstVersion) { // downgrade future versions. This is experimental, but slightly // better than not allowing use. This is intended to smooth // transitions to new autoplot versions. Future vap files // that use future features will not load properly, but may still be usable. for ( double s=srcVersion; s>dstVersion; s=s-0.01 ) { Source src = new DOMSource( root ); DOMResult res = new DOMResult( ); String fname= String.format( Locale.US, "Vap_%4.2f_to_%4.2f", s, s-0.01 ); fname= fname.replaceAll("\\.","_") + ".xsl"; InputStream xsl = StatePersistence.class.getResourceAsStream(fname); if ( xsl==null ) { // Unable to find the file 'fname' String vv= new Formatter().format( Locale.US, "%.2f",srcVersion ).toString() ; throw new RuntimeException("Unable to read .vap file version "+vv+". Upgrade to a newer version of Autoplot."); } TransformerFactory factory = TransformerFactory.newInstance(); Transformer tr = factory.newTransformer(new StreamSource(xsl)); tr.transform(src, res); root= (Element)res.getNode().getFirstChild(); } } else { // upgrade old vap file versions. for ( double s=srcVersion; s