JAL-3691 automatic insertion of Locale.ROOT to toUpperCase() and toLowerCase() and...
[jalview.git] / src / jalview / project / Jalview2XML.java
index 09f1675..9b6741b 100644 (file)
  */
 package jalview.project;
 
+import java.util.Locale;
+
 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.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;
@@ -39,6 +90,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLocus;
 import jalview.datamodel.GraphLine;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.Point;
@@ -57,9 +109,7 @@ import jalview.gui.AlignFrame;
 import jalview.gui.AlignViewport;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.AppVarna;
-import jalview.gui.ChimeraViewFrame;
 import jalview.gui.Desktop;
-import jalview.gui.FeatureRenderer;
 import jalview.gui.JvOptionPane;
 import jalview.gui.OOMWarning;
 import jalview.gui.PCAPanel;
@@ -85,6 +135,7 @@ import jalview.schemes.UserColourScheme;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.Format;
+import jalview.util.HttpUtils;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.util.StringUtils;
@@ -93,6 +144,7 @@ import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.PCAModel;
 import jalview.viewmodel.ViewportRanges;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
 import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
 import jalview.ws.jws2.Jws2Discoverer;
@@ -149,55 +201,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.
  * 
@@ -214,21 +217,9 @@ public class Jalview2XML
   // BH 2018 we add the .jvp binary extension to J2S so that
   // it will declare that binary when we do the file save from the browser
 
-  private static void addJ2SBinaryType(String ext)
-  {
-    ext = "." + ext + "?";
-
-    /**
-     * @j2sNative
-     * 
-     *            J2S._binaryTypes.push(ext);
-     * 
-     */
-  }
-
   static
   {
-    addJ2SBinaryType(".jvp?");
+    Platform.addJ2SBinaryType(".jvp?");
   }
 
   private static final String VIEWER_PREFIX = "viewer_";
@@ -474,7 +465,7 @@ public class Jalview2XML
       public boolean isResolvable()
       {
         return super.isResolvable() && mp.getTo() != null;
-      };
+      }
 
       @Override
       boolean resolve()
@@ -706,7 +697,6 @@ public class Jalview2XML
       } catch (Exception foo)
       {
       }
-      ;
       jout.close();
     } catch (Exception ex)
     {
@@ -768,9 +758,10 @@ public class Jalview2XML
     try
     {
       // create backupfiles object and get new temp filename destination
-      BackupFiles backupfiles = new BackupFiles(jarFile);
+      boolean doBackup = BackupFiles.getEnabled();
+      BackupFiles backupfiles = doBackup ? new BackupFiles(jarFile) : null;
       FileOutputStream fos = new FileOutputStream(
-              backupfiles.getTempFilePath());
+              doBackup ? backupfiles.getTempFilePath() : jarFile);
 
       JarOutputStream jout = new JarOutputStream(fos);
       List<AlignFrame> frames = new ArrayList<>();
@@ -791,12 +782,14 @@ public class Jalview2XML
       } catch (Exception foo)
       {
       }
-      ;
       jout.close();
       boolean success = true;
 
-      backupfiles.setWriteSuccess(success);
-      success = backupfiles.rollBackupsAndRenameTempFile();
+      if (doBackup)
+      {
+        backupfiles.setWriteSuccess(success);
+        success = backupfiles.rollBackupsAndRenameTempFile();
+      }
 
       return success;
     } catch (Exception ex)
@@ -967,7 +960,7 @@ public class Jalview2XML
         else
         {
           vamsasSeq = createVamsasSequence(id, jds);
-//          vamsasSet.addSequence(vamsasSeq);
+          // vamsasSet.addSequence(vamsasSeq);
           vamsasSet.getSequence().add(vamsasSeq);
           vamsasSetIds.put(id, vamsasSeq);
           seqRefIds.put(id, jds);
@@ -1098,25 +1091,27 @@ public class Jalview2XML
             if (frames[f] instanceof StructureViewerBase)
             {
               StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
-              matchedFile = saveStructureState(ap, jds, pdb, entry, viewIds,
-                      matchedFile, viewFrame);
+              matchedFile = saveStructureViewer(ap, jds, pdb, entry,
+                      viewIds, matchedFile, viewFrame);
               /*
                * Only store each structure viewer's state once in the project
                * jar. First time through only (storeDS==false)
                */
               String viewId = viewFrame.getViewId();
+              String viewerType = viewFrame.getViewerType().toString();
               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), viewerType);
+                }
+                else
                 {
-                  System.err.println(
-                          "Error saving viewer state: " + e.getMessage());
+                  Cache.log.error(
+                          "Failed to save viewer state for " + viewerType);
                 }
               }
             }
@@ -1138,7 +1133,7 @@ public class Jalview2XML
             if (!pdbfiles.contains(pdbId))
             {
               pdbfiles.add(pdbId);
-              copyFileToJar(jout, matchedFile, pdbId);
+              copyFileToJar(jout, matchedFile, pdbId, pdbId);
             }
           }
 
@@ -1360,9 +1355,8 @@ public class Jalview2XML
 
             if (colourScheme instanceof jalview.schemes.UserColourScheme)
             {
-              jGroup.setColour(
-                      setUserColourScheme(colourScheme, userColours,
-                              object));
+              jGroup.setColour(setUserColourScheme(colourScheme,
+                      userColours, object));
             }
             else
             {
@@ -1408,7 +1402,7 @@ public class Jalview2XML
         }
       }
 
-      //jms.setJGroup(groups);
+      // jms.setJGroup(groups);
       Object group;
       for (JGroup grp : groups)
       {
@@ -1522,11 +1516,14 @@ public class Jalview2XML
       view.setFollowHighlight(av.isFollowHighlight());
       view.setFollowSelection(av.followSelection);
       view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus());
+      view.setShowComplementFeatures(av.isShowComplementFeatures());
+      view.setShowComplementFeaturesOnTop(
+              av.isShowComplementFeaturesOnTop());
       if (av.getFeaturesDisplayed() != null)
       {
         FeatureSettings fs = new FeatureSettings();
 
-        FeatureRenderer fr = ap.getSeqPanel().seqCanvas
+        FeatureRendererModel fr = ap.getSeqPanel().seqCanvas
                 .getFeatureRenderer();
         String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
 
@@ -1542,11 +1539,13 @@ public class Jalview2XML
              * save any filter for the feature type
              */
             FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
-            if (filter != null)  {
-              Iterator<FeatureMatcherI> filters = filter.getMatchers().iterator();
+            if (filter != null)
+            {
+              Iterator<FeatureMatcherI> filters = filter.getMatchers()
+                      .iterator();
               FeatureMatcherI firstFilter = filters.next();
-              setting.setMatcherSet(Jalview2XML.marshalFilter(
-                      firstFilter, filters, filter.isAnded()));
+              setting.setMatcherSet(Jalview2XML.marshalFilter(firstFilter,
+                      filters, filter.isAnded()));
             }
 
             /*
@@ -1595,8 +1594,7 @@ public class Jalview2XML
 
             setting.setDisplay(
                     av.getFeaturesDisplayed().isVisible(featureType));
-            float rorder = fr
-                    .getOrder(featureType);
+            float rorder = fr.getOrder(featureType);
             if (rorder > -1)
             {
               setting.setOrder(rorder);
@@ -1620,7 +1618,7 @@ public class Jalview2XML
           Group g = new Group();
           g.setName(grp);
           g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
-                          .booleanValue());
+                  .booleanValue());
           // fs.addGroup(g);
           fs.getGroup().add(g);
           groupsAdded.addElement(grp);
@@ -1682,7 +1680,7 @@ public class Jalview2XML
       // using save and then load
       try
       {
-       fileName = fileName.replace('\\', '/');
+        fileName = fileName.replace('\\', '/');
         System.out.println("Writing jar entry " + fileName);
         JarEntry entry = new JarEntry(fileName);
         jout.putNextEntry(entry);
@@ -1983,7 +1981,7 @@ public class Jalview2XML
 
                 String varnaStateFile = varna.getStateInfo(model.rna);
                 jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter();
-                copyFileToJar(jout, varnaStateFile, jarEntryName);
+                copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna");
                 rnaSessions.put(model, jarEntryName);
               }
               SecondaryStructure ss = new SecondaryStructure();
@@ -2007,59 +2005,48 @@ public class Jalview2XML
    * @param jout
    * @param infilePath
    * @param jarEntryName
+   * @param msg
+   *          additional identifying info to log to the console
    */
   protected void copyFileToJar(JarOutputStream jout, String infilePath,
-          String jarEntryName)
+          String jarEntryName, String msg)
   {
-    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 + " (" + msg + ")");
+        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
-        }
-      }
     }
   }
 
   /**
-   * Write the data to a new entry of given name in the output jar file
+   * Copies input to output, in 4K buffers; handles any data (text or binary)
    * 
-   * @param jout
-   * @param jarEntryName
-   * @param data
+   * @param in
+   * @param out
    * @throws IOException
    */
-  protected void writeJarEntry(JarOutputStream jout, String jarEntryName,
-          byte[] data) throws IOException
+  protected void copyAll(InputStream in, OutputStream out)
+          throws IOException
   {
-    if (jout != null)
+    byte[] buffer = new byte[4096];
+    int bytesRead = 0;
+    while ((bytesRead = in.read(buffer)) != -1)
     {
-      jarEntryName = jarEntryName.replace('\\','/');
-      System.out.println("Writing jar entry " + jarEntryName);
-      jout.putNextEntry(new JarEntry(jarEntryName));
-      DataOutputStream dout = new DataOutputStream(jout);
-      dout.write(data, 0, data.length);
-      dout.flush();
-      jout.closeEntry();
+      out.write(buffer, 0, bytesRead);
     }
   }
 
@@ -2076,7 +2063,7 @@ public class Jalview2XML
    * @param viewFrame
    * @return
    */
-  protected String saveStructureState(AlignmentPanel ap, SequenceI jds,
+  protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds,
           Pdbids pdb, PDBEntry entry, List<String> viewIds,
           String matchedFile, StructureViewerBase viewFrame)
   {
@@ -2091,8 +2078,8 @@ public class Jalview2XML
       final PDBEntry pdbentry = bindingModel.getPdbEntry(peid);
       final String pdbId = pdbentry.getId();
       if (!pdbId.equals(entry.getId())
-              && !(entry.getId().length() > 4 && entry.getId().toLowerCase()
-                      .startsWith(pdbId.toLowerCase())))
+              && !(entry.getId().length() > 4 && entry.getId().toLowerCase(Locale.ROOT)
+                      .startsWith(pdbId.toLowerCase(Locale.ROOT))))
       {
         /*
          * not interested in a binding to a different PDB entry here
@@ -2130,7 +2117,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);
@@ -2388,7 +2375,8 @@ public class Jalview2XML
   {
     if (calcIdParam.getVersion().equals("1.0"))
     {
-      final String[] calcIds = calcIdParam.getServiceURL().toArray(new String[0]);
+      final String[] calcIds = calcIdParam.getServiceURL()
+              .toArray(new String[0]);
       Jws2Instance service = Jws2Discoverer.getDiscoverer()
               .getPreferredServiceFor(calcIds);
       if (service != null)
@@ -2530,6 +2518,10 @@ public class Jalview2XML
         parentseq = jds;
       }
     }
+
+    /*
+     * save any dbrefs; special subclass GeneLocus is flagged as 'locus'
+     */
     if (dbrefs != null)
     {
       for (int d = 0, nd = dbrefs.size(); d < nd; d++)
@@ -2539,13 +2531,17 @@ public class Jalview2XML
         dbref.setSource(ref.getSource());
         dbref.setVersion(ref.getVersion());
         dbref.setAccessionId(ref.getAccessionId());
+        dbref.setCanonical(ref.isCanonical());
+        if (ref instanceof GeneLocus)
+        {
+          dbref.setLocus(true);
+        }
         if (ref.hasMap())
         {
-          Mapping mp = createVamsasMapping(ref.getMap(), parentseq,
-                  jds, recurse);
+          Mapping mp = createVamsasMapping(ref.getMap(), parentseq, jds,
+                  recurse);
           dbref.setMapping(mp);
         }
-        // vamsasSeq.addDBRef(dbref);
         vamsasSeq.getDBRef().add(dbref);
       }
     }
@@ -2651,7 +2647,7 @@ public class Jalview2XML
         for (int i = 0; i < colours.length; i++)
         {
           Colour col = new Colour();
-          col.setName(ResidueProperties.aa[i].toLowerCase());
+          col.setName(ResidueProperties.aa[i].toLowerCase(Locale.ROOT));
           col.setRGB(jalview.util.Format.getHexString(colours[i]));
           // jbucs.addColour(col);
           jbucs.getColour().add(col);
@@ -2667,12 +2663,12 @@ public class Jalview2XML
     return id;
   }
 
-  jalview.schemes.UserColourScheme getUserColourScheme(
-          JalviewModel jm, String id)
+  jalview.schemes.UserColourScheme getUserColourScheme(JalviewModel jm,
+          String id)
   {
     List<UserColours> uc = jm.getUserColours();
     UserColours colours = null;
-/*
+    /*
     for (int i = 0; i < uc.length; i++)
     {
       if (uc[i].getId().equals(id))
@@ -2681,7 +2677,7 @@ public class Jalview2XML
         break;
       }
     }
-*/
+    */
     for (UserColours c : uc)
     {
       if (c.getId().equals(id))
@@ -2709,10 +2705,9 @@ public class Jalview2XML
       newColours = new java.awt.Color[23];
       for (int i = 0; i < 23; i++)
       {
-        newColours[i] = new java.awt.Color(Integer.parseInt(
-                colours.getUserColourScheme().getColour().get(i + 24)
-                        .getRGB(),
-                16));
+        newColours[i] = new java.awt.Color(
+                Integer.parseInt(colours.getUserColourScheme().getColour()
+                        .get(i + 24).getRGB(), 16));
       }
       ucs.setLowerCaseColours(newColours);
     }
@@ -2771,7 +2766,7 @@ public class Jalview2XML
           public void run()
           {
             setLoadingFinishedForNewStructureViewers();
-          };
+          }
         });
       } catch (Exception x)
       {
@@ -2781,52 +2776,69 @@ public class Jalview2XML
     return af;
   }
 
-       @SuppressWarnings("unused")
-       private jarInputStreamProvider createjarInputStreamProvider(final Object ofile) throws MalformedURLException {
-
-               // BH 2018 allow for bytes already attached to File object
-               try {
-                       String file = (ofile instanceof File ? ((File) ofile).getCanonicalPath() : ofile.toString());
-                       byte[] bytes = /** @j2sNative ofile._bytes || */
-                                       null;
-                       URL url = null;
-                       errorMessage = null;
-                       uniqueSetSuffix = null;
-                       seqRefIds = null;
-                       viewportsAdded.clear();
-                       frefedSequence = null;
-
-                       if (file.startsWith("http://")) {
-                               url = new URL(file);
-                       }
-                       final URL _url = url;
-                       return new jarInputStreamProvider() {
-
-                               @Override
-                               public JarInputStream getJarInputStream() throws IOException {
-                                       if (bytes != null) {
-//                                             System.out.println("Jalview2XML: opening byte jarInputStream for bytes.length=" + bytes.length);
-                                               return new JarInputStream(new ByteArrayInputStream(bytes));
-                                       }
-                                       if (_url != null) {
-//                                             System.out.println("Jalview2XML: opening url jarInputStream for " + _url);
-                                               return new JarInputStream(_url.openStream());
-                                       } else {
-//                                             System.out.println("Jalview2XML: opening file jarInputStream for " + file);
-                                               return new JarInputStream(new FileInputStream(file));
-                                       }
-                               }
-
-                               @Override
-                               public String getFilename() {
-                                       return file;
-                               }
-                       };
-               } catch (IOException e) {
-                       e.printStackTrace();
-                       return null;
-               }
-       }
+  @SuppressWarnings("unused")
+  private jarInputStreamProvider createjarInputStreamProvider(
+          final Object ofile) throws MalformedURLException
+  {
+
+    // BH 2018 allow for bytes already attached to File object
+    try
+    {
+      String file = (ofile instanceof File
+              ? ((File) ofile).getCanonicalPath()
+              : ofile.toString());
+      byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
+              : null;
+      URL url = null;
+      errorMessage = null;
+      uniqueSetSuffix = null;
+      seqRefIds = null;
+      viewportsAdded.clear();
+      frefedSequence = null;
+
+      if (HttpUtils.startsWithHttpOrHttps(file))
+      {
+        url = new URL(file);
+      }
+      final URL _url = url;
+      return new jarInputStreamProvider()
+      {
+
+        @Override
+        public JarInputStream getJarInputStream() throws IOException
+        {
+          if (bytes != null)
+          {
+            // System.out.println("Jalview2XML: opening byte jarInputStream for
+            // bytes.length=" + bytes.length);
+            return new JarInputStream(new ByteArrayInputStream(bytes));
+          }
+          if (_url != null)
+          {
+            // System.out.println("Jalview2XML: opening url jarInputStream for "
+            // + _url);
+            return new JarInputStream(_url.openStream());
+          }
+          else
+          {
+            // System.out.println("Jalview2XML: opening file jarInputStream for
+            // " + file);
+            return new JarInputStream(new FileInputStream(file));
+          }
+        }
+
+        @Override
+        public String getFilename()
+        {
+          return file;
+        }
+      };
+    } catch (IOException e)
+    {
+      e.printStackTrace();
+      return null;
+    }
+  }
 
   /**
    * Recover jalview session from a jalview project archive. Caller may
@@ -2873,8 +2885,8 @@ public class Jalview2XML
           XMLStreamReader streamReader = XMLInputFactory.newInstance()
                   .createXMLStreamReader(jin);
           javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
-          JAXBElement<JalviewModel> jbe = um
-                  .unmarshal(streamReader, JalviewModel.class);
+          JAXBElement<JalviewModel> jbe = um.unmarshal(streamReader,
+                  JalviewModel.class);
           JalviewModel object = jbe.getValue();
 
           if (true) // !skipViewport(object))
@@ -3176,53 +3188,42 @@ public class Jalview2XML
    * @param prefix
    *          a prefix for the temporary file name, must be at least three
    *          characters long
-   * @param origFile
+   * @param suffixModel
    *          null or original file - so new file can be given the same suffix
    *          as the old one
    * @return
    */
   protected String copyJarEntry(jarInputStreamProvider jprovider,
-          String jarEntryName, String prefix, String origFile)
+          String jarEntryName, String prefix, String suffixModel)
   {
-    BufferedReader in = null;
-    PrintWriter out = null;
     String suffix = ".tmp";
-    if (origFile == null)
+    if (suffixModel == null)
     {
-      origFile = jarEntryName;
+      suffixModel = jarEntryName;
     }
-    int sfpos = origFile.lastIndexOf(".");
-    if (sfpos > -1 && sfpos < (origFile.length() - 3))
+    int sfpos = suffixModel.lastIndexOf(".");
+    if (sfpos > -1 && sfpos < (suffixModel.length() - 1))
     {
-      suffix = "." + origFile.substring(sfpos + 1);
+      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;
       }
@@ -3233,22 +3234,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;
@@ -3289,7 +3274,8 @@ public class Jalview2XML
   AlignFrame loadFromObject(JalviewModel jalviewModel, String file,
           boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
   {
-    SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet().get(0);
+    SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet()
+            .get(0);
     List<Sequence> vamsasSeqs = vamsasSet.getSequence();
 
     // JalviewModelSequence jms = object.getJalviewModelSequence();
@@ -3348,9 +3334,10 @@ public class Jalview2XML
           if (tmpSeq.getStart() != jseq.getStart()
                   || tmpSeq.getEnd() != jseq.getEnd())
           {
-            System.err.println(
-                    "Warning JAL-2154 regression: updating start/end for sequence "
-                            + tmpSeq.toString() + " to " + jseq);
+            System.err.println(String.format(
+                    "Warning JAL-2154 regression: updating start/end for sequence %s from %d/%d to %d/%d",
+                    tmpSeq.getName(), tmpSeq.getStart(), tmpSeq.getEnd(),
+                    jseq.getStart(), jseq.getEnd()));
           }
         }
         else
@@ -3633,8 +3620,8 @@ public class Jalview2XML
               else
               {
                 // defer to later
-                frefedSequence.add(
-                        newAlcodMapRef(map.getDnasq(), cf, mapping));
+                frefedSequence
+                        .add(newAlcodMapRef(map.getDnasq(), cf, mapping));
               }
             }
           }
@@ -3842,8 +3829,7 @@ public class Jalview2XML
         jaa.setCalcId(annotation.getCalcId());
         if (annotation.getProperty().size() > 0)
         {
-          for (Annotation.Property prop : annotation
-                  .getProperty())
+          for (Annotation.Property prop : annotation.getProperty())
           {
             jaa.setProperty(prop.getName(), prop.getValue());
           }
@@ -3924,9 +3910,9 @@ public class Jalview2XML
         sg.setShowNonconserved(safeBoolean(jGroup.isShowUnconserved()));
         sg.thresholdTextColour = safeInt(jGroup.getTextColThreshold());
         // attributes with a default in the schema are never null
-          sg.setShowConsensusHistogram(jGroup.isShowConsensusHistogram());
-          sg.setshowSequenceLogo(jGroup.isShowSequenceLogo());
-          sg.setNormaliseSequenceLogo(jGroup.isNormaliseSequenceLogo());
+        sg.setShowConsensusHistogram(jGroup.isShowConsensusHistogram());
+        sg.setshowSequenceLogo(jGroup.isShowSequenceLogo());
+        sg.setNormaliseSequenceLogo(jGroup.isNormaliseSequenceLogo());
         sg.setIgnoreGapsConsensus(jGroup.isIgnoreGapsinConsensus());
         if (jGroup.getConsThreshold() != null
                 && jGroup.getConsThreshold().intValue() != 0)
@@ -3970,8 +3956,9 @@ public class Jalview2XML
         if (addAnnotSchemeGroup)
         {
           // reconstruct the annotation colourscheme
-          sg.setColourScheme(constructAnnotationColour(
-                  jGroup.getAnnotationColours(), null, al, jalviewModel, false));
+          sg.setColourScheme(
+                  constructAnnotationColour(jGroup.getAnnotationColours(),
+                          null, al, jalviewModel, false));
         }
       }
     }
@@ -4177,8 +4164,8 @@ public class Jalview2XML
    * @param av
    * @param ap
    */
-  protected void loadTrees(JalviewModel jm, Viewport view,
-          AlignFrame af, AlignViewport av, AlignmentPanel ap)
+  protected void loadTrees(JalviewModel jm, Viewport view, AlignFrame af,
+          AlignViewport av, AlignmentPanel ap)
   {
     // TODO result of automated refactoring - are all these parameters needed?
     try
@@ -4213,10 +4200,8 @@ public class Jalview2XML
           // TODO: verify 'associate with all views' works still
           tp.getTreeCanvas().setViewport(av); // af.viewport;
           tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
-          // FIXME: should we use safeBoolean here ?
-          tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
-
         }
+        tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
         if (tp == null)
         {
           warn("There was a problem recovering stored Newick tree: \n"
@@ -4288,8 +4273,8 @@ public class Jalview2XML
           for (int s = 0; s < structureStateCount; s++)
           {
             // check to see if we haven't already created this structure view
-            final StructureState structureState = pdbid
-                    .getStructureState().get(s);
+            final StructureState structureState = pdbid.getStructureState()
+                    .get(s);
             String sviewid = (structureState.getViewId() == null) ? null
                     : structureState.getViewId() + uniqueSetSuffix;
             jalview.datamodel.PDBEntry jpdb = new jalview.datamodel.PDBEntry();
@@ -4319,10 +4304,15 @@ public class Jalview2XML
             }
             if (!structureViewers.containsKey(sviewid))
             {
+              String viewerType = structureState.getType();
+              if (viewerType == null) // pre Jalview 2.9
+              {
+                viewerType = ViewerType.JMOL.toString();
+              }
               structureViewers.put(sviewid,
                       new StructureViewerModel(x, y, width, height, false,
                               false, true, structureState.getViewId(),
-                              structureState.getType()));
+                              viewerType));
               // Legacy pre-2.7 conversion JAL-823 :
               // do not assume any view has to be linked for colour by
               // sequence
@@ -4352,8 +4342,8 @@ public class Jalview2XML
             colourByViewer &= structureState.isColourByJmol();
             jmoldat.setColourByViewer(colourByViewer);
 
-            if (jmoldat.getStateData().length() < structureState
-                    .getValue()/*Content()*/.length())
+            if (jmoldat.getStateData().length() < structureState.getValue()
+                    /*Content()*/.length())
             {
               jmoldat.setStateData(structureState.getValue());// Content());
             }
@@ -4423,246 +4413,16 @@ public class Jalview2XML
       return;
     }
 
-    /*
-     * From 2.9: stateData.type contains JMOL or CHIMERA, data is in jar entry
-     * "viewer_"+stateData.viewId
-     */
-    if (ViewerType.CHIMERA.toString().equals(stateData.getType()))
-    {
-      createChimeraViewer(viewerData, af, jprovider);
-    }
-    else
-    {
-      /*
-       * else Jmol (if pre-2.9, stateData contains JMOL state string)
-       */
-      createJmolViewer(viewerData, af, jprovider);
-    }
-  }
-
-  /**
-   * Create a new Chimera viewer.
-   * 
-   * @param data
-   * @param af
-   * @param jprovider
-   */
-  protected void createChimeraViewer(
-          Entry<String, StructureViewerModel> viewerData, AlignFrame af,
-          jarInputStreamProvider jprovider)
-  {
-    StructureViewerModel data = viewerData.getValue();
-    String chimeraSessionFile = data.getStateData();
-
-    /*
-     * Copy Chimera 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());
-    chimeraSessionFile = copyJarEntry(jprovider, viewerJarEntryName,
-            "chimera", null);
-
-    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 colourByChimera = 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();
-
-    ChimeraViewFrame cvf = new ChimeraViewFrame(chimeraSessionFile,
-            af.alignPanel, pdbArray, seqsArray, colourByChimera,
-            colourBySequence, newViewId);
-    cvf.setSize(data.getWidth(), data.getHeight());
-    cvf.setLocation(data.getX(), data.getY());
-  }
-
-  /**
-   * Create a new Jmol window. First parse the Jmol state to translate filenames
-   * loaded into the view, and record the order in which files are shown in the
-   * Jmol view, so we can add the sequence mappings in same order.
-   * 
-   * @param viewerData
-   * @param af
-   * @param jprovider
-   */
-  protected void createJmolViewer(
-          final Entry<String, StructureViewerModel> viewerData,
-          AlignFrame af, jarInputStreamProvider jprovider)
-  {
-    final StructureViewerModel svattrib = viewerData.getValue();
-    String state = svattrib.getStateData();
-
-    /*
-     * Pre-2.9: state element value is the Jmol state string
-     * 
-     * 2.9+: @type is "JMOL", state data is in a Jar file member named "viewer_"
-     * + viewId
-     */
-    if (ViewerType.JMOL.toString().equals(svattrib.getType()))
-    {
-      state = readJarEntry(jprovider,
-              getViewerJarEntryName(svattrib.getViewId()));
-    }
-
-    List<String> pdbfilenames = new ArrayList<>();
-    List<SequenceI[]> seqmaps = new ArrayList<>();
-    List<String> pdbids = new ArrayList<>();
-    StringBuilder newFileLoc = new StringBuilder(64);
-    int cp = 0, ncp, ecp;
-    Map<File, StructureData> oldFiles = svattrib.getFileData();
-    while ((ncp = state.indexOf("load ", cp)) > -1)
-    {
-      do
-      {
-        // look for next filename in load statement
-        newFileLoc.append(state.substring(cp,
-                ncp = (state.indexOf("\"", ncp + 1) + 1)));
-        String oldfilenam = state.substring(ncp,
-                ecp = state.indexOf("\"", ncp));
-        // recover the new mapping data for this old filename
-        // have to normalize filename - since Jmol and jalview do
-        // filename
-        // translation differently.
-        StructureData filedat = oldFiles.get(new File(oldfilenam));
-        if (filedat == null)
-        {
-          String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\");
-          filedat = oldFiles.get(new File(reformatedOldFilename));
-        }
-        newFileLoc.append(Platform.escapeString(filedat.getFilePath()));
-        pdbfilenames.add(filedat.getFilePath());
-        pdbids.add(filedat.getPdbId());
-        seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0]));
-        newFileLoc.append("\"");
-        cp = ecp + 1; // advance beyond last \" and set cursor so we can
-                      // look for next file statement.
-      } while ((ncp = state.indexOf("/*file*/", cp)) > -1);
-    }
-    if (cp > 0)
-    {
-      // just append rest of state
-      newFileLoc.append(state.substring(cp));
-    }
-    else
-    {
-      System.err.print("Ignoring incomplete Jmol state for PDB ids: ");
-      newFileLoc = new StringBuilder(state);
-      newFileLoc.append("; load append ");
-      for (File id : oldFiles.keySet())
-      {
-        // add this and any other pdb files that should be present in
-        // the viewer
-        StructureData filedat = oldFiles.get(id);
-        newFileLoc.append(filedat.getFilePath());
-        pdbfilenames.add(filedat.getFilePath());
-        pdbids.add(filedat.getPdbId());
-        seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0]));
-        newFileLoc.append(" \"");
-        newFileLoc.append(filedat.getFilePath());
-        newFileLoc.append("\"");
-
-      }
-      newFileLoc.append(";");
-    }
-
-    if (newFileLoc.length() == 0)
-    {
-      return;
-    }
-    int histbug = newFileLoc.indexOf("history = ");
-    if (histbug > -1)
-    {
-      /*
-       * change "history = [true|false];" to "history = [1|0];"
-       */
-      histbug += 10;
-      int diff = histbug == -1 ? -1 : newFileLoc.indexOf(";", histbug);
-      String val = (diff == -1) ? null
-              : newFileLoc.substring(histbug, diff);
-      if (val != null && val.length() >= 4)
-      {
-        if (val.contains("e")) // eh? what can it be?
-        {
-          if (val.trim().equals("true"))
-          {
-            val = "1";
-          }
-          else
-          {
-            val = "0";
-          }
-          newFileLoc.replace(histbug, diff, val);
-        }
-      }
-    }
-
-    final String[] pdbf = pdbfilenames
-            .toArray(new String[pdbfilenames.size()]);
-    final String[] id = pdbids.toArray(new String[pdbids.size()]);
-    final SequenceI[][] sq = seqmaps
-            .toArray(new SequenceI[seqmaps.size()][]);
-    final String fileloc = newFileLoc.toString();
-    final String sviewid = viewerData.getKey();
-    final AlignFrame alf = af;
-    final Rectangle rect = new Rectangle(svattrib.getX(), svattrib.getY(),
-            svattrib.getWidth(), svattrib.getHeight());
+    String type = stateData.getType();
     try
     {
-      javax.swing.SwingUtilities.invokeAndWait(new Runnable()
-      {
-        @Override
-        public void run()
-        {
-          JalviewStructureDisplayI sview = null;
-          try
-          {
-            sview = new StructureViewer(
-                    alf.alignPanel.getStructureSelectionManager())
-                            .createView(StructureViewer.ViewerType.JMOL,
-                                    pdbf, id, sq, alf.alignPanel, svattrib,
-                                    fileloc, rect, sviewid);
-            addNewStructureViewer(sview);
-          } catch (OutOfMemoryError ex)
-          {
-            new OOMWarning("restoring structure view for PDB id " + id,
-                    (OutOfMemoryError) ex.getCause());
-            if (sview != null && sview.isVisible())
-            {
-              sview.closeViewer(false);
-              sview.setVisible(false);
-              sview.dispose();
-            }
-          }
-        }
-      });
-    } catch (InvocationTargetException ex)
+      ViewerType viewerType = ViewerType.valueOf(type);
+      createStructureViewer(viewerType, viewerData, af, jprovider);
+    } catch (IllegalArgumentException | NullPointerException e)
     {
-      warn("Unexpected error when opening Jmol view.", ex);
-
-    } catch (InterruptedException e)
-    {
-      // e.printStackTrace();
+      // TODO JAL-3619 show error dialog / offer an alternative viewer
+      Cache.log.error("Invalid structure viewer type: " + type);
     }
-
   }
 
   /**
@@ -4861,23 +4621,23 @@ public class Jalview2XML
   }
 
   AlignFrame loadViewport(String file, List<JSeq> JSEQ,
-          List<SequenceI> hiddenSeqs, AlignmentI al,
-          JalviewModel jm, Viewport view, String uniqueSeqSetId,
-          String viewId, List<JvAnnotRow> autoAlan)
+          List<SequenceI> hiddenSeqs, AlignmentI al, JalviewModel jm,
+          Viewport view, String uniqueSeqSetId, String viewId,
+          List<JvAnnotRow> autoAlan)
   {
     AlignFrame af = null;
     af = new AlignFrame(al, safeInt(view.getWidth()),
-            safeInt(view.getHeight()), uniqueSeqSetId, viewId) 
-//    {
-//     
-//     @Override
-//     protected void processKeyEvent(java.awt.event.KeyEvent e) {
-//             System.out.println("Jalview2XML   AF " + e);
-//             super.processKeyEvent(e);
-//             
-//     }
-//     
-//    }
+            safeInt(view.getHeight()), uniqueSeqSetId, viewId)
+    // {
+    //
+    // @Override
+    // protected void processKeyEvent(java.awt.event.KeyEvent e) {
+    // System.out.println("Jalview2XML AF " + e);
+    // super.processKeyEvent(e);
+    //
+    // }
+    //
+    // }
     ;
 
     af.setFileName(file, FileFormat.Jalview);
@@ -4955,9 +4715,8 @@ public class Jalview2XML
 
     viewport.setColourText(safeBoolean(view.isShowColourText()));
 
-    viewport
-            .setConservationSelected(
-                    safeBoolean(view.isConservationSelected()));
+    viewport.setConservationSelected(
+            safeBoolean(view.isConservationSelected()));
     viewport.setIncrement(safeInt(view.getConsThreshold()));
     viewport.setShowJVSuffix(safeBoolean(view.isShowFullId()));
     viewport.setRightAlignIds(safeBoolean(view.isRightAlignIds()));
@@ -5033,9 +4792,8 @@ public class Jalview2XML
     af.changeColour(cs);
     viewport.setColourAppliesToAllGroups(true);
 
-    viewport
-            .setShowSequenceFeatures(
-                    safeBoolean(view.isShowSequenceFeatures()));
+    viewport.setShowSequenceFeatures(
+            safeBoolean(view.isShowSequenceFeatures()));
 
     viewport.setCentreColumnLabels(view.isCentreColumnLabels());
     viewport.setIgnoreGapsConsensus(view.isIgnoreGapsinConsensus(), null);
@@ -5048,21 +4806,24 @@ public class Jalview2XML
     viewport.setShowNPFeats(safeBoolean(view.isShowNPfeatureTooltip()));
     viewport.setShowGroupConsensus(view.isShowGroupConsensus());
     viewport.setShowGroupConservation(view.isShowGroupConservation());
+    viewport.setShowComplementFeatures(view.isShowComplementFeatures());
+    viewport.setShowComplementFeaturesOnTop(
+            view.isShowComplementFeaturesOnTop());
 
     // recover feature settings
     if (jm.getFeatureSettings() != null)
     {
-      FeatureRenderer fr = af.alignPanel.getSeqPanel().seqCanvas
+      FeatureRendererModel fr = af.alignPanel.getSeqPanel().seqCanvas
               .getFeatureRenderer();
       FeaturesDisplayed fdi;
       viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
-      String[] renderOrder = new String[jm.getFeatureSettings()
-              .getSetting().size()];
+      String[] renderOrder = new String[jm.getFeatureSettings().getSetting()
+              .size()];
       Map<String, FeatureColourI> featureColours = new Hashtable<>();
       Map<String, Float> featureOrder = new Hashtable<>();
 
-      for (int fs = 0; fs < jm.getFeatureSettings()
-              .getSetting().size(); fs++)
+      for (int fs = 0; fs < jm.getFeatureSettings().getSetting()
+              .size(); fs++)
       {
         Setting setting = jm.getFeatureSettings().getSetting().get(fs);
         String featureType = setting.getType();
@@ -5074,8 +4835,8 @@ public class Jalview2XML
                 .getMatcherSet();
         if (filters != null)
         {
-          FeatureMatcherSetI filter = Jalview2XML
-                  .parseFilter(featureType, filters);
+          FeatureMatcherSetI filter = Jalview2XML.parseFilter(featureType,
+                  filters);
           if (!filter.isEmpty())
           {
             fr.setFeatureFilter(featureType, filter);
@@ -5107,8 +4868,7 @@ public class Jalview2XML
           float max = setting.getMax() == null ? 1f
                   : setting.getMax().floatValue();
           FeatureColourI gc = new FeatureColour(maxColour, minColour,
-                  maxColour,
-                  noValueColour, min, max);
+                  maxColour, noValueColour, min, max);
           if (setting.getAttributeName().size() > 0)
           {
             gc.setAttributeName(setting.getAttributeName().toArray(
@@ -5142,8 +4902,7 @@ public class Jalview2XML
         }
         else
         {
-          featureColours.put(featureType,
-                  new FeatureColour(maxColour));
+          featureColours.put(featureType, new FeatureColour(maxColour));
         }
         renderOrder[fs] = featureType;
         if (setting.getOrder() != null)
@@ -5152,7 +4911,7 @@ public class Jalview2XML
         }
         else
         {
-          featureOrder.put(featureType, new Float(
+          featureOrder.put(featureType, Float.valueOf(
                   fs / jm.getFeatureSettings().getSetting().size()));
         }
         if (safeBoolean(setting.isDisplay()))
@@ -5164,7 +4923,7 @@ public class Jalview2XML
       for (int gs = 0; gs < jm.getFeatureSettings().getGroup().size(); gs++)
       {
         Group grp = jm.getFeatureSettings().getGroup().get(gs);
-        fgtable.put(grp.getName(), new Boolean(grp.isDisplay()));
+        fgtable.put(grp.getName(), Boolean.valueOf(grp.isDisplay()));
       }
       // FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
       // fgtable, featureColours, jms.getFeatureSettings().hasTransparency() ?
@@ -5516,7 +5275,7 @@ public class Jalview2XML
         addDatasetRef(vamsasSet.getDatasetId(), ds);
       }
     }
-    Vector dseqs = null;
+    Vector<SequenceI> dseqs = null;
     if (!ignoreUnrefed)
     {
       // recovering an alignment View
@@ -5544,7 +5303,7 @@ public class Jalview2XML
       // try even harder to restore dataset
       AlignmentI xtantDS = checkIfHasDataset(vamsasSet.getSequence());
       // create a list of new dataset sequences
-      dseqs = new Vector();
+      dseqs = new Vector<>();
     }
     for (int i = 0, iSize = vamsasSet.getSequence().size(); i < iSize; i++)
     {
@@ -5616,6 +5375,7 @@ public class Jalview2XML
       }
     }
   }
+
   /**
    * 
    * @param vamsasSeq
@@ -5632,7 +5392,8 @@ public class Jalview2XML
    *          vamsasSeq array ordering, to preserve ordering of dataset
    */
   private void ensureJalviewDatasetSequence(Sequence vamsasSeq,
-          AlignmentI ds, Vector dseqs, boolean ignoreUnrefed, int vseqpos)
+          AlignmentI ds, Vector<SequenceI> dseqs, boolean ignoreUnrefed,
+          int vseqpos)
   {
     // JBP TODO: Check this is called for AlCodonFrames to support recovery of
     // xRef Codon Maps
@@ -5837,17 +5598,34 @@ public class Jalview2XML
     return datasetId;
   }
 
+  /**
+   * Add any saved DBRefEntry's to the sequence. An entry flagged as 'locus' is
+   * constructed as a special subclass GeneLocus.
+   * 
+   * @param datasetSequence
+   * @param sequence
+   */
   private void addDBRefs(SequenceI datasetSequence, Sequence sequence)
   {
     for (int d = 0; d < sequence.getDBRef().size(); d++)
     {
       DBRef dr = sequence.getDBRef().get(d);
-      jalview.datamodel.DBRefEntry entry = new jalview.datamodel.DBRefEntry(
-              dr.getSource(), dr.getVersion(), dr.getAccessionId());
+      DBRefEntry entry;
+      if (dr.isLocus())
+      {
+        entry = new GeneLocus(dr.getSource(), dr.getVersion(),
+                dr.getAccessionId());
+      }
+      else
+      {
+        entry = new DBRefEntry(dr.getSource(), dr.getVersion(),
+                dr.getAccessionId());
+      }
       if (dr.getMapping() != null)
       {
         entry.setMap(addMapping(dr.getMapping()));
       }
+      entry.setCanonical(dr.isCanonical());
       datasetSequence.addDBRef(entry);
     }
   }
@@ -6340,14 +6118,182 @@ public class Jalview2XML
   }
 
   /**
+   * Creates a new structure viewer window
+   * 
+   * @param viewerType
+   * @param viewerData
+   * @param af
+   * @param jprovider
+   */
+  protected void createStructureViewer(ViewerType viewerType,
+          final Entry<String, StructureViewerModel> viewerData,
+          AlignFrame af, jarInputStreamProvider jprovider)
+  {
+    final StructureViewerModel viewerModel = viewerData.getValue();
+    String sessionFilePath = null;
+
+    if (viewerType == ViewerType.JMOL)
+    {
+      sessionFilePath = rewriteJmolSession(viewerModel, jprovider);
+    }
+    else
+    {
+      String viewerJarEntryName = getViewerJarEntryName(
+              viewerModel.getViewId());
+      sessionFilePath = copyJarEntry(jprovider, viewerJarEntryName,
+              "viewerSession", ".tmp");
+    }
+    final String sessionPath = sessionFilePath;
+    final String sviewid = viewerData.getKey();
+    try
+    {
+      SwingUtilities.invokeAndWait(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+          JalviewStructureDisplayI sview = null;
+          try
+          {
+            sview = StructureViewer.createView(viewerType, af.alignPanel,
+                    viewerModel, sessionPath, sviewid);
+            addNewStructureViewer(sview);
+          } catch (OutOfMemoryError ex)
+          {
+            new OOMWarning("Restoring structure view for " + viewerType,
+                    (OutOfMemoryError) ex.getCause());
+            if (sview != null && sview.isVisible())
+            {
+              sview.closeViewer(false);
+              sview.setVisible(false);
+              sview.dispose();
+            }
+          }
+        }
+      });
+    } catch (InvocationTargetException | InterruptedException ex)
+    {
+      warn("Unexpected error when opening " + viewerType
+              + " structure viewer", ex);
+    }
+  }
+
+  /**
+   * Rewrites a Jmol session script, saves it to a temporary file, and returns
+   * the path of the file. "load file" commands are rewritten to change the
+   * original PDB file names to those created as the Jalview project is loaded.
+   * 
+   * @param svattrib
+   * @param jprovider
+   * @return
+   */
+  private String rewriteJmolSession(StructureViewerModel svattrib,
+          jarInputStreamProvider jprovider)
+  {
+    String state = svattrib.getStateData(); // Jalview < 2.9
+    if (state == null || state.isEmpty()) // Jalview >= 2.9
+    {
+      String jarEntryName = getViewerJarEntryName(svattrib.getViewId());
+      state = readJarEntry(jprovider, jarEntryName);
+    }
+    // TODO or simpler? for each key in oldFiles,
+    // replace key.getPath() in state with oldFiles.get(key).getFilePath()
+    // (allowing for different path escapings)
+    StringBuilder rewritten = new StringBuilder(state.length());
+    int cp = 0, ncp, ecp;
+    Map<File, StructureData> oldFiles = svattrib.getFileData();
+    while ((ncp = state.indexOf("load ", cp)) > -1)
+    {
+      do
+      {
+        // look for next filename in load statement
+        rewritten.append(state.substring(cp,
+                ncp = (state.indexOf("\"", ncp + 1) + 1)));
+        String oldfilenam = state.substring(ncp,
+                ecp = state.indexOf("\"", ncp));
+        // recover the new mapping data for this old filename
+        // have to normalize filename - since Jmol and jalview do
+        // filename translation differently.
+        StructureData filedat = oldFiles.get(new File(oldfilenam));
+        if (filedat == null)
+        {
+          String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\");
+          filedat = oldFiles.get(new File(reformatedOldFilename));
+        }
+        rewritten.append(Platform.escapeBackslashes(filedat.getFilePath()));
+        rewritten.append("\"");
+        cp = ecp + 1; // advance beyond last \" and set cursor so we can
+                      // look for next file statement.
+      } while ((ncp = state.indexOf("/*file*/", cp)) > -1);
+    }
+    if (cp > 0)
+    {
+      // just append rest of state
+      rewritten.append(state.substring(cp));
+    }
+    else
+    {
+      System.err.print("Ignoring incomplete Jmol state for PDB ids: ");
+      rewritten = new StringBuilder(state);
+      rewritten.append("; load append ");
+      for (File id : oldFiles.keySet())
+      {
+        // add pdb files that should be present in the viewer
+        StructureData filedat = oldFiles.get(id);
+        rewritten.append(" \"").append(filedat.getFilePath()).append("\"");
+      }
+      rewritten.append(";");
+    }
+
+    if (rewritten.length() == 0)
+    {
+      return null;
+    }
+    final String history = "history = ";
+    int historyIndex = rewritten.indexOf(history);
+    if (historyIndex > -1)
+    {
+      /*
+       * change "history = [true|false];" to "history = [1|0];"
+       */
+      historyIndex += history.length();
+      String val = rewritten.substring(historyIndex, historyIndex + 5);
+      if (val.startsWith("true"))
+      {
+        rewritten.replace(historyIndex, historyIndex + 4, "1");
+      }
+      else if (val.startsWith("false"))
+      {
+        rewritten.replace(historyIndex, historyIndex + 5, "0");
+      }
+    }
+
+    try
+    {
+      File tmp = File.createTempFile("viewerSession", ".tmp");
+      try (OutputStream os = new FileOutputStream(tmp))
+      {
+        InputStream is = new ByteArrayInputStream(
+                rewritten.toString().getBytes());
+        copyAll(is, os);
+        return tmp.getAbsolutePath();
+      }
+    } catch (IOException e)
+    {
+      Cache.log.error("Error restoring Jmol session: " + e.toString());
+    }
+    return null;
+  }
+
+  /**
    * Populates an XML model of the feature colour scheme for one feature type
    * 
    * @param featureType
    * @param fcol
    * @return
    */
-  public static Colour marshalColour(
-          String featureType, FeatureColourI fcol)
+  public static Colour marshalColour(String featureType,
+          FeatureColourI fcol)
   {
     Colour col = new Colour();
     if (fcol.isSimpleColour())
@@ -6408,7 +6354,7 @@ public class Jalview2XML
           boolean and)
   {
     jalview.xml.binding.jalview.FeatureMatcherSet result = new jalview.xml.binding.jalview.FeatureMatcherSet();
-  
+
     if (filters.hasNext())
     {
       /*
@@ -6458,7 +6404,7 @@ public class Jalview2XML
       }
       result.setMatchCondition(matcherModel);
     }
-  
+
     return result;
   }
 
@@ -6469,8 +6415,7 @@ public class Jalview2XML
    * @param matcherSetModel
    * @return
    */
-  public static FeatureMatcherSetI parseFilter(
-          String featureType,
+  public static FeatureMatcherSetI parseFilter(String featureType,
           jalview.xml.binding.jalview.FeatureMatcherSet matcherSetModel)
   {
     FeatureMatcherSetI result = new FeatureMatcherSet();
@@ -6485,7 +6430,7 @@ public class Jalview2XML
                       featureType, e.getMessage()));
       // return as much as was parsed up to the error
     }
-  
+
     return result;
   }
 
@@ -6500,8 +6445,7 @@ public class Jalview2XML
    * @throws IllegalStateException
    *           if AND and OR conditions are mixed
    */
-  protected static void parseFilterConditions(
-          FeatureMatcherSetI matcherSet,
+  protected static void parseFilterConditions(FeatureMatcherSetI matcherSet,
           jalview.xml.binding.jalview.FeatureMatcherSet matcherSetModel,
           boolean and)
   {
@@ -6523,7 +6467,7 @@ public class Jalview2XML
       else if (filterBy == FilterBy.BY_SCORE)
       {
         matchCondition = FeatureMatcher.byScore(cond, pattern);
-  
+
       }
       else if (filterBy == FilterBy.BY_ATTRIBUTE)
       {
@@ -6533,7 +6477,7 @@ public class Jalview2XML
         matchCondition = FeatureMatcher.byAttribute(cond, pattern,
                 attNames);
       }
-  
+
       /*
        * note this throws IllegalStateException if AND-ing to a 
        * previously OR-ed compound condition, or vice versa
@@ -6576,13 +6520,13 @@ public class Jalview2XML
   public static FeatureColourI parseColour(Colour colourModel)
   {
     FeatureColourI colour = null;
-  
+
     if (colourModel.getMax() != null)
     {
       Color mincol = null;
       Color maxcol = null;
       Color noValueColour = null;
-  
+
       try
       {
         mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
@@ -6591,7 +6535,7 @@ public class Jalview2XML
       {
         Cache.log.warn("Couldn't parse out graduated feature color.", e);
       }
-  
+
       NoValueColour noCol = colourModel.getNoValueColour();
       if (noCol == NoValueColour.MIN)
       {
@@ -6601,7 +6545,7 @@ public class Jalview2XML
       {
         noValueColour = maxcol;
       }
-  
+
       colour = new FeatureColour(maxcol, mincol, maxcol, noValueColour,
               safeFloat(colourModel.getMin()),
               safeFloat(colourModel.getMax()));
@@ -6640,7 +6584,7 @@ public class Jalview2XML
       Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
       colour = new FeatureColour(color);
     }
-  
+
     return colour;
   }
 }