diff --git a/docs/export-format.md b/docs/export-format.md
index df94fcc5c..c951e46ce 100644
--- a/docs/export-format.md
+++ b/docs/export-format.md
@@ -48,6 +48,8 @@ The file names of repeat groups depend on their position inside the model tree o
| Top group | `{INSTANCE ID}` | `uuid:00000000-0000-0000-0000-000000000000` |
| Descendant group | `{PARENT KEY}` | `uuid:00000000-0000-0000-0000-000000000000/group1[1]` |
+_Note: non-repeat groups in the chain of ancestors are always ignored_
+
### `KEY`
| | Pattern | Example |
@@ -56,6 +58,7 @@ The file names of repeat groups depend on their position inside the model tree o
| Descendant group | `{PARENT KEY}/{GROUP NAME}[{ORDERING}]` | `uuid:00000000-0000-0000-0000-000000000000/group1[1]/group2[1]` |
_Note: the `[{ORDERING}]` part is 1-indexed_
+_Note: non-repeat groups in the chain of ancestors are always ignored_
### `SET-OF-{GROUP NAME}`
@@ -64,6 +67,8 @@ _Note: the `[{ORDERING}]` part is 1-indexed_
| Top group | `{INSTANCE ID}/{GROUP NAME}` | `uuid:00000000-0000-0000-0000-000000000000/group1` |
| Descendant group | `{PARENT KEY}/{GROUP NAME}` | `uuid:00000000-0000-0000-0000-000000000000/group1[1]/group2` |
+_Note: non-repeat groups in the chain of ancestors are always ignored_
+
The SET-OF columns may be named differently in the two output files. For example, if form X contains a non-repeat group Y, which contains a repeat group Z, then:
- The main output file will have a column named SET-OF-Y-Z (the long name of the repeat group).
diff --git a/src/org/opendatakit/briefcase/export/Csv.java b/src/org/opendatakit/briefcase/export/Csv.java
index 6787b6a36..9ae60ecaf 100644
--- a/src/org/opendatakit/briefcase/export/Csv.java
+++ b/src/org/opendatakit/briefcase/export/Csv.java
@@ -3,7 +3,6 @@
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
-
import static org.opendatakit.briefcase.export.CsvSubmissionMappers.getMainHeader;
import static org.opendatakit.briefcase.export.CsvSubmissionMappers.getRepeatHeader;
import static org.opendatakit.briefcase.reused.UncheckedFiles.write;
@@ -12,7 +11,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
-
import org.opendatakit.briefcase.reused.UncheckedFiles;
/**
@@ -60,8 +58,14 @@ static Csv repeat(FormDefinition formDefinition, Model groupModel, ExportConfigu
String repeatFileNameBase = configuration.getExportFileName()
.map(UncheckedFiles::stripFileExtension)
.orElse(stripIllegalChars(formDefinition.getFormName()));
+ String suffix = groupModel.getName();
+ Model current = groupModel;
+ while (grandParentIsRoot(current)) {
+ current = current.getParent();
+ suffix = current.getName() + "-" + suffix;
+ }
Path output = configuration.getExportDir().resolve(
- repeatFileNameBase + "-" + groupModel.getName() + ".csv"
+ repeatFileNameBase + "-" + suffix + ".csv"
);
return new Csv(
groupModel.fqn(),
@@ -73,6 +77,29 @@ static Csv repeat(FormDefinition formDefinition, Model groupModel, ExportConfigu
);
}
+ /**
+ * Returns true if the grandparent node of the given Model is the model's root
+ *
+ * Example 1:
+ *
+ *
+ * <data>
+ * </some_field>
+ * </data>
+ *
+ *
+ * In this example:
+ *
+ * - <data> has a parent with
null
name
+ * - Returns true on <some_field>
+ *
+ */
+ private static boolean grandParentIsRoot(Model current) {
+ return current.hasParent()
+ && current.getParent().hasParent() // Check if current has a grandparent
+ && current.getParent().getParent().getName() != null; // The root node has a null name
+ }
+
/**
* This method ensures that the output file is ready to receive new
* contents by appending lines.
diff --git a/src/org/opendatakit/briefcase/export/CsvSubmissionMappers.java b/src/org/opendatakit/briefcase/export/CsvSubmissionMappers.java
index 9f61d7e32..18bc93121 100644
--- a/src/org/opendatakit/briefcase/export/CsvSubmissionMappers.java
+++ b/src/org/opendatakit/briefcase/export/CsvSubmissionMappers.java
@@ -20,7 +20,6 @@
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
-
import static org.javarosa.core.model.DataType.DATE;
import static org.javarosa.core.model.DataType.DATE_TIME;
import static org.javarosa.core.model.DataType.GEOPOINT;
@@ -34,7 +33,6 @@
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-
import org.javarosa.core.model.DataType;
import org.javarosa.core.model.SelectChoice;
import org.opendatakit.briefcase.reused.Pair;
@@ -85,15 +83,15 @@ static CsvSubmissionMapper repeat(Model groupModel, ExportConfiguration configur
submission.getElements(groupModel.fqn()).stream().map(element -> {
List cols = new ArrayList<>();
cols.addAll(groupModel.flatMap(field -> getMapper(field, configuration.resolveExplodeChoiceLists()).apply(
- element.getCurrentLocalId(submission.getInstanceId(true)),
+ element.getCurrentLocalId(field, submission.getInstanceId(true)),
submission.getWorkingDir(),
field,
element.findElement(field.getName()),
configuration
).map(CsvSubmissionMappers::encodeRepeatValue)).collect(toList()));
- cols.add(encode(element.getParentLocalId(submission.getInstanceId(true)), false));
- cols.add(encode(element.getCurrentLocalId(submission.getInstanceId(true)), false));
- cols.add(encode(element.getGroupLocalId(submission.getInstanceId(true)), false));
+ cols.add(encode(element.getParentLocalId(groupModel, submission.getInstanceId(true)), false));
+ cols.add(encode(element.getCurrentLocalId(groupModel, submission.getInstanceId(true)), false));
+ cols.add(encode(element.getGroupLocalId(groupModel, submission.getInstanceId(true)), false));
return cols.stream().collect(joining(","));
}).collect(toList())
);
diff --git a/src/org/opendatakit/briefcase/export/FormDefinition.java b/src/org/opendatakit/briefcase/export/FormDefinition.java
index a1f037d5c..8221d7791 100644
--- a/src/org/opendatakit/briefcase/export/FormDefinition.java
+++ b/src/org/opendatakit/briefcase/export/FormDefinition.java
@@ -32,7 +32,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
-
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.IDataReference;
import org.javarosa.core.model.IFormElement;
@@ -56,7 +55,7 @@ public class FormDefinition {
private final Model model;
private final List repeatFields;
- private FormDefinition(String id, Path formFile, String name, boolean isEncrypted, Model model) {
+ FormDefinition(String id, Path formFile, String name, boolean isEncrypted, Model model) {
this.id = id;
this.name = name;
this.formFile = formFile;
diff --git a/src/org/opendatakit/briefcase/export/Model.java b/src/org/opendatakit/briefcase/export/Model.java
index af7f8966f..3f5171f9a 100644
--- a/src/org/opendatakit/briefcase/export/Model.java
+++ b/src/org/opendatakit/briefcase/export/Model.java
@@ -231,7 +231,7 @@ boolean isRoot() {
return countAncestors() == 0;
}
- private boolean hasParent() {
+ boolean hasParent() {
return model.getParent() != null;
}
diff --git a/src/org/opendatakit/briefcase/export/XmlElement.java b/src/org/opendatakit/briefcase/export/XmlElement.java
index 3e533a49e..a678d907f 100644
--- a/src/org/opendatakit/briefcase/export/XmlElement.java
+++ b/src/org/opendatakit/briefcase/export/XmlElement.java
@@ -58,35 +58,28 @@ static XmlElement of(Document document) {
/**
* Builds and returns this {@link XmlElement} instance's parent's local ID.
* This ID is used to cross-reference values in different exported files.
- *
- * @param instanceId the Form submission's instance ID
- * @return a {@link String} with this {@link XmlElement} instance's parent's local ID.
*/
- String getParentLocalId(String instanceId) {
- return isFirstLevelGroup() ? instanceId : getParent().getCurrentLocalId(instanceId);
+ String getParentLocalId(Model field, String instanceId) {
+ return isFirstLevelGroup() ? instanceId : getParent().getCurrentLocalId(field.getParent(), instanceId);
}
/**
* Builds and returns this {@link XmlElement} instance's current local ID.
* This ID is used to cross-reference values in different exported files.
- *
- * @param instanceId the Form submission's instance ID
- * @return a {@link String} with this {@link XmlElement} instance's current local ID.
*/
- String getCurrentLocalId(String instanceId) {
- String prefix = isFirstLevelGroup() ? instanceId : getParent().getCurrentLocalId(instanceId);
- return prefix + "/" + getName() + "[" + getPlaceAmongSameTagSiblings() + "]";
+ String getCurrentLocalId(Model field, String instanceId) {
+ String prefix = isFirstLevelGroup() ? instanceId : getParent().getCurrentLocalId(field.getParent(), instanceId);
+ return field.isRepeatable()
+ ? prefix + "/" + getName() + "[" + getPlaceAmongSameTagSiblings() + "]"
+ : prefix;
}
/**
* Builds and returns this {@link XmlElement} instance's group local ID.
* This ID is used to cross-reference values in different exported files.
- *
- * @param instanceId the Form submission's instance ID
- * @return a {@link String} with this {@link XmlElement} instance's group local ID.
*/
- String getGroupLocalId(String instanceId) {
- String prefix = isFirstLevelGroup() ? instanceId : getParent().getCurrentLocalId(instanceId);
+ String getGroupLocalId(Model field, String instanceId) {
+ String prefix = isFirstLevelGroup() ? instanceId : getParent().getCurrentLocalId(field.getParent(), instanceId);
return prefix + "/" + getName();
}
diff --git a/test/java/org/opendatakit/briefcase/export/CsvTest.java b/test/java/org/opendatakit/briefcase/export/CsvTest.java
new file mode 100644
index 000000000..fb2bad4a1
--- /dev/null
+++ b/test/java/org/opendatakit/briefcase/export/CsvTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 Nafundi
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.opendatakit.briefcase.export;
+
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+import org.junit.Test;
+import org.opendatakit.briefcase.matchers.PathMatchers;
+import org.opendatakit.briefcase.reused.OverridableBoolean;
+
+public class CsvTest {
+ @Test
+ public void includes_non_repeat_groups_in_repeat_filenames() throws IOException {
+ Model group = new XmlElementTest.ModelBuilder()
+ .addGroup("data")
+ .addGroup("g1")
+ .addGroup("g2")
+ .addGroup("g3")
+ .addRepeatGroup("r")
+ .build();
+
+ FormDefinition formDef = new FormDefinition(
+ "some_form",
+ Files.createTempFile("briefcase_some_form", ".xml"),
+ "some_form",
+ false,
+ group.getParent().getParent().getParent()
+ );
+
+ Path exportDir = Files.createTempDirectory("briefcase_export_dir");
+
+ ExportConfiguration conf = new ExportConfiguration(
+ Optional.of("some_form.csv"),
+ Optional.of(exportDir),
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ OverridableBoolean.FALSE,
+ OverridableBoolean.TRUE,
+ OverridableBoolean.TRUE,
+ Optional.of(false)
+ );
+
+ Csv repeat = Csv.repeat(formDef, group, conf);
+ repeat.prepareOutputFiles();
+
+ assertThat(exportDir.resolve("some_form-g1-g2-g3-r.csv"), PathMatchers.exists());
+ }
+}
\ No newline at end of file
diff --git a/test/java/org/opendatakit/briefcase/export/ExportToCsvScenario.java b/test/java/org/opendatakit/briefcase/export/ExportToCsvScenario.java
index 402f17d79..3045f4d23 100644
--- a/test/java/org/opendatakit/briefcase/export/ExportToCsvScenario.java
+++ b/test/java/org/opendatakit/briefcase/export/ExportToCsvScenario.java
@@ -176,9 +176,11 @@ void assertSameMedia(String suffix) {
}
void assertSameContentRepeats(String suffix, String... groupNames) {
+ final StringBuilder groupPrefix = new StringBuilder();
Arrays.asList(groupNames).forEach(groupName -> {
- String oldOutput = new String(readAllBytes(getPath(formDef.getFormId() + "-" + groupName + (suffix.isEmpty() ? "" : "-" + suffix) + ".csv.expected")));
- String newOutput = new String(readAllBytes(outputDir.resolve("new").resolve(stripIllegalChars(formDef.getFormName()) + "-" + groupName + ".csv")));
+ String oldOutput = new String(readAllBytes(getPath(formDef.getFormId() + "-" + groupPrefix.toString() + groupName + (suffix.isEmpty() ? "" : "-" + suffix) + ".csv.expected")));
+ String newOutput = new String(readAllBytes(outputDir.resolve("new").resolve(stripIllegalChars(formDef.getFormName()) + "-" + groupPrefix.toString() + groupName + ".csv")));
+ groupPrefix.append(groupName).append("-");
assertThat(newOutput, is(oldOutput));
});
}
diff --git a/test/java/org/opendatakit/briefcase/export/XmlElementTest.java b/test/java/org/opendatakit/briefcase/export/XmlElementTest.java
new file mode 100644
index 000000000..c289f1851
--- /dev/null
+++ b/test/java/org/opendatakit/briefcase/export/XmlElementTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2018 Nafundi
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.opendatakit.briefcase.export;
+
+import static java.util.Collections.emptyMap;
+import static org.hamcrest.Matchers.is;
+import static org.javarosa.core.model.instance.TreeReference.DEFAULT_MULTIPLICITY;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+import java.io.StringReader;
+import org.javarosa.core.model.DataType;
+import org.javarosa.core.model.instance.TreeElement;
+import org.junit.Test;
+import org.kxml2.io.KXmlParser;
+import org.kxml2.kdom.Document;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class XmlElementTest {
+ @Test
+ public void knows_how_to_generate_keys_of_a_repeat_nested_in_groups() throws IOException, XmlPullParserException {
+ Model field = new ModelBuilder()
+ .addGroup("g1")
+ .addGroup("g2")
+ .addGroup("g3")
+ .addRepeatGroup("r")
+ .build();
+ XmlElement xmlElement = buildXmlElementFrom(field);
+
+ assertThat(xmlElement.getCurrentLocalId(field, "uuid:SOMELONGUUID"), is("uuid:SOMELONGUUID/r[1]"));
+ assertThat(xmlElement.getParentLocalId(field, "uuid:SOMELONGUUID"), is("uuid:SOMELONGUUID"));
+ assertThat(xmlElement.getGroupLocalId(field, "uuid:SOMELONGUUID"), is("uuid:SOMELONGUUID/r"));
+ }
+
+ @Test
+ public void knows_how_to_generate_keys_of_nested_repeats() throws IOException, XmlPullParserException {
+ Model field = new ModelBuilder()
+ .addGroup("g1")
+ .addGroup("g2")
+ .addGroup("g3")
+ .addRepeatGroup("r1")
+ .addRepeatGroup("r2")
+ .build();
+ XmlElement xmlElement = buildXmlElementFrom(field);
+
+ assertThat(xmlElement.getCurrentLocalId(field, "uuid:SOMELONGUUID"), is("uuid:SOMELONGUUID/r1[1]/r2[1]"));
+ assertThat(xmlElement.getParentLocalId(field, "uuid:SOMELONGUUID"), is("uuid:SOMELONGUUID/r1[1]"));
+ assertThat(xmlElement.getGroupLocalId(field, "uuid:SOMELONGUUID"), is("uuid:SOMELONGUUID/r1[1]/r2"));
+ }
+
+ private static Document parse(String xml) throws XmlPullParserException, IOException {
+ Document tempDoc = new Document();
+ KXmlParser parser = new KXmlParser();
+ parser.setInput(new StringReader(xml));
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ tempDoc.parse(parser);
+ return tempDoc;
+ }
+
+ static class ModelBuilder {
+ private TreeElement current = new TreeElement(null, DEFAULT_MULTIPLICITY);
+
+ ModelBuilder addGroup(String name) {
+ TreeElement child = new TreeElement(name, DEFAULT_MULTIPLICITY);
+ child.setDataType(DataType.NULL.value);
+ child.setRepeatable(false);
+ child.setParent(current);
+ current.addChild(child);
+ current = child;
+ return this;
+ }
+
+ ModelBuilder addRepeatGroup(String name) {
+ TreeElement child = new TreeElement(name, DEFAULT_MULTIPLICITY);
+ child.setDataType(DataType.NULL.value);
+ child.setRepeatable(true);
+ child.setParent(current);
+ current.addChild(child);
+ current = child;
+ return this;
+ }
+
+ ModelBuilder addField(String name, DataType dataType) {
+ TreeElement child = new TreeElement(name, DEFAULT_MULTIPLICITY);
+ child.setDataType(dataType.value);
+ child.setParent(current);
+ current.addChild(child);
+ current = child;
+ return this;
+ }
+
+ Model build() {
+ return new Model(current, emptyMap());
+ }
+ }
+
+ private static XmlElement buildXmlElementFrom(Model field) throws IOException, XmlPullParserException {
+ Model current = field;
+ String xml = "<" + current.getName() + "/>";
+ while (current.hasParent()) {
+ current = current.getParent();
+ xml = "<" + current.getName() + ">" + xml + "" + current.getName() + ">";
+ }
+ return XmlElement.of(parse(xml)).findElement(field.getName()).get();
+ }
+}
\ No newline at end of file
diff --git a/test/resources/org/opendatakit/briefcase/export/nested-repeats-g2-append.csv.expected b/test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-append.csv.expected
similarity index 100%
rename from test/resources/org/opendatakit/briefcase/export/nested-repeats-g2-append.csv.expected
rename to test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-append.csv.expected
diff --git a/test/resources/org/opendatakit/briefcase/export/nested-repeats-g3-append.csv.expected b/test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-g3-append.csv.expected
similarity index 100%
rename from test/resources/org/opendatakit/briefcase/export/nested-repeats-g3-append.csv.expected
rename to test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-g3-append.csv.expected
diff --git a/test/resources/org/opendatakit/briefcase/export/nested-repeats-g3-overwrite.csv.expected b/test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-g3-overwrite.csv.expected
similarity index 100%
rename from test/resources/org/opendatakit/briefcase/export/nested-repeats-g3-overwrite.csv.expected
rename to test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-g3-overwrite.csv.expected
diff --git a/test/resources/org/opendatakit/briefcase/export/nested-repeats-g3.csv.expected b/test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-g3.csv.expected
similarity index 100%
rename from test/resources/org/opendatakit/briefcase/export/nested-repeats-g3.csv.expected
rename to test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-g3.csv.expected
diff --git a/test/resources/org/opendatakit/briefcase/export/nested-repeats-g2-overwrite.csv.expected b/test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-overwrite.csv.expected
similarity index 100%
rename from test/resources/org/opendatakit/briefcase/export/nested-repeats-g2-overwrite.csv.expected
rename to test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2-overwrite.csv.expected
diff --git a/test/resources/org/opendatakit/briefcase/export/nested-repeats-g2.csv.expected b/test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2.csv.expected
similarity index 100%
rename from test/resources/org/opendatakit/briefcase/export/nested-repeats-g2.csv.expected
rename to test/resources/org/opendatakit/briefcase/export/nested-repeats-g1-g2.csv.expected