/**
  * Retrieves all Proteins, cross references and matches for a range
  *
  * @param bottom range lower bound (included)
  * @param top range upper bound (included)
  * @return The Protein, with matches loaded. (matches are LAZY by default) or null if the primary
  *     key is not present in the database.
  */
 @Override
 @Transactional(readOnly = true)
 @SuppressWarnings("unchecked")
 public List<Protein> getProteinsAndMatchesAndCrossReferencesBetweenIds(long bottom, long top) {
   Query query =
       entityManager.createQuery(
           "select distinct p from Protein p "
               + "left outer join fetch p.matches "
               + "left outer join fetch p.crossReferences where p.id >= :bottom and p.id <= :top");
   query.setParameter("bottom", bottom);
   query.setParameter("top", top);
   List<Protein> matchingProteins = (List<Protein>) query.getResultList();
   if (LOGGER.isTraceEnabled()) {
     LOGGER.trace("Querying proteins with IDs in range: " + bottom + " to " + top);
     LOGGER.trace("Matching protein count: " + matchingProteins.size());
     for (Protein protein : matchingProteins) {
       LOGGER.trace("Protein ID: " + protein.getId() + " MD5: " + protein.getMd5());
       LOGGER.trace("Has " + protein.getMatches().size() + " matches");
       for (ProteinXref xref : protein.getCrossReferences()) {
         LOGGER.trace("Xref: " + xref.getIdentifier());
       }
     }
   }
   return matchingProteins;
 }
  /**
   * Writes out protein view to an zipped and compressed HTML file.
   *
   * @param protein containing matches to be written out
   * @return the number of rows printed (i.e. the number of Locations on Matches).
   * @throws java.io.IOException in the event of I/O problem writing out the file.
   */
  public int write(final Protein protein) throws IOException {
    checkEntryHierarchy();

    if (entryHierarchy != null) {
      for (ProteinXref xref : protein.getCrossReferences()) {
        final SimpleProtein simpleProtein = SimpleProtein.valueOf(protein, xref, entryHierarchy);
        if (simpleProtein != null) {
          // Build model for FreeMarker
          final SimpleHash model = buildModelMap(simpleProtein, entryHierarchy);
          // Render template and write result to a file
          Writer writer = null;
          try {
            final Template temp = freeMarkerConfig.getTemplate(freeMarkerTemplate);
            checkTempDirectory(tempDirectory);
            if (!tempDirectory.endsWith("/")) {
              tempDirectory = tempDirectory + "/";
            }

            UrlFriendlyIdGenerator gen = UrlFriendlyIdGenerator.getInstance();
            String urlFriendlyId = gen.generate(xref.getIdentifier());
            final File newResultFile = new File(tempDirectory + urlFriendlyId + ".svg");
            resultFiles.add(newResultFile);
            writer = new PrintWriter(new FileWriter(newResultFile));
            temp.process(model, writer);
            writer.flush();
          } catch (TemplateException e) {
            e.printStackTrace();
          } catch (IOException e) {
            e.printStackTrace();
          } finally {
            if (writer != null) {
              writer.close();
            }
          }
        }
      }
    }
    return 0;
  }
  /**
   * Inserts new Proteins. If there are Protein objects with the same MD5 / sequence in the
   * database, this method updates these proteins, rather than inserting the new ones.
   *
   * <p>Note that this method inserts the new Protein objects AND and new Xrefs (possibly updating
   * an existing Protein object if necessary with the new Xref.)
   *
   * @param newProteins being a List of new Protein objects to insert
   * @return a new List<Protein> containing all of the inserted / updated Protein objects. (Allows
   *     the caller to retrieve the primary keys for the proteins).
   */
  @Transactional
  @SuppressWarnings("unchecked")
  public PersistedProteins insertNewProteins(Collection<Protein> newProteins) {
    PersistedProteins persistentProteins = new PersistedProteins();
    if (newProteins.size() > 0) {
      // Create a List of MD5s (just as Strings) to query the database with
      final List<String> newMd5s = new ArrayList<String>(newProteins.size());
      for (Protein newProtein : newProteins) {
        newMd5s.add(newProtein.getMd5());
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("MD5 of new protein: " + newProtein.getMd5());
        }
      }
      // Retrieve any proteins AND associated xrefs that have the same MD5 as one of the 'new'
      // proteins
      // being inserted and place in a Map of MD5 to Protein object.
      final Map<String, Protein> md5ToExistingProtein = new HashMap<String, Protein>();
      final Query query =
          entityManager.createQuery(
              "select p from Protein p left outer join fetch P.crossReferences where p.md5 in (:md5)");
      query.setParameter("md5", newMd5s);
      for (Protein existingProtein : (List<Protein>) query.getResultList()) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Found 1 existing protein with MD5: " + existingProtein.getMd5());
        }
        md5ToExistingProtein.put(existingProtein.getMd5(), existingProtein);
      }

      // Now have the List of 'new' proteins, and a list of existing proteins that match
      // them. Insert / update proteins as appropriate.
      for (Protein candidate : newProteins) {

        // PROTEIN ALREADY EXISTS in the DB. - update cross references and save.
        if (md5ToExistingProtein.keySet().contains(candidate.getMd5())) {
          // This protein is already in the database - add any new Xrefs and update.
          Protein existingProtein = md5ToExistingProtein.get(candidate.getMd5());
          boolean updateRequired = false;
          if (candidate.getCrossReferences() != null) {
            if (LOGGER.isTraceEnabled()) {
              LOGGER.trace("Protein TO BE STORED has xrefs:");
            }
            for (ProteinXref xref : candidate.getCrossReferences()) {
              if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(xref.getIdentifier());
              }
              // Add any NEW cross references.
              if (!existingProtein.getCrossReferences().contains(xref)) {
                if (LOGGER.isTraceEnabled()) {
                  LOGGER.trace(
                      "Adding " + xref.getIdentifier() + " and setting updateRequired = true");
                }
                existingProtein.addCrossReference(xref);
                updateRequired = true;
              }
            }
          }
          if (updateRequired) {
            // PROTEIN is NOT new, but CHANGED (new Xrefs)
            if (LOGGER.isTraceEnabled()) {
              LOGGER.trace("Merging protein with new Xrefs: " + existingProtein.getMd5());
            }
            entityManager.merge(existingProtein);
          }
          persistentProteins.addPreExistingProtein(existingProtein);
        }
        // PROTEIN IS NEW - save it.
        else {
          if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Saving new protein: " + candidate.getMd5());
          }
          entityManager.persist(candidate);
          persistentProteins.addNewProtein(candidate);
          // Check for this new protein next time through the loop, just in case the new source of
          // proteins is redundant (e.g. a FASTA file with sequences repeated).
          md5ToExistingProtein.put(candidate.getMd5(), candidate);
        }
      }
    }
    // Finally return all the persisted Protein objects (new or existing)
    entityManager.flush();
    return persistentProteins;
  }