diff --git a/build.gradle b/build.gradle index 8f1d3ae13..d1861285f 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,7 @@ sourceSets { test.java.srcDirs = ['src/test/java'] test.resources.srcDirs = ['src/test/resources'] } + compileJava.options.encoding = "UTF-8" compileTestJava.options.encoding = "UTF-8" diff --git a/src/jmh/java/org/javarosa/benchmarks/BenchmarkUtils.java b/src/jmh/java/org/javarosa/benchmarks/BenchmarkUtils.java index bde48f5c1..a987a6dc6 100644 --- a/src/jmh/java/org/javarosa/benchmarks/BenchmarkUtils.java +++ b/src/jmh/java/org/javarosa/benchmarks/BenchmarkUtils.java @@ -1,8 +1,9 @@ package org.javarosa.benchmarks; -import static org.javarosa.test.utils.ResourcePathHelper.r; import static org.javarosa.core.reference.ReferenceManagerTestUtils.setUpSimpleReferenceManager; +import static org.javarosa.test.utils.ResourcePathHelper.r; +import java.io.File; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; @@ -16,10 +17,16 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.stream.Stream; + +import org.javarosa.benchmarks.utils.XFormFileGenerator; +import org.javarosa.core.model.CoreModelModule; import org.javarosa.core.model.QuestionDef; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.LongData; import org.javarosa.core.model.data.StringData; +import org.javarosa.core.services.PrototypeManager; +import org.javarosa.core.util.JavaRosaCoreModule; +import org.javarosa.model.xform.XFormsModule; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; @@ -27,6 +34,7 @@ public class BenchmarkUtils { private static Path CACHE_PATH; + private static Path WORKING_DIR; public static Path prepareAssets(String... filenames) { try { Path assetsDir = Files.createTempDirectory("javarosa_benchmarks_"); @@ -146,30 +154,69 @@ public static Path getMinifiedNigeriaWardsXMLWithInternal2ndryInstance(){ public static Path getNigeriaWardsXMLWithExternal2ndryInstance(){ Path assetsPath = prepareAssets("nigeria_wards_external_2ndry_instance.xml", "lgas.xml", "wards.xml"); - setUpSimpleReferenceManager(assetsPath, "file"); + setUpSimpleReferenceManager(assetsPath,"file"); Path filePath = assetsPath.resolve("nigeria_wards_external_2ndry_instance.xml"); return filePath; } public static Path getWardsExternalInstance(){ Path assetsPath = prepareAssets( "wards.xml"); - setUpSimpleReferenceManager(assetsPath, "file"); + setUpSimpleReferenceManager( assetsPath,"file"); Path filePath = assetsPath.resolve("wards.xml"); return filePath; } public static Path getLGAsExternalInstance(){ Path assetsPath = prepareAssets( "lgas.xml"); - setUpSimpleReferenceManager(assetsPath, "file"); + setUpSimpleReferenceManager( assetsPath,"file"); Path filePath = assetsPath.resolve("lgas.xml"); return filePath; } + + public static File generateXFormFile(int noOfQuestions, int noOfQuestionGroups, int noOfInternalSecondaryInstances, int noOfExternalSecondaryInstances, int noOf2ndryInstanceElements) throws IOException { + XFormFileGenerator xFormFileGenerator = new XFormFileGenerator(); + String title = String.format("xform_%s_%sISI%sE_%sESI%sE", noOfQuestions, + noOfInternalSecondaryInstances, noOf2ndryInstanceElements, + noOfExternalSecondaryInstances, noOf2ndryInstanceElements + ); + File existingFile = getWorkingDir().resolve(title + ".xml").toFile(); + File xFormXmlFile; + if(existingFile.exists()){ + xFormXmlFile = existingFile; + }else{ + xFormXmlFile = xFormFileGenerator.generateXFormFile(title, noOfQuestions, noOfQuestionGroups, noOfInternalSecondaryInstances, noOfExternalSecondaryInstances, noOf2ndryInstanceElements, getWorkingDir()); + } + return xFormXmlFile; + } + + public static void registerCacheProtoTypes() { + PrototypeManager.registerPrototypes(JavaRosaCoreModule.classNames); + PrototypeManager.registerPrototypes(CoreModelModule.classNames); + new XFormsModule().registerModule(); + } + + + public static Path getCachePath() throws IOException { if(CACHE_PATH == null){ - CACHE_PATH = Files.createTempDirectory("javarosa_benchmarks_cache"); + File cacheDir = new File(getWorkingDir() + File.separator + "_cache"); + cacheDir.mkdir(); + CACHE_PATH = cacheDir.toPath(); } return CACHE_PATH; } + public static Path getWorkingDir() throws IOException { + if(WORKING_DIR == null){ + String tempDir = System.getProperty("java.io.tmpdir"); + File file = new File(tempDir + File.separator + "javarosa_benchmarks"); + if(!file.exists()){ + file.mkdir(); + } + WORKING_DIR = file.toPath(); + } + return WORKING_DIR; + } + } diff --git a/src/jmh/java/org/javarosa/benchmarks/Cache2FormDefBenchmark.java b/src/jmh/java/org/javarosa/benchmarks/Cache2FormDefBenchmark.java new file mode 100644 index 000000000..096910174 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/Cache2FormDefBenchmark.java @@ -0,0 +1,62 @@ +package org.javarosa.benchmarks; + +import org.javarosa.benchmarks.utils.FormDefCache; +import org.javarosa.core.model.FormDef; +import org.javarosa.xform.parse.FormParserHelper; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.File; +import java.io.IOException; + +import static org.javarosa.core.reference.ReferenceManagerTestUtils.setUpSimpleReferenceManager; +import static org.javarosa.benchmarks.BenchmarkUtils.dryRun; +import static org.javarosa.benchmarks.BenchmarkUtils.getCachePath; +import static org.javarosa.benchmarks.BenchmarkUtils.getWorkingDir; +import static org.javarosa.benchmarks.BenchmarkUtils.registerCacheProtoTypes; + +public class Cache2FormDefBenchmark { + + public static void main(String[] args) { + dryRun(Cache2FormDefBenchmark.class); + } + + @State(Scope.Thread) + public static class FormTypesState { + File xFormXmlFile ; + FormDef formDef ; + String CACHE_PATH; + @Param({"10", "200", "500"}) + public int noOfQuestions; + @Param({"1", "10"}) + public int noOfInternalSecondaryInstances; + @Param({"50", "500", "5000"}) + public int noOf2ndryInstanceElements; + @Param({"0"}) + public int noOfQuestionGroups; + @Param({"0"}) + public int noOfExternalSecondaryInstances; + @Setup(Level.Trial) + public void initialize() throws IOException { + CACHE_PATH = getCachePath().toString(); + xFormXmlFile = BenchmarkUtils.generateXFormFile(noOfQuestions, noOfQuestionGroups, noOfInternalSecondaryInstances, noOfExternalSecondaryInstances, noOf2ndryInstanceElements); + setUpSimpleReferenceManager(getWorkingDir(),"file"); + String formPath = xFormXmlFile.getPath(); + formDef = FormParserHelper.parse(xFormXmlFile.toPath()); + registerCacheProtoTypes(); + FormDefCache.writeCache(formDef, formPath, CACHE_PATH); + } + } + + @Benchmark + public void runBenchmark(FormTypesState state, Blackhole bh) throws IOException { + FormDef formDef = FormDefCache.readCache(state.xFormXmlFile, state.CACHE_PATH); + bh.consume(formDef); + } + +} diff --git a/src/jmh/java/org/javarosa/benchmarks/FormDef2CacheBenchmark.java b/src/jmh/java/org/javarosa/benchmarks/FormDef2CacheBenchmark.java new file mode 100644 index 000000000..60c44a639 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/FormDef2CacheBenchmark.java @@ -0,0 +1,61 @@ +package org.javarosa.benchmarks; + +import org.javarosa.benchmarks.utils.FormDefCache; +import org.javarosa.core.model.FormDef; +import org.javarosa.xform.parse.FormParserHelper; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import java.io.File; +import java.io.IOException; + +import static org.javarosa.core.reference.ReferenceManagerTestUtils.setUpSimpleReferenceManager; +import static org.javarosa.benchmarks.BenchmarkUtils.dryRun; +import static org.javarosa.benchmarks.BenchmarkUtils.getCachePath; +import static org.javarosa.benchmarks.BenchmarkUtils.getWorkingDir; +import static org.javarosa.benchmarks.BenchmarkUtils.registerCacheProtoTypes; + +public class FormDef2CacheBenchmark { + + public static void main(String[] args) { + dryRun(FormDef2CacheBenchmark.class); + } + + @State(Scope.Thread) + public static class FormTypesState { + String formPath ; + FormDef formDef ; + String CACHE_PATH; + @Param({"10", "200", "500"}) + public int noOfQuestions; + @Param({"1", "10"}) + public int noOfInternalSecondaryInstances; + @Param({"50", "500", "5000"}) + public int noOf2ndryInstanceElements; + @Param({"0"}) + public int noOfQuestionGroups = 1; + @Param({"0"}) + public int noOfExternalSecondaryInstances; + @Setup(Level.Trial) + public void initialize() throws IOException { + CACHE_PATH = getCachePath().toString(); + File xFormXmlFile = BenchmarkUtils.generateXFormFile(noOfQuestions, noOfQuestionGroups, noOfInternalSecondaryInstances, noOfExternalSecondaryInstances, noOf2ndryInstanceElements); + setUpSimpleReferenceManager(getWorkingDir(), "file"); + registerCacheProtoTypes(); + formPath = xFormXmlFile.getPath(); + formDef = FormParserHelper.parse(xFormXmlFile.toPath()); + registerCacheProtoTypes(); + } + } + + + @Benchmark + public void runBenchmark(FormTypesState state) throws IOException { + FormDefCache.writeCache(state.formDef,state.formPath, state.CACHE_PATH); + } + +} \ No newline at end of file diff --git a/src/jmh/java/org/javarosa/benchmarks/XForm2FormDefBenchmark.java b/src/jmh/java/org/javarosa/benchmarks/XForm2FormDefBenchmark.java new file mode 100644 index 000000000..6225b21f2 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/XForm2FormDefBenchmark.java @@ -0,0 +1,54 @@ +package org.javarosa.benchmarks; + +import org.javarosa.core.model.FormDef; +import org.javarosa.xform.parse.FormParserHelper; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static org.javarosa.benchmarks.BenchmarkUtils.dryRun; +import static org.javarosa.benchmarks.BenchmarkUtils.getWorkingDir; +import static org.javarosa.core.reference.ReferenceManagerTestUtils.setUpSimpleReferenceManager; + +public class XForm2FormDefBenchmark { + + public static void main(String[] args) { + dryRun(XForm2FormDefBenchmark.class); + } + + @State(Scope.Thread) + public static class FormTypesState { + Path xFormXmlPath ; + @Param({"10", "200", "500"}) + public int noOfQuestions; + @Param({"1", "10"}) + public int noOfInternalSecondaryInstances; + @Param({"50", "500", "5000"}) + public int noOf2ndryInstanceElements; + @Param({"0"}) + public int noOfQuestionGroups; + @Param({"0"}) + public int noOfExternalSecondaryInstances; + @Setup(Level.Trial) + public void initialize() throws IOException { + File xFormXmlFile = BenchmarkUtils.generateXFormFile(noOfQuestions, noOfQuestionGroups, noOfInternalSecondaryInstances, noOfExternalSecondaryInstances, noOf2ndryInstanceElements); + setUpSimpleReferenceManager(getWorkingDir(),"file"); + xFormXmlPath = xFormXmlFile.toPath(); + } + } + + @Benchmark + public void runBenchmark(FormTypesState state, Blackhole bh) throws IOException { + FormDef formDef = FormParserHelper.parse(state.xFormXmlPath); + bh.consume(formDef); + } + +} \ No newline at end of file diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/XFormFileGenerator.java b/src/jmh/java/org/javarosa/benchmarks/utils/XFormFileGenerator.java new file mode 100644 index 000000000..f0acb8543 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/XFormFileGenerator.java @@ -0,0 +1,108 @@ +package org.javarosa.benchmarks.utils; + +import org.javarosa.benchmarks.utils.builder.XFormBuilder; +import org.javarosa.benchmarks.utils.builder.XFormXmlDef; +import org.javarosa.benchmarks.utils.builder.form.Choice; +import org.javarosa.benchmarks.utils.builder.form.ChoiceSelector; +import org.javarosa.benchmarks.utils.builder.form.Question; +import org.javarosa.benchmarks.utils.builder.form.QuestionGroup; +import org.javarosa.benchmarks.utils.builder.form.QuestionType; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Used to create the XForm Builder and to generate the Xform XML File + * into the specified working directory + */ +public class XFormFileGenerator { + + public File generateXFormFile(String title, int noOfQuestions, int noOfQuestionGroups, int noOfInternalSecondaryInstances, int noOfExternalSecondaryInstances, int noOf2ndryInstanceElements, Path workingDirectory) throws IOException { + File file = new File(workingDirectory.resolve(title + ".xml").toString()); + FileWriter fileWriter = new FileWriter(file); + XFormBuilder xFormBuilder = generateXFormBuilder(title, noOfQuestions, noOfQuestionGroups, noOfInternalSecondaryInstances, noOfExternalSecondaryInstances, noOf2ndryInstanceElements,workingDirectory); + fileWriter.write(xFormBuilder.build()); + fileWriter.close(); + return file; + } + + public XFormBuilder generateXFormBuilder(String title, int noOfQuestions, int noOfQuestionGroups, int noOfInternalSecondaryInstances, int noOfExternalSecondaryInstances, int noOf2ndryInstanceElements, Path workingDirectory) throws IOException { + List internalSecondaryInstances = generateOptionSelectors(ChoiceSelector.Type.INTERNAL, noOfInternalSecondaryInstances, noOf2ndryInstanceElements); + List externalSecondaryInstances = generateOptionSelectors( ChoiceSelector.Type.EXTERNAL, noOfExternalSecondaryInstances, noOf2ndryInstanceElements); + List secondaryInstances = new ArrayList<>(); + secondaryInstances.addAll(internalSecondaryInstances); + secondaryInstances.addAll(externalSecondaryInstances); + XFormXmlDef XFormXmlDef = + new XFormXmlDef( + title, + generateQuestionGroups(noOfQuestionGroups, secondaryInstances), + generateQuestions(noOfQuestions, secondaryInstances), + internalSecondaryInstances, + externalSecondaryInstances); + + XFormBuilder xFormFileBuilder = new XFormBuilder(XFormXmlDef, workingDirectory); + return xFormFileBuilder; + } + + private List generateQuestions(int noOfQuestions, List choiceSelectorList){ + List questions = new ArrayList<>(noOfQuestions); + while(noOfQuestions > 0){ + questions.add(0, autoGenerate(noOfQuestions, choiceSelectorList)); + noOfQuestions--; + } + return questions; + } + + private Question autoGenerate(int index, List choiceSelectorList) { + Random random = new Random(); + Question question; + ChoiceSelector choiceSelector = null; + if(!choiceSelectorList.isEmpty()){ + int randomOptionSelector; + randomOptionSelector = random.nextInt(choiceSelectorList.size()); + choiceSelector = choiceSelectorList.get(randomOptionSelector); + } + + question = new Question(QuestionType.TEXT,"enter_input_" + index,"What is answer to question" + index + "?", "Hint to question " + index, choiceSelector); + + return question; + } + + private List generateQuestionGroups(int count, List choiceSelectorList){ + List questionGroups = new ArrayList<>(count); + while(count > 0){ + questionGroups.add(0, new QuestionGroup("group"+ count, generateQuestions(4, choiceSelectorList))); + count--; + } + return questionGroups; + } + + private List generateOptionSelectors(ChoiceSelector.Type type, int noOfOptionSelectors, int noOf2ndryInstanceElements){ + List instances = new ArrayList<>(noOfOptionSelectors); + String secondaryInstanceType = type.toString().toLowerCase(); + String instanceIdTemplate = secondaryInstanceType +"_secondary_instance_%sE_%0" + (noOfOptionSelectors + "").length() +"d"; + while(noOfOptionSelectors > 0){ + instances.add(0, generateOptionSelector(String.format(instanceIdTemplate, noOf2ndryInstanceElements, noOfOptionSelectors), noOf2ndryInstanceElements)); + noOfOptionSelectors--; + } + return instances; + } + + private ChoiceSelector generateOptionSelector(String instanceId, int noOfOptions){ + return new ChoiceSelector(instanceId, generateOptions(noOfOptions)); + } + + private List generateOptions(int noOfOptions){ + List choices = new ArrayList<>(); + for(int i = 0; i < noOfOptions; i++){ + choices.add(new Choice("item " + (i + 1), "option" + (i + 1))); + } + return choices; + } + +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/Constants.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/Constants.java new file mode 100644 index 000000000..906ff67a3 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/Constants.java @@ -0,0 +1,29 @@ +package org.javarosa.benchmarks.utils.builder; + +public class Constants { + + public static String EMPTY_STRING = ""; + public static String DOUBLE_QUOTE = "\""; + public static String SPACE = " "; + public static String OPEN_TOKEN = "<"; + public static String CLOSE_TOKEN = ">"; + public static String FORWARD_SLASH = "/"; + public static String EQUALS = "="; + public static String HTML = "html"; + public static String HEAD = "head"; + public static String BODY = "body"; + public static String TITLE = "title"; + public static String MODEL = "model"; + public static String INSTANCE = "instance"; + public static String BIND = "bind"; + public static String NODE_SET = "nodeset"; + public static String ITEM_SET = "itemset"; + public static String ITEM = "item"; + public static String LABEL = "label"; + public static String REF = "ref"; + public static String HINT = "hint"; + public static String VALUE = "value"; + public static String INPUT_TEXT = "input"; + public static String GROUP = "group"; + public static String NEW_LINE = System.getProperty("line.separator"); +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/XFormBuilder.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/XFormBuilder.java new file mode 100644 index 000000000..7855c8f3f --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/XFormBuilder.java @@ -0,0 +1,446 @@ +package org.javarosa.benchmarks.utils.builder; + +import org.javarosa.benchmarks.utils.builder.form.Choice; +import org.javarosa.benchmarks.utils.builder.form.ChoiceSelector; +import org.javarosa.benchmarks.utils.builder.form.Question; +import org.javarosa.benchmarks.utils.builder.form.QuestionGroup; +import org.javarosa.benchmarks.utils.builder.form.QuestionType; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.javarosa.benchmarks.utils.builder.Constants.BIND; +import static org.javarosa.benchmarks.utils.builder.Constants.BODY; +import static org.javarosa.benchmarks.utils.builder.Constants.CLOSE_TOKEN; +import static org.javarosa.benchmarks.utils.builder.Constants.DOUBLE_QUOTE; +import static org.javarosa.benchmarks.utils.builder.Constants.EMPTY_STRING; +import static org.javarosa.benchmarks.utils.builder.Constants.EQUALS; +import static org.javarosa.benchmarks.utils.builder.Constants.FORWARD_SLASH; +import static org.javarosa.benchmarks.utils.builder.Constants.GROUP; +import static org.javarosa.benchmarks.utils.builder.Constants.HEAD; +import static org.javarosa.benchmarks.utils.builder.Constants.HINT; +import static org.javarosa.benchmarks.utils.builder.Constants.HTML; +import static org.javarosa.benchmarks.utils.builder.Constants.INPUT_TEXT; +import static org.javarosa.benchmarks.utils.builder.Constants.INSTANCE; +import static org.javarosa.benchmarks.utils.builder.Constants.ITEM; +import static org.javarosa.benchmarks.utils.builder.Constants.ITEM_SET; +import static org.javarosa.benchmarks.utils.builder.Constants.LABEL; +import static org.javarosa.benchmarks.utils.builder.Constants.MODEL; +import static org.javarosa.benchmarks.utils.builder.Constants.NEW_LINE; +import static org.javarosa.benchmarks.utils.builder.Constants.NODE_SET; +import static org.javarosa.benchmarks.utils.builder.Constants.OPEN_TOKEN; +import static org.javarosa.benchmarks.utils.builder.Constants.REF; +import static org.javarosa.benchmarks.utils.builder.Constants.SPACE; +import static org.javarosa.benchmarks.utils.builder.Constants.TITLE; +import static org.javarosa.benchmarks.utils.builder.Constants.VALUE; + +public class XFormBuilder { + + private StringBuilder stringBuilder; + private XFormXmlDef XFormXmlDef; + private Path workingDirectory; + Map externalSecondaryInstances; + private boolean minify; + + public XFormBuilder(XFormXmlDef XFormXmlDef, Path workingDirectory) { + stringBuilder = new StringBuilder("\n"); + this.XFormXmlDef = XFormXmlDef; + this.workingDirectory = workingDirectory; + this.minify = false; + } + + public String build() { + return + // formatXML( + buildHtml() + .buildHead() + .buildBody() + .buildTitle() + .buildModel() + .buildPrimaryInstance() + .buildInternalSecondaryInstances() + .buildExternalSecondaryInstances() + .buildBind() + .buildControls() + .toString(); + //); + } + + public Map buildExternalInstances() { + return + buildHtml() + .buildHead() + .buildBody() + .buildTitle() + .buildModel() + .buildPrimaryInstance() + .buildInternalSecondaryInstances() + .buildExternalSecondaryInstances() + .buildBind() + .buildControls() + .getExternalSecondaryInstances(); + } + + private XFormBuilder buildHtml() { + if (!hasHtml()) { + String htmlElementString = openAndClose(HTML, XFormXmlDef.getNamespaces()); + stringBuilder.append(htmlElementString); + } + return this; + } + + private XFormBuilder buildHead() { + addChild(HTML, openAndClose(HEAD)); + return this; + } + + private XFormBuilder buildBody() { + addChild(HTML, openAndClose(BODY)); + return this; + } + + private XFormBuilder buildTitle() { + addChild(HEAD, openAndClose(TITLE, null, XFormXmlDef.getTitle())); + return this; + } + + private XFormBuilder buildModel() { + addChild(HEAD, openAndClose(MODEL)); + return this; + } + + static Map buildMap(String[]... args) { + Map map = new HashMap<>(); + for (String[] pair : args) map.put(pair[0], pair[1]); + return map; + } + + private XFormBuilder buildPrimaryInstance() { + final String ROOT = XFormXmlDef.getMainInstanceTagName(); + final String primaryInstanceString = + new StringBuilder(openingTag(INSTANCE)) + .append(openingTag(ROOT, buildMap(XFormXmlDef.getIdAttribute()))) + .append(shortOpenAndClose("start")) + .append(shortOpenAndClose("end")) + .append(shortOpenAndClose("today")) + .append(shortOpenAndClose("deviceid")) + .append(shortOpenAndClose("subscriberid")) + .append(shortOpenAndClose("simserial")) + .append(shortOpenAndClose("phonenumber")) + .append(generateQuestionGroup(XFormXmlDef.getQuestionGroups())) + .append(generateQuestions(XFormXmlDef.getQuestions())) + .append(closingTag(ROOT)) + .append(closingTag(INSTANCE)) + .toString(); + addChild(MODEL, primaryInstanceString); + return this; + } + + private XFormBuilder buildInternalSecondaryInstances() { + for (ChoiceSelector choiceSelector : XFormXmlDef.getInternalChoiceSelectorList()) { + StringBuilder sb = new StringBuilder(); + final Map idAttr = buildMap(new String[]{"id", choiceSelector.getInstanceId()}); + sb.append(openingTag(INSTANCE, idAttr)) + .append(openingTag(choiceSelector.getInstanceId())) + .append(generateSecondaryInstanceOptions(choiceSelector)) + .append(closingTag(choiceSelector.getInstanceId())) + .append(closingTag(INSTANCE)); + + addChild(MODEL, sb.toString()); + } + return this; + } + + private XFormBuilder buildExternalSecondaryInstances() { + generateExternalInstanceFiles(XFormXmlDef.getExternalChoiceSelectorList()); + for (ChoiceSelector choiceSelector : XFormXmlDef.getExternalChoiceSelectorList()) { + String instanceId = choiceSelector.getInstanceId(); + final Map attributesMap = buildMap( + new String[]{"id", choiceSelector.getInstanceId()}, + new String[]{"src", "jr://file/" + externalSecondaryInstances.get(instanceId).toFile().getName()} + ); + addChild(MODEL, openAndClose(INSTANCE, attributesMap)); + } + return this; + } + + private XFormBuilder buildBind() { + List questionGroups = XFormXmlDef.getQuestionGroups(); + for (QuestionGroup questionGroup : questionGroups) { + for (Question question : questionGroup.getQuestions()) { + String nodeset = generatePath(XFormXmlDef.getMainInstanceTagName(), + questionGroup.getName(), + question.getTagName() + ); + Map attrs = buildMap( + new String[]{NODE_SET, nodeset}, + new String[]{"type", "string"} + ); + addChild(MODEL, shortOpenAndClose(BIND, attrs)); + } + } + + for (Question question : XFormXmlDef.getQuestions()) { + String nodeset = generatePath(XFormXmlDef.getMainInstanceTagName(), question.getTagName()); + Map attrs = buildMap( + new String[]{NODE_SET, nodeset}, + new String[]{"type", "string"} + ); + addChild(MODEL, shortOpenAndClose(BIND, attrs)); + } + return this; + } + + private XFormBuilder buildControls() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append( + buildControl(XFormXmlDef.getQuestions(), + XFormXmlDef.getMainInstanceTagName()) + ); + List questionGroups = XFormXmlDef.getQuestionGroups(); + for (QuestionGroup questionGroup : questionGroups) { + stringBuilder.append(openingTag(GROUP, buildMap(new String[]{"appearance", "field-list"}))); + stringBuilder.append( + buildControl(questionGroup.getQuestions(), + generatePath(XFormXmlDef.getMainInstanceTagName(), questionGroup.getName())) + ); + stringBuilder.append(closingTag(GROUP)); + } + addChild(BODY, stringBuilder.toString()); + return this; + } + + private String buildControl(List questions, String parentNode) { + StringBuilder stringBuilder = new StringBuilder(); + for (Question question : questions) { + String ref = generatePath(parentNode, question.getTagName()); + String controlTag = INPUT_TEXT; + Map attributes = buildMap(new String[]{REF, ref}); + stringBuilder + .append(openingTag(controlTag, attributes)) + .append(openAndClose(LABEL, null, question.getLabel())) + .append(openAndClose(HINT, null, question.getHint())); + + if (question.getQuestionType().equals(QuestionType.TEXT)) { + String instanceId = question.getOptionSelector().getInstanceId(); + String instanceSelector = "instance('" + instanceId + "')"; + String nodeset = generatePath(false, instanceSelector, instanceId, ITEM); + stringBuilder + .append(openingTag(ITEM_SET, buildMap(new String[]{NODE_SET, nodeset}))) + .append(shortOpenAndClose(VALUE, buildMap(new String[]{REF, VALUE}))) + .append(shortOpenAndClose(LABEL, buildMap(new String[]{REF, LABEL}))) + .append(closingTag(ITEM_SET)); + } + stringBuilder + .append(closingTag(controlTag)); + + } + return stringBuilder.toString(); + } + + + public Map getExternalSecondaryInstances() { + return externalSecondaryInstances; + } + + private void generateExternalInstanceFiles(List choiceSelectorList) { + externalSecondaryInstances = new HashMap<>(); + try { + for (ChoiceSelector choiceSelector : choiceSelectorList) { + StringBuilder sb = new StringBuilder(); + String instanceId = choiceSelector.getInstanceId(); + sb.append(openingTag(instanceId)) + .append(generateSecondaryInstanceOptions(choiceSelector)) + .append(closingTag(instanceId)); + File externalInstanceFile = new File(workingDirectory + File.separator + instanceId + ".xml"); + FileWriter fileWriter = new FileWriter(externalInstanceFile); + fileWriter.write(sb.toString()); + fileWriter.close(); + externalSecondaryInstances.put(choiceSelector.getInstanceId(), externalInstanceFile.toPath()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private String generateQuestionGroup(List questionGroupList) { + if (questionGroupList != null) { + StringBuilder stringBuilder = new StringBuilder(); + for (QuestionGroup questionGroup : questionGroupList) { + stringBuilder + .append(openingTag(questionGroup.getName())) + .append(generateQuestions(questionGroup.getQuestions())) + .append(closingTag(questionGroup.getName())); + } + return stringBuilder.toString(); + } + return EMPTY_STRING; + } + + private String generatePath (String ...parts){ + return generatePath(true, parts); + } + + private String generatePath ( boolean absolute, String ...parts){ + return ((absolute ? FORWARD_SLASH : EMPTY_STRING) + String.join(FORWARD_SLASH, parts)).replaceAll("//", FORWARD_SLASH); + } + + private String generateSecondaryInstanceOptions (ChoiceSelector choiceSelector){ + StringBuilder stringBuilder = new StringBuilder(); + for (Choice choice : choiceSelector.getItems()) { + stringBuilder + .append(openingTag(ITEM)) + .append(openAndClose(LABEL, null, choice.getLabel())) + .append(openAndClose(VALUE, null, choice.getValue())) + .append(closingTag(ITEM)); + } + return stringBuilder.toString(); + } + + private String generateQuestions (List < Question > questions) { + StringBuilder stringBuilder = new StringBuilder(); + for (Question question : questions) { + String realTagName = question.getTagName(); + stringBuilder.append(shortOpenAndClose(realTagName)); + } + return stringBuilder.toString(); + } + + + private String generateAttributes(Map < String, String > attributes){ + if (attributes != null) { + StringBuilder stringBuilder = new StringBuilder(); + Iterator> iterator = attributes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry pair = iterator.next(); + stringBuilder.append(generateAttribute(pair.getKey(), pair.getValue())); + stringBuilder.append(iterator.hasNext() ? SPACE : EMPTY_STRING); + } + return stringBuilder.toString(); + } + return EMPTY_STRING; + } + + private String generateAttribute(String key, String value){ + return key + EQUALS + DOUBLE_QUOTE + value + DOUBLE_QUOTE; + } + + private boolean hasHtml () { + return false; + } + + private String shortOpenAndClose (String name, Map < String, String > attributes){ + return OPEN_TOKEN + + name + + SPACE + + generateAttributes(attributes) + + FORWARD_SLASH + CLOSE_TOKEN + + newLine(); + } + + private String shortOpenAndClose (String name){ + return OPEN_TOKEN + name + FORWARD_SLASH + CLOSE_TOKEN + newLine(); + } + + private String openAndClose (String name){ + return openingTag(name) + closingTag(name); + } + + private String openAndClose (String name, Map < String, String > attributes){ + return openingTag(name, attributes) + closingTag(name); + } + + private String openAndClose (String name, Map < String, String > attributes, String xmlText){ + return openingTag(name, attributes) + + xmlText + + NEW_LINE + + closingTag(name); + } + + private String openingTag (String name){ + return OPEN_TOKEN + name + CLOSE_TOKEN + newLine(); + } + + private String openingTag (String name, Map < String, String > attributes){ + return + new StringBuilder(OPEN_TOKEN) + .append(name) + .append(attributes == null ? EMPTY_STRING : SPACE) + .append(generateAttributes(attributes)) + .append(CLOSE_TOKEN) + .append(NEW_LINE) + .toString(); + } + + private String closingTag (String name){ + return OPEN_TOKEN + FORWARD_SLASH + name + CLOSE_TOKEN + newLine(); + } + + private void addChild (String parentName, String childString){ + String CLOSING_TAG_TOKEN = closingTag(parentName); + int insertionIndex = stringBuilder.indexOf(CLOSING_TAG_TOKEN); + stringBuilder.insert(insertionIndex, childString); + } + + private String newLine () { + return minify ? EMPTY_STRING : NEW_LINE; + } + + public String toString () { + return stringBuilder.toString(); + } + + public String formatXML (){ + String unformattedXML = stringBuilder.toString(); + final int length = unformattedXML.length(); + final int indentSpace = 3; + final StringBuilder newString = new StringBuilder(length + length / 10); + final char space = ' '; + int i = 0; + int indentCount = 0; + char currentChar = unformattedXML.charAt(i++); + char previousChar = currentChar; + boolean nodeStarted = true; + newString.append(currentChar); + for (; i < length - 1; ) { + currentChar = unformattedXML.charAt(i++); + if (((int) currentChar < 33) && !nodeStarted) { + continue; + } + switch (currentChar) { + case '<': + if ('>' == previousChar && '/' != unformattedXML.charAt(i - 1) && '/' != unformattedXML.charAt(i) && '!' != unformattedXML.charAt(i)) { + indentCount++; + } + newString.append(System.lineSeparator()); + for (int j = indentCount * indentSpace; j > 0; j--) { + newString.append(space); + } + newString.append(currentChar); + nodeStarted = true; + break; + case '>': + newString.append(currentChar); + nodeStarted = false; + break; + case '/': + if ('<' == previousChar || '>' == unformattedXML.charAt(i)) { + indentCount--; + } + newString.append(currentChar); + break; + default: + newString.append(currentChar); + } + previousChar = currentChar; + } + newString.append(unformattedXML.charAt(length - 1)); + return newString.toString(); + } +} \ No newline at end of file diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/XFormXmlDef.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/XFormXmlDef.java new file mode 100644 index 000000000..b32ae35e9 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/XFormXmlDef.java @@ -0,0 +1,82 @@ +package org.javarosa.benchmarks.utils.builder; + +import org.javarosa.benchmarks.utils.builder.form.ChoiceSelector; +import org.javarosa.benchmarks.utils.builder.form.Question; +import org.javarosa.benchmarks.utils.builder.form.QuestionGroup; + +import java.util.List; +import java.util.Map; + +/** + * Defines main components of the XForm + * being used to generate the xform XML file + */ +public class XFormXmlDef { + + static final Map DEFAULT_NAMESPACES = + XFormBuilder.buildMap( + new String[]{"xmlns", "http://www.w3.org/2002/xforms"}, + new String[]{"xmlns:h", "http://www.w3.org/1999/xhtml"}, + new String[]{"xmlns:ev", "http://www.w3.org/2001/xml-events"}, + new String[]{"xmlns:jr", "http://openrosa.org/javarosa"}, + new String[]{"xmlns:odk", "http://www.opendatakit.org/xforms"}, + new String[]{"xmlns:orx", "http://openrosa.org/xforms"}, + new String[]{"xmlns:xsd", "http://www.w3.org/2001/XMLSchema"} + ); + + private String title; + private String formId; + private List questions; + private List questionGroups; + private List internalChoiceSelectorList; + private List externalChoiceSelectorList; + + public XFormXmlDef(String title, List questionGroups, List questions, + List internalChoiceSelectorList, List externalChoiceSelectorList) { + this.title = title; + this.questions = questions; + this.questionGroups = questionGroups; + this.internalChoiceSelectorList = internalChoiceSelectorList; + this.externalChoiceSelectorList = externalChoiceSelectorList; + this.formId = "form_" + System.currentTimeMillis(); + } + + private String getFormId() { + return formId; + } + + String getTitle() { + return title; + } + + List getQuestions() { + return questions; + } + + List getQuestionGroups() { + return questionGroups; + } + + List getExternalChoiceSelectorList() { + return externalChoiceSelectorList; + } + + Map getNamespaces() { + return DEFAULT_NAMESPACES; + } + + List getInternalChoiceSelectorList() { + return internalChoiceSelectorList; + } + + //Non POJO methods + String getMainInstanceTagName(){ + return getFormId().toLowerCase() + .replace(" ", "_") + .replace("-", "_"); + } + + String[] getIdAttribute(){ + return new String[]{"id", getFormId()}; + } +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/Choice.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/Choice.java new file mode 100644 index 000000000..fc106aa86 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/Choice.java @@ -0,0 +1,32 @@ +package org.javarosa.benchmarks.utils.builder.form; + +import java.util.HashMap; +import java.util.Map; + +/** + * Abstracts an element of the Secondary instance + */ +public class Choice implements IsNode { + String label; + String value; + Map attributes; + + public Choice(String label, String value) { + this.label = label; + this.value = value; + attributes = new HashMap<>(); + } + + public String getLabel() { + return label; + } + + public String getValue() { + return value; + } + + @Override + public String getTagName() { + return "item"; + } +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/ChoiceSelector.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/ChoiceSelector.java new file mode 100644 index 000000000..4c127ae74 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/ChoiceSelector.java @@ -0,0 +1,31 @@ +package org.javarosa.benchmarks.utils.builder.form; + +import java.util.List; + +/** + * Abstracts the secondary instances + */ +public class ChoiceSelector { + + public enum Type { + INTERNAL, + EXTERNAL + } + + private String instanceId; + private List items; + + public ChoiceSelector(String instanceId, List items) { + this.instanceId = instanceId; + this.items = items; + } + + public String getInstanceId() { + return instanceId; + } + + public List getItems() { + return items; + } + +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/IsNode.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/IsNode.java new file mode 100644 index 000000000..47d269d89 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/IsNode.java @@ -0,0 +1,14 @@ +package org.javarosa.benchmarks.utils.builder.form; + +/** + * Interface is used to identify node parts of + * the XForm + */ +public interface IsNode { + /** + * Gets the tag name + * of a node in the xform file + * @return The name of the node tag + */ + public String getTagName(); +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/Question.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/Question.java new file mode 100644 index 000000000..b005f3a3c --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/Question.java @@ -0,0 +1,62 @@ +package org.javarosa.benchmarks.utils.builder.form; + +/** + * Abstracts the controls of the XForm + */ +public class Question implements IsNode { + + private String tagName; + private QuestionType questionType; + private String label; + private String hint; + private ChoiceSelector options; + + public Question(QuestionType questionType, String label) { + this(questionType, label, ""); + } + + public Question(QuestionType questionType, String label, String hint) { + this(questionType, label, hint, null); + } + + public Question(QuestionType questionType, String label, String hint, ChoiceSelector options) { + this(questionType, generateTagName(label) , label, hint, options); + } + + public Question(QuestionType questionType, String tagName, String label, String hint, ChoiceSelector options) { + this.tagName = tagName; + this.questionType = questionType; + this.label = label; + this.hint = hint; + this.options = options; + } + + public String getTagName() { + return tagName; + } + + public QuestionType getQuestionType() { + return questionType; + } + + public String getLabel() { + return label; + } + + public String getHint() { + return hint; + } + + public ChoiceSelector getOptionSelector() { + return options; + } + + private static String generateTagName(String label){ + String tagName = label + .toLowerCase() + .replaceAll("[^_0-9a-zA-Z]+", "_"); + int lastIndex = tagName.length() - 1; + return tagName.substring(lastIndex).equals("_") ? tagName.substring(0, lastIndex) : tagName; + } + +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/QuestionGroup.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/QuestionGroup.java new file mode 100644 index 000000000..553fa7b73 --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/QuestionGroup.java @@ -0,0 +1,31 @@ +package org.javarosa.benchmarks.utils.builder.form; + +import java.util.List; + +public class QuestionGroup implements IsNode { + + private String name; + private List questions; + + public QuestionGroup(String name, List questions) { + this.name = name; + this.questions = questions; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getQuestions() { + return questions; + } + + @Override + public String getTagName() { + return "group"; + } +} diff --git a/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/QuestionType.java b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/QuestionType.java new file mode 100644 index 000000000..74db30baf --- /dev/null +++ b/src/jmh/java/org/javarosa/benchmarks/utils/builder/form/QuestionType.java @@ -0,0 +1,9 @@ +package org.javarosa.benchmarks.utils.builder.form; + +/** + * Used to identify the type + * of control + */ +public enum QuestionType { + TEXT, +}