Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable single backslashes at fileDirectory #9889

Merged
merged 9 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/org/jabref/logic/exporter/BibWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public BibWriter(Writer writer, String newLineSeparator) {
}

/**
* Writes the given string. The newlines of the given string are converted to the newline set for this clas
* Writes the given string. The newlines of the given string are converted to the newline set for this class
*/
public void write(String string) throws IOException {
if (precedingNewLineRequired) {
Expand Down
54 changes: 35 additions & 19 deletions src/main/java/org/jabref/logic/exporter/MetaDataSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.stream.Collectors;

Expand All @@ -21,6 +22,9 @@
import org.jabref.model.metadata.MetaData;
import org.jabref.model.strings.StringUtil;

/**
* Reading is done at {@link org.jabref.logic.importer.util.MetaDataParser}
*/
public class MetaDataSerializer {

private MetaDataSerializer() {
Expand All @@ -32,8 +36,12 @@ private MetaDataSerializer() {
public static Map<String, String> getSerializedStringMap(MetaData metaData,
GlobalCitationKeyPattern globalCiteKeyPattern) {

// First write all meta data except groups
// metadata-key, list of contents
// - contents to be separated by OS.NEWLINE
// - each meta data item is written as separate @Comment entry - see org.jabref.logic.exporter.BibtexDatabaseWriter.writeMetaDataItem
Map<String, List<String>> stringyMetaData = new HashMap<>();

// First write all meta data except groups
metaData.getSaveOrderConfig().ifPresent(
saveOrderConfig -> stringyMetaData.put(MetaData.SAVE_ORDER_CONFIG, saveOrderConfig.getAsStringList()));
metaData.getSaveActions().ifPresent(
Expand All @@ -51,7 +59,7 @@ public static Map<String, String> getSerializedStringMap(MetaData metaData,
metaData.getLatexFileDirectories().forEach((user, path) -> stringyMetaData
.put(MetaData.FILE_DIRECTORY_LATEX + '-' + user, Collections.singletonList(path.toString().trim())));
metaData.getVersionDBStructure().ifPresent(
VersionDBStructure -> stringyMetaData.put(MetaData.VERSION_DB_STRUCT, Collections.singletonList(VersionDBStructure.trim())));
versionDBStructure -> stringyMetaData.put(MetaData.VERSION_DB_STRUCT, Collections.singletonList(versionDBStructure.trim())));

for (ContentSelector selector : metaData.getContentSelectorList()) {
stringyMetaData.put(MetaData.SELECTOR_META_PREFIX + selector.getField().getName(), selector.getValues());
Expand All @@ -67,10 +75,10 @@ public static Map<String, String> getSerializedStringMap(MetaData metaData,
// finally add all unknown meta data items to the serialization map
Map<String, List<String>> unknownMetaData = metaData.getUnknownMetaData();
for (Map.Entry<String, List<String>> entry : unknownMetaData.entrySet()) {
StringBuilder value = new StringBuilder();
value.append(OS.NEWLINE);
// The last "MetaData.SEPARATOR_STRING" adds compatibility to JabRef v5.9 and earlier
StringJoiner value = new StringJoiner(MetaData.SEPARATOR_STRING + OS.NEWLINE, OS.NEWLINE, MetaData.SEPARATOR_STRING + OS.NEWLINE);
for (String line : entry.getValue()) {
value.append(line.replaceAll(";", "\\\\;") + MetaData.SEPARATOR_STRING + OS.NEWLINE);
value.add(line.replace(MetaData.SEPARATOR_STRING, "\\" + MetaData.SEPARATOR_STRING));
}
serializedMetaData.put(entry.getKey(), value.toString());
}
Expand All @@ -81,25 +89,33 @@ public static Map<String, String> getSerializedStringMap(MetaData metaData,
private static Map<String, String> serializeMetaData(Map<String, List<String>> stringyMetaData) {
Map<String, String> serializedMetaData = new TreeMap<>();
for (Map.Entry<String, List<String>> metaItem : stringyMetaData.entrySet()) {
StringBuilder stringBuilder = new StringBuilder();
for (String dataItem : metaItem.getValue()) {
if (!metaItem.getKey().equals(MetaData.VERSION_DB_STRUCT)) {
stringBuilder.append(StringUtil.quote(dataItem, MetaData.SEPARATOR_STRING, MetaData.ESCAPE_CHARACTER)).append(MetaData.SEPARATOR_STRING);
List<String> itemList = metaItem.getValue();
if (itemList.isEmpty()) {
// Only add non-empty values
continue;
}

boolean isSaveActions = metaItem.getKey().equals(MetaData.SAVE_ACTIONS);
// The last "MetaData.SEPARATOR_STRING" adds compatibility to JabRef v5.9 and earlier
StringJoiner joiner = new StringJoiner(MetaData.SEPARATOR_STRING, "", MetaData.SEPARATOR_STRING);
boolean lastWasSaveActionsEnablement = false;
for (String dataItem : itemList) {
String string;
if (lastWasSaveActionsEnablement) {
string = OS.NEWLINE;
} else {
stringBuilder.append(StringUtil.quote(dataItem, MetaData.SEPARATOR_STRING, MetaData.ESCAPE_CHARACTER));
string = "";
}

string += StringUtil.quote(dataItem, MetaData.SEPARATOR_STRING, MetaData.ESCAPE_CHARACTER);
// in case of save actions, add an additional newline after the enabled flag
if (metaItem.getKey().equals(MetaData.SAVE_ACTIONS)
lastWasSaveActionsEnablement = isSaveActions
&& (FieldFormatterCleanups.ENABLED.equals(dataItem)
|| FieldFormatterCleanups.DISABLED.equals(dataItem))) {
stringBuilder.append(OS.NEWLINE);
}
|| FieldFormatterCleanups.DISABLED.equals(dataItem));
joiner.add(string);
}

String serializedItem = stringBuilder.toString();
// Only add non-empty values
if (!serializedItem.isEmpty() && !MetaData.SEPARATOR_STRING.equals(serializedItem)) {
String serializedItem = joiner.toString();
if (!serializedItem.isEmpty()) {
// Only add non-empty values
serializedMetaData.put(metaItem.getKey(), serializedItem);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ public BibtexParser(ImportFormatPreferences importFormatPreferences, FileUpdateM
* It is undetermined which entry is returned, so use this in case you know there is only one entry in the string.
*
* @return An Optional&lt;BibEntry>. Optional.empty() if non was found or an error occurred.
* @throws ParseException
*/
public static Optional<BibEntry> singleFromString(String bibtexString, ImportFormatPreferences importFormatPreferences, FileUpdateMonitor fileMonitor) throws ParseException {
Collection<BibEntry> entries = new BibtexParser(importFormatPreferences, fileMonitor).parseEntries(bibtexString);
Expand Down
72 changes: 54 additions & 18 deletions src/main/java/org/jabref/logic/importer/util/MetaDataParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import org.jabref.logic.cleanup.FieldFormatterCleanups;
import org.jabref.logic.importer.ParseException;
Expand All @@ -29,6 +30,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Writing is done at {@link org.jabref.logic.exporter.MetaDataSerializer}.
*/
public class MetaDataParser {

private static final Logger LOGGER = LoggerFactory.getLogger(MetaDataParser.class);
Expand Down Expand Up @@ -81,46 +85,46 @@ public MetaData parse(MetaData metaData, Map<String, String> data, Character key
entryList.sort(groupsLast());

for (Map.Entry<String, String> entry : entryList) {
List<String> value = getAsList(entry.getValue());
List<String> values = getAsList(entry.getValue());

if (entry.getKey().startsWith(MetaData.PREFIX_KEYPATTERN)) {
EntryType entryType = EntryTypeFactory.parse(entry.getKey().substring(MetaData.PREFIX_KEYPATTERN.length()));
nonDefaultCiteKeyPatterns.put(entryType, Collections.singletonList(getSingleItem(value)));
} else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY + '-')) {
// The user name starts directly after FILE_DIRECTORY + '-'
String user = entry.getKey().substring(MetaData.FILE_DIRECTORY.length() + 1);
metaData.setUserFileDirectory(user, getSingleItem(value));
nonDefaultCiteKeyPatterns.put(entryType, Collections.singletonList(getSingleItem(values)));
} else if (entry.getKey().startsWith(MetaData.SELECTOR_META_PREFIX)) {
// edge case, it might be one special field e.g. article from biblatex-apa, but we can't distinguish this from any other field and rather prefer to handle it as UnknownField
metaData.addContentSelector(ContentSelectors.parse(FieldFactory.parseField(entry.getKey().substring(MetaData.SELECTOR_META_PREFIX.length())), StringUtil.unquote(entry.getValue(), MetaData.ESCAPE_CHARACTER)));
} else if (entry.getKey().equals(MetaData.FILE_DIRECTORY)) {
metaData.setDefaultFileDirectory(parseDirectory(entry.getValue()));
} else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY + '-')) {
// The user name starts directly after FILE_DIRECTORY + '-'
String user = entry.getKey().substring(MetaData.FILE_DIRECTORY.length() + 1);
metaData.setUserFileDirectory(user, parseDirectory(entry.getValue()));
} else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY_LATEX)) {
// The user name starts directly after FILE_DIRECTORY_LATEX" + '-'
String user = entry.getKey().substring(MetaData.FILE_DIRECTORY_LATEX.length() + 1);
Path path = Path.of(getSingleItem(value)).normalize();
Path path = Path.of(parseDirectory(entry.getValue())).normalize();
metaData.setLatexFileDirectory(user, path);
} else if (entry.getKey().equals(MetaData.SAVE_ACTIONS)) {
metaData.setSaveActions(FieldFormatterCleanups.parse(value));
metaData.setSaveActions(FieldFormatterCleanups.parse(values));
} else if (entry.getKey().equals(MetaData.DATABASE_TYPE)) {
metaData.setMode(BibDatabaseMode.parse(getSingleItem(value)));
metaData.setMode(BibDatabaseMode.parse(getSingleItem(values)));
} else if (entry.getKey().equals(MetaData.KEYPATTERNDEFAULT)) {
defaultCiteKeyPattern = Collections.singletonList(getSingleItem(value));
defaultCiteKeyPattern = Collections.singletonList(getSingleItem(values));
} else if (entry.getKey().equals(MetaData.PROTECTED_FLAG_META)) {
if (Boolean.parseBoolean(getSingleItem(value))) {
if (Boolean.parseBoolean(getSingleItem(values))) {
metaData.markAsProtected();
} else {
metaData.markAsNotProtected();
}
} else if (entry.getKey().equals(MetaData.FILE_DIRECTORY)) {
metaData.setDefaultFileDirectory(getSingleItem(value));
} else if (entry.getKey().equals(MetaData.SAVE_ORDER_CONFIG)) {
metaData.setSaveOrderConfig(SaveOrder.parse(value));
metaData.setSaveOrderConfig(SaveOrder.parse(values));
} else if (entry.getKey().equals(MetaData.GROUPSTREE) || entry.getKey().equals(MetaData.GROUPSTREE_LEGACY)) {
metaData.setGroups(GroupsParser.importGroups(value, keywordSeparator, fileMonitor, metaData));
metaData.setGroups(GroupsParser.importGroups(values, keywordSeparator, fileMonitor, metaData));
} else if (entry.getKey().equals(MetaData.VERSION_DB_STRUCT)) {
metaData.setVersionDBStructure(getSingleItem(value));
metaData.setVersionDBStructure(getSingleItem(values));
} else {
// Keep meta data items that we do not know in the file
metaData.putUnknownMetaDataItem(entry.getKey(), value);
metaData.putUnknownMetaDataItem(entry.getKey(), values);
}
}

Expand All @@ -131,6 +135,31 @@ public MetaData parse(MetaData metaData, Map<String, String> data, Character key
return metaData;
}

/**
* Parse the content of the value as provided by "raw" content.
*
* We do not use unescaped value (created by @link{#getAsList(java.lang.String)}),
* because this leads to difficulties with UNC names.
*
* No normalization is done - the general file directory could be passed as Mac OS X path, but the user could sit on Windows.
*
* @param value the raw value (as stored in the .bib file)
*/
static String parseDirectory(String value) {
value = StringUtil.removeStringAtTheEnd(value, MetaData.SEPARATOR_STRING);
Pattern SINGLE_BACKSLASH = Pattern.compile("[^\\\\]\\\\[^\\\\]");
if (value.contains("\\\\\\\\")) {
// This is an escaped Windows UNC path
return value.replace("\\\\", "\\");
} else if (value.contains("\\\\") && !SINGLE_BACKSLASH.matcher(value).find()) {
// All backslashes escaped
return value.replace("\\\\", "\\");
} else {
// No backslash escaping
return value;
}
}

private static Comparator<? super Map.Entry<String, String>> groupsLast() {
return (s1, s2) -> MetaData.GROUPSTREE.equals(s1.getKey()) || MetaData.GROUPSTREE_LEGACY.equals(s1.getKey()) ? 1 :
MetaData.GROUPSTREE.equals(s2.getKey()) || MetaData.GROUPSTREE_LEGACY.equals(s2.getKey()) ? -1 : 0;
Expand Down Expand Up @@ -174,7 +203,14 @@ private static Optional<String> getNextUnit(Reader reader) throws IOException {
StringBuilder res = new StringBuilder();
while ((c = reader.read()) != -1) {
if (escape) {
res.append((char) c);
// at org.jabref.logic.exporter.MetaDataSerializer.serializeMetaData, only MetaData.SEPARATOR_CHARACTER, MetaData.ESCAPE_CHARACTER are quoted
// That means ; and \\
char character = (char) c;
if (character != MetaData.SEPARATOR_CHARACTER && character != MetaData.ESCAPE_CHARACTER) {
// Keep the escape character
res.append("\\");
}
res.append(character);
escape = false;
} else if (c == MetaData.ESCAPE_CHARACTER) {
escape = true;
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/org/jabref/model/metadata/MetaData.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class MetaData {
private final Map<String, List<String>> unknownMetaData = new HashMap<>();
private boolean isEventPropagationEnabled = true;
private boolean encodingExplicitlySupplied;
private String VersionDBStructure;
private String versionDBStructure;

/**
* Constructs an empty metadata.
Expand Down Expand Up @@ -214,11 +214,11 @@ public void setDefaultFileDirectory(String path) {
}

public Optional<String> getVersionDBStructure() {
return Optional.ofNullable(VersionDBStructure);
return Optional.ofNullable(versionDBStructure);
}

public void setVersionDBStructure(String version) {
VersionDBStructure = Objects.requireNonNull(version).trim();
versionDBStructure = Objects.requireNonNull(version).trim();
postChange();
}

Expand Down Expand Up @@ -384,17 +384,17 @@ public boolean equals(Object o) {
&& (mode == metaData.mode)
&& Objects.equals(defaultFileDirectory, metaData.defaultFileDirectory)
&& Objects.equals(contentSelectors, metaData.contentSelectors)
&& Objects.equals(VersionDBStructure, metaData.VersionDBStructure);
&& Objects.equals(versionDBStructure, metaData.versionDBStructure);
}

@Override
public int hashCode() {
return Objects.hash(isProtected, groupsRoot.getValue(), encoding, encodingExplicitlySupplied, saveOrder, citeKeyPatterns, userFileDirectory,
laTexFileDirectory, defaultCiteKeyPattern, saveActions, mode, defaultFileDirectory, contentSelectors, VersionDBStructure);
laTexFileDirectory, defaultCiteKeyPattern, saveActions, mode, defaultFileDirectory, contentSelectors, versionDBStructure);
}

@Override
public String toString() {
return "MetaData [citeKeyPatterns=" + citeKeyPatterns + ", userFileDirectory=" + userFileDirectory + ", laTexFileDirectory=" + laTexFileDirectory + ", groupsRoot=" + groupsRoot + ", encoding=" + encoding + ", saveOrderConfig=" + saveOrder + ", defaultCiteKeyPattern=" + defaultCiteKeyPattern + ", saveActions=" + saveActions + ", mode=" + mode + ", isProtected=" + isProtected + ", defaultFileDirectory=" + defaultFileDirectory + ", contentSelectors=" + contentSelectors + ", encodingExplicitlySupplied=" + encodingExplicitlySupplied + ", VersionDBStructure=" + VersionDBStructure + "]";
return "MetaData [citeKeyPatterns=" + citeKeyPatterns + ", userFileDirectory=" + userFileDirectory + ", laTexFileDirectory=" + laTexFileDirectory + ", groupsRoot=" + groupsRoot + ", encoding=" + encoding + ", saveOrderConfig=" + saveOrder + ", defaultCiteKeyPattern=" + defaultCiteKeyPattern + ", saveActions=" + saveActions + ", mode=" + mode + ", isProtected=" + isProtected + ", defaultFileDirectory=" + defaultFileDirectory + ", contentSelectors=" + contentSelectors + ", encodingExplicitlySupplied=" + encodingExplicitlySupplied + ", VersionDBStructure=" + versionDBStructure + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Answers;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -129,11 +131,10 @@ void singleFromStringRecognizesEntry() throws ParseException {
""",
importFormatPreferences, fileMonitor);

BibEntry expected = new BibEntry();
expected.setType(StandardEntryType.Article);
expected.setCitationKey("canh05");
expected.setField(StandardField.AUTHOR, "Crowston, K. and Annabi, H.");
expected.setField(StandardField.TITLE, "Title A");
BibEntry expected = new BibEntry(StandardEntryType.Article)
.withCitationKey("canh05")
.withField(StandardField.AUTHOR, "Crowston, K. and Annabi, H.")
.withField(StandardField.TITLE, "Title A");

assertEquals(Optional.of(expected), parsed);
}
Expand Down Expand Up @@ -1655,6 +1656,29 @@ void integrationTestFileDirectories() throws IOException {
assertEquals("D:\\Latex", result.getMetaData().getLatexFileDirectory("defaultOwner-user").get().toString());
}

@ParameterizedTest
@CsvSource({
// single backslash kept
"C:\\temp\\test",
"\\\\servername\\path\\to\\file",
"//servername/path/to/file",
"."})
void fileDirectoriesUnmodified(String directory) throws IOException {
ParserResult result = parser.parse(
new StringReader("@comment{jabref-meta: fileDirectory:" + directory + "}"));
assertEquals(directory, result.getMetaData().getDefaultFileDirectory().get());
}

@ParameterizedTest
@CsvSource({
"C:\\temp\\test, C:\\\\temp\\\\test",
"\\\\servername\\path\\to\\file, \\\\\\\\servername\\\\path\\\\to\\\\file"})
void fileDirectoryWithDoubleEscapeIsRead(String expected, String provided) throws IOException {
ParserResult result = parser.parse(
new StringReader("@comment{jabref-meta: fileDirectory: " + provided + "}"));
assertEquals(expected, result.getMetaData().getDefaultFileDirectory().get());
}

@Test
void parseReturnsEntriesInSameOrder() throws IOException {
List<BibEntry> expected = new ArrayList<>();
Expand Down
Loading