Merge branch 'develop' into feature/JAL-3551Pymol
[jalview.git] / src / jalview / project / Jalview2XML.java
index 6340e64..4cfac7b 100644 (file)
@@ -24,6 +24,56 @@ import static jalview.math.RotatableMatrix.Axis.X;
 import static jalview.math.RotatableMatrix.Axis.Y;
 import static jalview.math.RotatableMatrix.Axis.Z;
 
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Rectangle;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.DataOutputStream;
+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.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.math.BigInteger;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Vector;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+
+import javax.swing.JInternalFrame;
+import javax.swing.SwingUtilities;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.Marshaller;
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamReader;
+
 import jalview.analysis.Conservation;
 import jalview.analysis.PCA;
 import jalview.analysis.scoremodels.ScoreModels;
@@ -53,17 +103,21 @@ import jalview.datamodel.features.FeatureMatcher;
 import jalview.datamodel.features.FeatureMatcherI;
 import jalview.datamodel.features.FeatureMatcherSet;
 import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 import jalview.ext.varna.RnaModel;
 import jalview.gui.AlignFrame;
 import jalview.gui.AlignViewport;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.AppVarna;
 import jalview.gui.ChimeraViewFrame;
+import jalview.gui.ChimeraXViewFrame;
 import jalview.gui.Desktop;
+import jalview.gui.JalviewChimeraXBindingModel;
 import jalview.gui.JvOptionPane;
 import jalview.gui.OOMWarning;
 import jalview.gui.PCAPanel;
 import jalview.gui.PaintRefresher;
+import jalview.gui.PymolViewer;
 import jalview.gui.SplitFrame;
 import jalview.gui.StructureViewer;
 import jalview.gui.StructureViewer.ViewerType;
@@ -150,55 +204,6 @@ import jalview.xml.binding.jalview.SequenceSet.SequenceSetProperties;
 import jalview.xml.binding.jalview.ThresholdType;
 import jalview.xml.binding.jalview.VAMSAS;
 
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.Rectangle;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.lang.reflect.InvocationTargetException;
-import java.math.BigInteger;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.Vector;
-import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
-import java.util.jar.JarOutputStream;
-
-import javax.swing.JInternalFrame;
-import javax.swing.SwingUtilities;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.Marshaller;
-import javax.xml.datatype.DatatypeConfigurationException;
-import javax.xml.datatype.DatatypeFactory;
-import javax.xml.datatype.XMLGregorianCalendar;
-import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLStreamReader;
-
 /**
  * Write out the current jalview desktop state as a Jalview XML stream.
  * 
@@ -1099,15 +1104,17 @@ public class Jalview2XML
               if (!storeDS && !viewIds.contains(viewId))
               {
                 viewIds.add(viewId);
-                try
+                File viewerState = viewFrame.saveSession();
+                if (viewerState != null)
                 {
-                  String viewerState = viewFrame.getStateInfo();
-                  writeJarEntry(jout, getViewerJarEntryName(viewId),
-                          viewerState.getBytes());
-                } catch (IOException e)
+                  copyFileToJar(jout, viewerState.getPath(),
+                          getViewerJarEntryName(viewId));
+                }
+                else
                 {
-                  System.err.println(
-                          "Error saving viewer state: " + e.getMessage());
+                  Cache.log.error("Failed to save viewer state for "
+                          +
+                          viewFrame.getViewerType().toString());
                 }
               }
             }
@@ -2005,32 +2012,23 @@ public class Jalview2XML
   protected void copyFileToJar(JarOutputStream jout, String infilePath,
           String jarEntryName)
   {
-    DataInputStream dis = null;
-    try
+    try (InputStream is = new FileInputStream(infilePath))
     {
       File file = new File(infilePath);
       if (file.exists() && jout != null)
       {
-        dis = new DataInputStream(new FileInputStream(file));
-        byte[] data = new byte[(int) file.length()];
-        dis.readFully(data);
-        writeJarEntry(jout, jarEntryName, data);
+        System.out.println("Writing jar entry " + jarEntryName);
+        jout.putNextEntry(new JarEntry(jarEntryName));
+        copyAll(is, jout);
+        jout.closeEntry();
+        // dis = new DataInputStream(new FileInputStream(file));
+        // byte[] data = new byte[(int) file.length()];
+        // dis.readFully(data);
+        // writeJarEntry(jout, jarEntryName, data);
       }
     } catch (Exception ex)
     {
       ex.printStackTrace();
-    } finally
-    {
-      if (dis != null)
-      {
-        try
-        {
-          dis.close();
-        } catch (IOException e)
-        {
-          // ignore
-        }
-      }
     }
   }
 
@@ -2058,6 +2056,24 @@ public class Jalview2XML
   }
 
   /**
+   * Copies input to output, in 4K buffers; handles any data (text or binary)
+   * 
+   * @param in
+   * @param out
+   * @throws IOException
+   */
+  protected void copyAll(InputStream in, OutputStream out)
+          throws IOException
+  {
+    byte[] buffer = new byte[4096];
+    int bytesRead = 0;
+    while ((bytesRead = in.read(buffer)) != -1)
+    {
+      out.write(buffer, 0, bytesRead);
+    }
+  }
+
+  /**
    * Save the state of a structure viewer
    * 
    * @param ap
@@ -2124,7 +2140,7 @@ public class Jalview2XML
           final String viewId = viewFrame.getViewId();
           state.setViewId(viewId);
           state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
-          state.setColourwithAlignPanel(viewFrame.isUsedforcolourby(ap));
+          state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap));
           state.setColourByJmol(viewFrame.isColouredByViewer());
           state.setType(viewFrame.getViewerType().toString());
           // pdb.addStructureState(state);
@@ -3185,8 +3201,6 @@ public class Jalview2XML
   protected String copyJarEntry(jarInputStreamProvider jprovider,
           String jarEntryName, String prefix, String suffixModel)
   {
-    BufferedReader in = null;
-    PrintWriter out = null;
     String suffix = ".tmp";
     if (suffixModel == null)
     {
@@ -3197,33 +3211,24 @@ public class Jalview2XML
     {
       suffix = "." + suffixModel.substring(sfpos + 1);
     }
-    try
-    {
-      JarInputStream jin = jprovider.getJarInputStream();
-      /*
-       * if (jprovider.startsWith("http://")) { jin = new JarInputStream(new
-       * URL(jprovider).openStream()); } else { jin = new JarInputStream(new
-       * FileInputStream(jprovider)); }
-       */
 
+    try (JarInputStream jin = jprovider.getJarInputStream())
+    {
       JarEntry entry = null;
       do
       {
         entry = jin.getNextJarEntry();
       } while (entry != null && !entry.getName().equals(jarEntryName));
+
       if (entry != null)
       {
-        in = new BufferedReader(new InputStreamReader(jin, UTF_8));
+        // in = new BufferedReader(new InputStreamReader(jin, UTF_8));
         File outFile = File.createTempFile(prefix, suffix);
         outFile.deleteOnExit();
-        out = new PrintWriter(new FileOutputStream(outFile));
-        String data;
-
-        while ((data = in.readLine()) != null)
+        try (OutputStream os = new FileOutputStream(outFile))
         {
-          out.println(data);
+          copyAll(jin, os);
         }
-        out.flush();
         String t = outFile.getAbsolutePath();
         return t;
       }
@@ -3234,22 +3239,6 @@ public class Jalview2XML
     } catch (Exception ex)
     {
       ex.printStackTrace();
-    } finally
-    {
-      if (in != null)
-      {
-        try
-        {
-          in.close();
-        } catch (IOException e)
-        {
-          // ignore
-        }
-      }
-      if (out != null)
-      {
-        out.close();
-      }
     }
 
     return null;
@@ -4428,29 +4417,46 @@ public class Jalview2XML
      * From 2.9: stateData.type contains JMOL or CHIMERA, data is in jar entry
      * "viewer_"+stateData.viewId
      */
-    if (ViewerType.CHIMERA.toString().equals(stateData.getType()))
+    String type = stateData.getType();
+    if (type == null)
     {
-      createChimeraViewer(viewerData, af, jprovider);
+      type = ViewerType.JMOL.toString();
     }
-    else
+    try
     {
-      /*
-       * else Jmol (if pre-2.9, stateData contains JMOL state string)
-       */
-      createJmolViewer(viewerData, af, jprovider);
+      ViewerType viewerType = ViewerType.valueOf(type);
+      switch (viewerType)
+      {
+      case CHIMERA:
+        createChimeraViewer(viewerData, af, jprovider, false);
+        break;
+      case CHIMERAX:
+        createChimeraViewer(viewerData, af, jprovider, true);
+        break;
+      case PYMOL:
+        createPymolViewer(viewerData, af, jprovider);
+        break;
+      case JMOL:
+        createJmolViewer(viewerData, af, jprovider);
+      }
+    } catch (IllegalArgumentException | NullPointerException e)
+    {
+      Cache.log.error(
+              "Invalid structure viewer type: " + type);
     }
   }
 
   /**
-   * Create a new Chimera viewer.
+   * Create a new Chimera or ChimeraX viewer
    * 
    * @param data
    * @param af
    * @param jprovider
+   * @param isChimeraX
    */
   protected void createChimeraViewer(
           Entry<String, StructureViewerModel> viewerData, AlignFrame af,
-          jarInputStreamProvider jprovider)
+          jarInputStreamProvider jprovider, boolean isChimeraX)
   {
     StructureViewerModel data = viewerData.getValue();
     String chimeraSessionFile = data.getStateData();
@@ -4462,8 +4468,11 @@ public class Jalview2XML
      * 'uniquified' sviewid used to reconstruct the viewer here
      */
     String viewerJarEntryName = getViewerJarEntryName(data.getViewId());
+    String extension = isChimeraX
+            ? JalviewChimeraXBindingModel.CHIMERAX_SESSION_EXTENSION
+            : JalviewChimeraBinding.CHIMERA_SESSION_EXTENSION;
     chimeraSessionFile = copyJarEntry(jprovider, viewerJarEntryName,
-            "chimera", ".py");
+            "chimera", extension);
 
     Set<Entry<File, StructureData>> fileData = data.getFileData()
             .entrySet();
@@ -4489,9 +4498,13 @@ public class Jalview2XML
             .toArray(new SequenceI[allseqs.size()][]);
     String newViewId = viewerData.getKey();
 
-    ChimeraViewFrame cvf = new ChimeraViewFrame(chimeraSessionFile,
-            af.alignPanel, pdbArray, seqsArray, colourByChimera,
-            colourBySequence, newViewId);
+    ChimeraViewFrame cvf = isChimeraX
+            ? new ChimeraXViewFrame(chimeraSessionFile, af.alignPanel,
+                    pdbArray, seqsArray, colourByChimera, colourBySequence,
+                    newViewId)
+            : new ChimeraViewFrame(chimeraSessionFile, af.alignPanel,
+                    pdbArray, seqsArray, colourByChimera, colourBySequence,
+                    newViewId);
     cvf.setSize(data.getWidth(), data.getHeight());
     cvf.setLocation(data.getX(), data.getY());
   }
@@ -6360,6 +6373,61 @@ public class Jalview2XML
   }
 
   /**
+   * Create a new PyMol viewer
+   * 
+   * @param data
+   * @param af
+   * @param jprovider
+   */
+  protected void createPymolViewer(
+          Entry<String, StructureViewerModel> viewerData, AlignFrame af,
+          jarInputStreamProvider jprovider)
+  {
+    StructureViewerModel data = viewerData.getValue();
+    String pymolSessionFile = data.getStateData();
+  
+    /*
+     * Copy PyMol session from jar entry "viewer_"+viewId to a temporary file
+     * 
+     * NB this is the 'saved' viewId as in the project file XML, _not_ the
+     * 'uniquified' sviewid used to reconstruct the viewer here
+     */
+    String viewerJarEntryName = getViewerJarEntryName(data.getViewId());
+    pymolSessionFile = copyJarEntry(jprovider, viewerJarEntryName,
+            "pymol", ".pse");
+  
+    Set<Entry<File, StructureData>> fileData = data.getFileData()
+            .entrySet();
+    List<PDBEntry> pdbs = new ArrayList<>();
+    List<SequenceI[]> allseqs = new ArrayList<>();
+    for (Entry<File, StructureData> pdb : fileData)
+    {
+      String filePath = pdb.getValue().getFilePath();
+      String pdbId = pdb.getValue().getPdbId();
+      // pdbs.add(new PDBEntry(filePath, pdbId));
+      pdbs.add(new PDBEntry(pdbId, null, PDBEntry.Type.PDB, filePath));
+      final List<SequenceI> seqList = pdb.getValue().getSeqList();
+      SequenceI[] seqs = seqList.toArray(new SequenceI[seqList.size()]);
+      allseqs.add(seqs);
+    }
+  
+    boolean colourByPymol = data.isColourByViewer();
+    boolean colourBySequence = data.isColourWithAlignPanel();
+  
+    // TODO use StructureViewer as a factory here, see JAL-1761
+    final PDBEntry[] pdbArray = pdbs.toArray(new PDBEntry[pdbs.size()]);
+    final SequenceI[][] seqsArray = allseqs
+            .toArray(new SequenceI[allseqs.size()][]);
+    String newViewId = viewerData.getKey();
+  
+    PymolViewer pv = new PymolViewer(pymolSessionFile,
+            af.alignPanel, pdbArray, seqsArray, colourByPymol,
+            colourBySequence, newViewId);
+    pv.setSize(data.getWidth(), data.getHeight());
+    pv.setLocation(data.getX(), data.getY());
+  }
+
+  /**
    * Populates an XML model of the feature colour scheme for one feature type
    * 
    * @param featureType