@Test
  public void reformatEntryIfAskedToDoSo() throws Exception {
    BibEntry entry = new BibEntry();
    entry.setType(BibtexEntryTypes.ARTICLE);
    entry.setField("author", "Mr. author");
    entry.setParsedSerialization("wrong serialization");
    entry.setChanged(false);
    database.insertEntry(entry);

    SavePreferences preferences = new SavePreferences().withReformatFile(true);
    StringSaveSession session =
        databaseWriter.savePartOfDatabase(
            bibtexContext, Collections.singletonList(entry), preferences);

    assertEquals(
        OS.NEWLINE
            + "@Article{,"
            + OS.NEWLINE
            + "  author = {Mr. author},"
            + OS.NEWLINE
            + "}"
            + OS.NEWLINE
            + OS.NEWLINE
            + "@Comment{jabref-meta: databaseType:bibtex;}"
            + OS.NEWLINE,
        session.getStringValue());
  }
Beispiel #2
0
  /**
   * Checks if the two entries represent the same publication.
   *
   * @param one BibEntry
   * @param two BibEntry
   * @return boolean
   */
  public static boolean isDuplicate(BibEntry one, BibEntry two, BibDatabaseMode bibDatabaseMode) {

    // First check if they are of the same type - a necessary condition:
    if (!one.getType().equals(two.getType())) {
      return false;
    }
    EntryType type = EntryTypes.getTypeOrDefault(one.getType(), bibDatabaseMode);

    // The check if they have the same required fields:
    java.util.List<String> var = type.getRequiredFieldsFlat();
    String[] fields = var.toArray(new String[var.size()]);
    double[] req;
    if (fields == null) {
      req = new double[] {0., 0.};
    } else {
      req = DuplicateCheck.compareFieldSet(fields, one, two);
    }

    if (Math.abs(req[0] - DuplicateCheck.duplicateThreshold) > DuplicateCheck.DOUBT_RANGE) {
      // Far from the threshold value, so we base our decision on the req. fields only
      return req[0] >= DuplicateCheck.duplicateThreshold;
    }
    // Close to the threshold value, so we take a look at the optional fields, if any:
    java.util.List<String> optionalFields = type.getOptionalFields();
    fields = optionalFields.toArray(new String[optionalFields.size()]);
    if (fields != null) {
      double[] opt = DuplicateCheck.compareFieldSet(fields, one, two);
      double totValue =
          ((DuplicateCheck.REQUIRED_WEIGHT * req[0] * req[1]) + (opt[0] * opt[1]))
              / ((req[1] * DuplicateCheck.REQUIRED_WEIGHT) + opt[1]);
      return totValue >= DuplicateCheck.duplicateThreshold;
    }
    return req[0] >= DuplicateCheck.duplicateThreshold;
  }
  @Test
  public void writeEntryWithCustomizedTypeAlsoWritesTypeDeclaration() throws Exception {
    try {
      EntryTypes.addOrModifyCustomEntryType(
          new CustomEntryType("customizedType", "required", "optional"), BibDatabaseMode.BIBTEX);
      BibEntry entry = new BibEntry();
      entry.setType("customizedType");
      database.insertEntry(entry);

      StringSaveSession session =
          databaseWriter.savePartOfDatabase(
              bibtexContext, Collections.singletonList(entry), new SavePreferences());

      assertEquals(
          OS.NEWLINE
              + "@Customizedtype{,"
              + OS.NEWLINE
              + "}"
              + OS.NEWLINE
              + OS.NEWLINE
              + "@Comment{jabref-meta: databaseType:bibtex;}"
              + OS.NEWLINE
              + OS.NEWLINE
              + "@Comment{jabref-entrytype: Customizedtype: req[required] opt[optional]}"
              + OS.NEWLINE,
          session.getStringValue());
    } finally {
      EntryTypes.removeAllCustomEntryTypes();
    }
  }
  /**
   * Unabbreviate the journal name of the given entry.
   *
   * @param entry The entry to be treated.
   * @param fieldName The field name (e.g. "journal")
   * @param ce If the entry is changed, add an edit to this compound.
   * @return true if the entry was changed, false otherwise.
   */
  public boolean unabbreviate(
      BibDatabase database, BibEntry entry, String fieldName, CompoundEdit ce) {
    if (!entry.hasField(fieldName)) {
      return false;
    }
    String text = entry.getFieldOptional(fieldName).get();
    String origText = text;
    if (database != null) {
      text = database.resolveForStrings(text);
    }

    if (!journalAbbreviationRepository.isKnownName(text)) {
      return false; // cannot do anything if it is not known
    }

    if (!journalAbbreviationRepository.isAbbreviatedName(text)) {
      return false; // cannot unabbreviate unabbreviated name.
    }

    Abbreviation abbreviation =
        journalAbbreviationRepository.getAbbreviation(text).get(); // must be here
    String newText = abbreviation.getName();
    entry.setField(fieldName, newText);
    ce.addEdit(new UndoableFieldChange(entry, fieldName, origText, newText));
    return true;
  }
  @Test
  public void monthFieldSpecialSyntax() throws IOException {
    // @formatter:off
    String bibtexEntry =
        "@Article{test,"
            + OS.NEWLINE
            + "  Author                   = {Foo Bar},"
            + OS.NEWLINE
            + "  Month                    = mar,"
            + OS.NEWLINE
            + "  Number                   = {1}"
            + OS.NEWLINE
            + "}";
    // @formatter:on

    // read in bibtex string
    ParserResult result = BibtexParser.parse(new StringReader(bibtexEntry));
    Collection<BibEntry> entries = result.getDatabase().getEntries();
    BibEntry entry = entries.iterator().next();

    // modify month field
    Set<String> fields = entry.getFieldNames();
    assertTrue(fields.contains("month"));
    assertEquals("#mar#", entry.getFieldOptional("month").get());

    // write out bibtex string
    StringWriter stringWriter = new StringWriter();
    writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX);
    String actual = stringWriter.toString();

    assertEquals(bibtexEntry, actual);
  }
  @Test
  public void addSingleGroupToNonemptyBibEntryAppendsToGroupsField() {
    entry.setField(FieldName.GROUPS, "some thing");
    group.add(entry);

    assertEquals(Optional.of("some thing, myExplicitGroup"), entry.getField(FieldName.GROUPS));
  }
  @Test
  public void roundtripWithUserCommentAndEntryChange() throws Exception {
    Path testBibtexFile = Paths.get("src/test/resources/testbib/bibWithUserComments.bib");
    Charset encoding = StandardCharsets.UTF_8;
    ParserResult result =
        new BibtexParser(importFormatPreferences)
            .parse(Importer.getReader(testBibtexFile, encoding));

    BibEntry entry = result.getDatabase().getEntryByKey("1137631").get();
    entry.setField("author", "Mr. Author");

    SavePreferences preferences =
        new SavePreferences().withEncoding(encoding).withSaveInOriginalOrder(true);
    BibDatabaseContext context =
        new BibDatabaseContext(
            result.getDatabase(), result.getMetaData(), new Defaults(BibDatabaseMode.BIBTEX));

    StringSaveSession session =
        databaseWriter.savePartOfDatabase(context, result.getDatabase().getEntries(), preferences);

    try (Scanner scanner =
        new Scanner(
            Paths.get("src/test/resources/testbib/bibWithUserCommentAndEntryChange.bib"),
            encoding.name())) {
      assertEquals(scanner.useDelimiter("\\A").next(), session.getStringValue());
    }
  }
  @Test
  public void addDuplicateGroupDoesNotChangeGroupsField() throws Exception {
    entry.setField(FieldName.GROUPS, "myExplicitGroup");
    group.add(entry);

    assertEquals(Optional.of("myExplicitGroup"), entry.getField(FieldName.GROUPS));
  }
Beispiel #9
0
 private boolean isAnyFieldSetForSelectedEntry(List<String> fieldnames) {
   if (panel.getMainTable().getSelectedRowCount() == 1) {
     BibEntry entry = panel.getMainTable().getSelected().get(0);
     return !Collections.disjoint(fieldnames, entry.getFieldNames());
   }
   return false;
 }
Beispiel #10
0
  private void index() {

    /*  Old version, from when set was a TreeSet.

    // The boolean "changing" is true in the situation that an entry is about to change,
    // and has temporarily been removed from the entry set in this sorter. So, if we index
    // now, we will cause exceptions other places because one entry has been left out of
    // the indexed array. Simply waiting foth this to change can lead to deadlocks,
    // so we have no other choice than to return without indexing.
    if (changing)
        return;
    */

    synchronized (set) {

      // Resort if necessary:
      if (changed) {
        Collections.sort(set, comp);
        changed = false;
      }

      // Create an array of IDs for quick access, since getIdAt() is called by
      // getValueAt() in EntryTableModel, which *has* to be efficient.

      int count = set.size();
      idArray = new String[count];
      entryArray = new BibEntry[count];
      int piv = 0;
      for (BibEntry entry : set) {
        idArray[piv] = entry.getId();
        entryArray[piv] = entry;
        piv++;
      }
    }
  }
Beispiel #11
0
  private void parseField(BibEntry entry) throws IOException {
    String key = parseTextToken().toLowerCase();

    skipWhitespace();
    consume('=');
    String content = parseFieldContent(key);
    if (!content.isEmpty()) {
      if (entry.hasField(key)) {
        // The following hack enables the parser to deal with multiple
        // author or
        // editor lines, stringing them together instead of getting just
        // one of them.
        // Multiple author or editor lines are not allowed by the bibtex
        // format, but
        // at least one online database exports bibtex like that, making
        // it inconvenient
        // for users if JabRef didn't accept it.
        if (InternalBibtexFields.getFieldExtras(key).contains(FieldProperties.PERSON_NAMES)) {
          entry.setField(key, entry.getFieldOptional(key).get() + " and " + content);
        } else if (FieldName.KEYWORDS.equals(key)) {
          // multiple keywords fields should be combined to one
          entry.addKeyword(content, Globals.prefs.get(JabRefPreferences.KEYWORD_SEPARATOR));
        }
      } else {
        entry.setField(key, content);
      }
    }
  }
  /**
   * Generates a Citation based on the given entry, style, and output format WARNING: the citation
   * is generated with JavaScript which may take some time, better call it in outside the main
   * Thread
   */
  protected static String generateCitation(
      BibEntry entry, String style, CitationStyleOutputFormat outputFormat) {
    try {
      String citeKey = entry.getCiteKeyOptional().orElse("");
      BibTeXEntry bibTeXEntry = new BibTeXEntry(new Key(entry.getType()), new Key(citeKey));
      for (Map.Entry<String, String> field : entry.getFieldMap().entrySet()) {
        String value = UNICODE_TO_LATEX_FORMATTER.format(field.getValue());
        bibTeXEntry.addField(new Key(field.getKey()), new DigitStringValue(value));
      }

      CSLItemData cslItemData = BIBTEX_CONVERTER.toItemData(bibTeXEntry);
      Bibliography bibliography =
          CSL.makeAdhocBibliography(style, outputFormat.getFormat(), cslItemData);
      return bibliography.getEntries()[0];

    } catch (IOException | ArrayIndexOutOfBoundsException e) {
      LOGGER.error("Could not generate BibEntry citation", e);
      return Localization.lang("Cannot generate preview based on selected citation style.");
    } catch (TokenMgrException e) {
      LOGGER.error("Bad character inside BibEntry", e);
      // sadly one cannot easily retrieve the bad char from the TokenMgrError
      return new StringBuilder()
          .append(Localization.lang("Cannot generate preview based on selected citation style."))
          .append(outputFormat == CitationStyleOutputFormat.HTML ? "<br>" : "\n")
          .append(Localization.lang("Bad character inside entry"))
          .append(outputFormat == CitationStyleOutputFormat.HTML ? "<br>" : "\n")
          .append(e.getLocalizedMessage())
          .toString();
    }
  }
Beispiel #13
0
 @Test
 public void testGrammarSearch() {
   BibEntry entry = new BibEntry();
   entry.addKeyword("one two", ',');
   SearchQuery searchQuery = new SearchQuery("keywords=\"one two\"", false, false);
   assertTrue(searchQuery.isMatch(entry));
 }
  @Test
  public void testSerialization() throws IOException {
    StringWriter stringWriter = new StringWriter();

    BibEntry entry = new BibEntry("1234", "article");
    // set a required field
    entry.setField("author", "Foo Bar");
    entry.setField("journal", "International Journal of Something");
    // set an optional field
    entry.setField("number", "1");
    entry.setField("note", "some note");

    writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX);

    String actual = stringWriter.toString();

    // @formatter:off
    String expected =
        OS.NEWLINE
            + "@Article{,"
            + OS.NEWLINE
            + "  author  = {Foo Bar},"
            + OS.NEWLINE
            + "  journal = {International Journal of Something},"
            + OS.NEWLINE
            + "  number  = {1},"
            + OS.NEWLINE
            + "  note    = {some note},"
            + OS.NEWLINE
            + "}"
            + OS.NEWLINE;
    // @formatter:on

    assertEquals(expected, actual);
  }
Beispiel #15
0
 @Test
 public void testGrammarSearchFullEntry() {
   BibEntry entry = new BibEntry();
   entry.setField(FieldName.TITLE, "systematic review");
   SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", false, false);
   assertTrue(searchQuery.isMatch(entry));
 }
Beispiel #16
0
  @Test
  public void testSearchingForOpenBraketInBooktitle() {
    BibEntry e = new BibEntry(BibtexEntryTypes.INPROCEEDINGS.getName());
    e.setField(FieldName.BOOKTITLE, "Super Conference (SC)");

    SearchQuery searchQuery = new SearchQuery("booktitle=\"(\"", false, false);
    assertTrue(searchQuery.isMatch(e));
  }
Beispiel #17
0
  @Test
  public void testSearchMatchesSingleKeyword() {
    BibEntry e = new BibEntry(BibtexEntryTypes.INPROCEEDINGS.getName());
    e.setField("keywords", "banana, pineapple, orange");

    SearchQuery searchQuery = new SearchQuery("anykeyword==pineapple", false, false);
    assertTrue(searchQuery.isMatch(e));
  }
  @Test
  // For https://github.com/JabRef/jabref/issues/2334
  public void removeDoesNotChangeFieldIfContainsNameAsWord() throws Exception {
    entry.setField(FieldName.GROUPS, "myExplicitGroup alternative");
    group.remove(entry);

    assertEquals(Optional.of("myExplicitGroup alternative"), entry.getField(FieldName.GROUPS));
  }
Beispiel #19
0
  private static BibEntry downloadEntryBibTeX(String id, boolean downloadAbstract) {
    try {
      URL url =
          new URL(
              ACMPortalFetcher.START_URL
                  + ACMPortalFetcher.BIBTEX_URL
                  + id
                  + ACMPortalFetcher.BIBTEX_URL_END);
      URLConnection connection = url.openConnection();

      // set user-agent to avoid being blocked as a crawler
      connection.addRequestProperty(
          "User-Agent", "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0");
      Collection<BibEntry> items = null;
      try (BufferedReader in =
          new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
        items = BibtexParser.parse(in).getDatabase().getEntries();
      } catch (IOException e) {
        LOGGER.info("Download of BibTeX information from ACM Portal failed.", e);
      }
      if ((items == null) || items.isEmpty()) {
        return null;
      }
      BibEntry entry = items.iterator().next();
      Thread.sleep(
          ACMPortalFetcher.WAIT_TIME); // wait between requests or you will be blocked by ACM

      // get abstract
      if (downloadAbstract) {
        url = new URL(ACMPortalFetcher.START_URL + ACMPortalFetcher.ABSTRACT_URL + id);
        String page = Util.getResults(url);
        Matcher absM = ACMPortalFetcher.ABSTRACT_PATTERN.matcher(page);
        if (absM.find()) {
          entry.setField("abstract", absM.group(1).trim());
        }
        Thread.sleep(
            ACMPortalFetcher.WAIT_TIME); // wait between requests or you will be blocked by ACM
      }

      return entry;
    } catch (NoSuchElementException e) {
      LOGGER.info(
          "Bad Bibtex record read at: "
              + ACMPortalFetcher.BIBTEX_URL
              + id
              + ACMPortalFetcher.BIBTEX_URL_END,
          e);
      return null;
    } catch (MalformedURLException e) {
      LOGGER.info("Malformed URL.", e);
      return null;
    } catch (IOException e) {
      LOGGER.info("Cannot connect.", e);
      return null;
    } catch (InterruptedException ignored) {
      return null;
    }
  }
Beispiel #20
0
  @Test
  public void isMatchedForNormalAndFieldBasedSearchMixed() {
    BibEntry entry = new BibEntry();
    entry.setType(BibtexEntryTypes.ARTICLE);
    entry.setField("author", "asdf");
    entry.setField("abstract", "text");

    assertTrue(new SearchQuery("text AND author=asdf", true, true).isMatch(entry));
  }
Beispiel #21
0
  @Test
  public void testSearchAllFieldsNotForSpecificField() {
    BibEntry e = new BibEntry(BibtexEntryTypes.INPROCEEDINGS.getName());
    e.setField("title", "Fruity features");
    e.setField("keywords", "banana, pineapple, orange");

    SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords!=banana", false, false);
    assertFalse(searchQuery.isMatch(e));
  }
 private Set<String> getFieldContentAsWords(BibEntry entry) {
   if (onlySplitWordsAtSeparator) {
     return entry
         .getField(searchField)
         .map(content -> KeywordList.parse(content, keywordSeparator).toStringList())
         .orElse(Collections.emptySet());
   } else {
     return entry.getFieldAsWords(searchField);
   }
 }
Beispiel #23
0
  @Test
  public void testIsMatch() {
    BibEntry entry = new BibEntry();
    entry.setType(BibtexEntryTypes.ARTICLE);
    entry.setField("author", "asdf");

    assertFalse(new SearchQuery("qwer", true, true).isMatch(entry));
    assertTrue(new SearchQuery("asdf", true, true).isMatch(entry));
    assertTrue(new SearchQuery("author=asdf", true, true).isMatch(entry));
  }
 /**
  * Check the database to find out whether any of a set of fields are used for any of the entries.
  *
  * @param database The bib database.
  * @param fields The set of fields to look for.
  * @return true if at least one of the given fields is set in at least one entry, false otherwise.
  */
 private boolean linksFound(BibDatabase database, String[] fields) {
   for (BibEntry entry : database.getEntries()) {
     for (String field : fields) {
       if (entry.getField(field) != null) {
         return true;
       }
     }
   }
   return false;
 }
Beispiel #25
0
  private static int compareSingleField(String field, BibEntry one, BibEntry two) {
    String s1 = one.getField(field);
    String s2 = two.getField(field);
    if (s1 == null) {
      if (s2 == null) {
        return EMPTY_IN_BOTH;
      }
      return EMPTY_IN_ONE;
    } else if (s2 == null) {
      return EMPTY_IN_TWO;
    }

    if ("author".equals(field) || "editor".equals(field)) {
      // Specific for name fields.
      // Harmonise case:
      String auth1 =
          AuthorList.fixAuthor_lastNameOnlyCommas(s1, false).replace(" and ", " ").toLowerCase();
      String auth2 =
          AuthorList.fixAuthor_lastNameOnlyCommas(s2, false).replace(" and ", " ").toLowerCase();
      double similarity = DuplicateCheck.correlateByWords(auth1, auth2);
      if (similarity > 0.8) {
        return EQUAL;
      }
      return NOT_EQUAL;
    } else if ("pages".equals(field)) {
      // Pages can be given with a variety of delimiters, "-", "--", " - ", " -- ".
      // We do a replace to harmonize these to a simple "-":
      // After this, a simple test for equality should be enough:
      s1 = s1.replaceAll("[- ]+", "-");
      s2 = s2.replaceAll("[- ]+", "-");
      if (s1.equals(s2)) {
        return EQUAL;
      }
      return NOT_EQUAL;
    } else if ("journal".equals(field)) {
      // We do not attempt to harmonize abbreviation state of the journal names,
      // but we remove periods from the names in case they are abbreviated with
      // and without dots:
      s1 = s1.replace(".", "").toLowerCase();
      s2 = s2.replace(".", "").toLowerCase();
      double similarity = DuplicateCheck.correlateByWords(s1, s2);
      if (similarity > 0.8) {
        return EQUAL;
      }
      return NOT_EQUAL;
    } else {
      s1 = s1.toLowerCase();
      s2 = s2.toLowerCase();
      double similarity = DuplicateCheck.correlateByWords(s1, s2);
      if (similarity > 0.8) {
        return EQUAL;
      }
      return NOT_EQUAL;
    }
  }
  @Test
  public void testEntryTypeChange() throws IOException {
    // @formatter:off
    String expected =
        OS.NEWLINE
            + "@Article{test,"
            + OS.NEWLINE
            + "  author       = {BlaBla},"
            + OS.NEWLINE
            + "  journal      = {International Journal of Something},"
            + OS.NEWLINE
            + "  number       = {1},"
            + OS.NEWLINE
            + "  note         = {some note},"
            + OS.NEWLINE
            + "  howpublished = {asdf},"
            + OS.NEWLINE
            + "}"
            + OS.NEWLINE;
    // @formatter:on

    // read in bibtex string
    ParserResult result = BibtexParser.parse(new StringReader(expected));
    Collection<BibEntry> entries = result.getDatabase().getEntries();
    BibEntry entry = entries.iterator().next();

    // modify entry
    entry.setType("inproceedings");

    // write out bibtex string
    StringWriter stringWriter = new StringWriter();
    writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX);
    String actual = stringWriter.toString();

    // @formatter:off
    String expectedNewEntry =
        OS.NEWLINE
            + "@InProceedings{test,"
            + OS.NEWLINE
            + "  author       = {BlaBla},"
            + OS.NEWLINE
            + "  number       = {1},"
            + OS.NEWLINE
            + "  note         = {some note},"
            + OS.NEWLINE
            + "  howpublished = {asdf},"
            + OS.NEWLINE
            + "  journal      = {International Journal of Something},"
            + OS.NEWLINE
            + "}"
            + OS.NEWLINE;
    // @formatter:on
    assertEquals(expectedNewEntry, actual);
  }
  @Test
  public void roundTripWithCamelCasingInTheOriginalEntryAndResultInLowerCase() throws IOException {
    // @formatter:off
    String bibtexEntry =
        OS.NEWLINE
            + "@Article{test,"
            + OS.NEWLINE
            + "  Author                   = {Foo Bar},"
            + OS.NEWLINE
            + "  Journal                  = {International Journal of Something},"
            + OS.NEWLINE
            + "  Note                     = {some note},"
            + OS.NEWLINE
            + "  Number                   = {1},"
            + OS.NEWLINE
            + "  HowPublished             = {asdf},"
            + OS.NEWLINE
            + "}";
    // @formatter:on

    // read in bibtex string
    ParserResult result = BibtexParser.parse(new StringReader(bibtexEntry));
    Collection<BibEntry> entries = result.getDatabase().getEntries();
    BibEntry entry = entries.iterator().next();

    // modify entry
    entry.setField("author", "BlaBla");

    // write out bibtex string
    StringWriter stringWriter = new StringWriter();
    writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX);
    String actual = stringWriter.toString();

    // @formatter:off
    String expected =
        OS.NEWLINE
            + "@Article{test,"
            + OS.NEWLINE
            + "  author       = {BlaBla},"
            + OS.NEWLINE
            + "  journal      = {International Journal of Something},"
            + OS.NEWLINE
            + "  number       = {1},"
            + OS.NEWLINE
            + "  note         = {some note},"
            + OS.NEWLINE
            + "  howpublished = {asdf},"
            + OS.NEWLINE
            + "}"
            + OS.NEWLINE;
    // @formatter:on
    assertEquals(expected, actual);
  }
 /**
  * Will check if the current library uses any entry types from another mode. For example it will
  * warn the user if he uses entry types defined for Biblatex inside a BibTeX library.
  */
 @Override
 public List<IntegrityMessage> check(BibEntry entry) {
   if (EntryTypes.isExclusiveBibLatex(entry.getType())) {
     return Collections.singletonList(
         new IntegrityMessage(
             Localization.lang(
                 "Entry type %0 is only defined for Biblatex but not for BibTeX", entry.getType()),
             entry,
             "bibtexkey"));
   }
   return Collections.emptyList();
 }
Beispiel #29
0
  @Override
  public Optional<URL> findFullText(BibEntry entry) throws IOException {
    Objects.requireNonNull(entry);

    String stampString = "";
    // Try URL first -- will primarily work for entries from the old IEEE search
    Optional<String> urlString = entry.getField(FieldName.URL);
    if (urlString.isPresent()) {
      // Is the URL a direct link to IEEE?
      Matcher matcher = STAMP_PATTERN.matcher(urlString.get());
      if (matcher.find()) {
        // Found it
        stampString = matcher.group(1);
      }
    }

    // If not, try DOI
    if (stampString.isEmpty()) {
      Optional<DOI> doi = entry.getField(FieldName.DOI).flatMap(DOI::build);
      if (doi.isPresent()
          && doi.get().getDOI().startsWith(IEEE_DOI)
          && doi.get().getURI().isPresent()) {
        // Download the HTML page from IEEE
        String resolvedDOIPage =
            new URLDownload(doi.get().getURI().get().toURL())
                .downloadToString(StandardCharsets.UTF_8);
        // Try to find the link
        Matcher matcher = STAMP_PATTERN.matcher(resolvedDOIPage);
        if (matcher.find()) {
          // Found it
          stampString = matcher.group(1);
        }
      }
    }

    // Any success?
    if (stampString.isEmpty()) {
      return Optional.empty();
    }

    // Download the HTML page containing a frame with the PDF
    String framePage =
        new URLDownload(BASE_URL + stampString).downloadToString(StandardCharsets.UTF_8);
    // Try to find the direct PDF link
    Matcher matcher = PDF_PATTERN.matcher(framePage);
    if (matcher.find()) {
      // The PDF was found
      LOGGER.debug("Full text document found on IEEE Xplore");
      return Optional.of(new URL(matcher.group(1)));
    }
    return Optional.empty();
  }
  @Test
  public void roundTripWithPrecedingCommentAndModificationTest() throws IOException {
    // @formatter:off
    String bibtexEntry =
        "% Some random comment that should stay here"
            + OS.NEWLINE
            + "@Article{test,"
            + OS.NEWLINE
            + "  Author                   = {Foo Bar},"
            + OS.NEWLINE
            + "  Journal                  = {International Journal of Something},"
            + OS.NEWLINE
            + "  Note                     = {some note},"
            + OS.NEWLINE
            + "  Number                   = {1}"
            + OS.NEWLINE
            + "}";
    // @formatter:on

    // read in bibtex string
    ParserResult result = BibtexParser.parse(new StringReader(bibtexEntry));
    Collection<BibEntry> entries = result.getDatabase().getEntries();
    BibEntry entry = entries.iterator().next();

    // change the entry
    entry.setField("author", "John Doe");

    // write out bibtex string
    StringWriter stringWriter = new StringWriter();
    writer.write(entry, stringWriter, BibDatabaseMode.BIBTEX);
    String actual = stringWriter.toString();
    // @formatter:off
    String expected =
        "% Some random comment that should stay here"
            + OS.NEWLINE
            + OS.NEWLINE
            + "@Article{test,"
            + OS.NEWLINE
            + "  author  = {John Doe},"
            + OS.NEWLINE
            + "  journal = {International Journal of Something},"
            + OS.NEWLINE
            + "  number  = {1},"
            + OS.NEWLINE
            + "  note    = {some note},"
            + OS.NEWLINE
            + "}"
            + OS.NEWLINE;
    // @formatter:on

    assertEquals(expected, actual);
  }