diff --git a/modules/solr-configset/src/main/scripts/cli/util/model/Block.java b/modules/solr-configset/src/main/scripts/cli/util/model/Block.java new file mode 100644 index 00000000000..3c9f8f331f7 --- /dev/null +++ b/modules/solr-configset/src/main/scripts/cli/util/model/Block.java @@ -0,0 +1,12 @@ +package cli.util.model; + +import java.util.List; +import java.util.Optional; + +public final class Block { + public static final String TRIGGER = Constants.TRIGGER_INDICATOR + "metadataBlock"; + + private Block() {} + + Optional> fields = Optional.empty(); +} diff --git a/modules/solr-configset/src/main/scripts/cli/util/model/Constants.java b/modules/solr-configset/src/main/scripts/cli/util/model/Constants.java new file mode 100644 index 00000000000..1fd8c6cdb67 --- /dev/null +++ b/modules/solr-configset/src/main/scripts/cli/util/model/Constants.java @@ -0,0 +1,7 @@ +package cli.util.model; + +public class Constants { + public static final String COMMENT_INDICATOR = "%%"; + public static final String TRIGGER_INDICATOR = "#"; + public static final String COLUMN_SEPARATOR = "\t"; +} diff --git a/modules/solr-configset/src/main/scripts/cli/util/model/ControlledVocabulary.java b/modules/solr-configset/src/main/scripts/cli/util/model/ControlledVocabulary.java new file mode 100644 index 00000000000..7804c617945 --- /dev/null +++ b/modules/solr-configset/src/main/scripts/cli/util/model/ControlledVocabulary.java @@ -0,0 +1,5 @@ +package cli.util.model; + +public class ControlledVocabulary { + public static final String TRIGGER = Constants.TRIGGER_INDICATOR + "controlledVocabulary"; +} diff --git a/modules/solr-configset/src/main/scripts/cli/util/model/Field.java b/modules/solr-configset/src/main/scripts/cli/util/model/Field.java new file mode 100644 index 00000000000..f4e7eed04d2 --- /dev/null +++ b/modules/solr-configset/src/main/scripts/cli/util/model/Field.java @@ -0,0 +1,12 @@ +package cli.util.model; + +import java.util.List; +import java.util.Optional; + +public class Field { + public static final String TRIGGER = Constants.TRIGGER_INDICATOR + "datasetField"; + + private Field() {} + + Optional> controlledVocabularyValues = Optional.empty(); +} diff --git a/modules/solr-configset/src/main/scripts/cli/util/model/ParsingState.java b/modules/solr-configset/src/main/scripts/cli/util/model/ParsingState.java new file mode 100644 index 00000000000..fc4e11c141f --- /dev/null +++ b/modules/solr-configset/src/main/scripts/cli/util/model/ParsingState.java @@ -0,0 +1,45 @@ +package cli.util.model; + +import cli.util.TsvBlockReader; + +public enum ParsingState { + Vocabularies(ControlledVocabulary.TRIGGER), + Fields(Field.TRIGGER, Vocabularies), + MetadataBlock(Block.TRIGGER, Fields), + // This state is only used exactly once and should never be reached from input. + // For safety, make the validation fail. + Init(Constants.COMMENT_INDICATOR, MetadataBlock); + + private final String stateTrigger; + private final ParsingState nextState; + + ParsingState(String trigger, ParsingState next) { + this.stateTrigger = trigger; + this.nextState = next; + } + + /** + * Create final state (no next step) + * @param trigger + */ + ParsingState(String trigger) { + this.stateTrigger = trigger; + this.nextState = this; + } + + public boolean isAllowedFinalState() { + return this == Fields || this == Vocabularies; + } + + public ParsingState transitionState(String headerLine) throws ParserException { + // if not null, not starting the same state again (no loops allowed) and starting the correct next state, return the next state + if(headerLine != null && ! headerLine.startsWith(this.stateTrigger) && + headerLine.startsWith(this.nextState.stateTrigger)) { + return this.nextState; + } + // otherwise throw a parsing exception + throw new ParserException("Invalid header '" + + (headerLine == null ? "null" : headerLine.substring(0, Math.min(25, headerLine.length()))) + + "...' while in section '" + this.stateTrigger + "'"); + } +} diff --git a/modules/solr-configset/src/test/java/cli/util/model/ParsingStateTest.java b/modules/solr-configset/src/test/java/cli/util/model/ParsingStateTest.java new file mode 100644 index 00000000000..b498639c852 --- /dev/null +++ b/modules/solr-configset/src/test/java/cli/util/model/ParsingStateTest.java @@ -0,0 +1,70 @@ +package cli.util.model; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ParsingStateTest { + + static Stream failingStateTransitionExamples() { + return Stream.of( + Arguments.of(ParsingState.Init, null), + Arguments.of(ParsingState.MetadataBlock, null), + Arguments.of(ParsingState.Fields, null), + Arguments.of(ParsingState.Vocabularies, null), + + Arguments.of(ParsingState.Init, ""), + Arguments.of(ParsingState.MetadataBlock, ""), + Arguments.of(ParsingState.Fields, ""), + Arguments.of(ParsingState.Vocabularies, ""), + + Arguments.of(ParsingState.Init, "foobar"), + Arguments.of(ParsingState.MetadataBlock, "foobar"), + Arguments.of(ParsingState.Fields, "foobar"), + Arguments.of(ParsingState.Vocabularies, "foobar"), + + Arguments.of(ParsingState.Init, Constants.TRIGGER_INDICATOR), + Arguments.of(ParsingState.Init, Constants.COMMENT_INDICATOR), + Arguments.of(ParsingState.Init, Constants.COLUMN_SEPARATOR), + + Arguments.of(ParsingState.Init, Field.TRIGGER), + Arguments.of(ParsingState.Init, ControlledVocabulary.TRIGGER), + + Arguments.of(ParsingState.MetadataBlock, Constants.COMMENT_INDICATOR), + Arguments.of(ParsingState.MetadataBlock, ControlledVocabulary.TRIGGER), + + Arguments.of(ParsingState.Fields, Constants.COMMENT_INDICATOR), + Arguments.of(ParsingState.Fields, Block.TRIGGER), + + Arguments.of(ParsingState.Vocabularies, Constants.COMMENT_INDICATOR), + Arguments.of(ParsingState.Vocabularies, Block.TRIGGER), + Arguments.of(ParsingState.Vocabularies, Field.TRIGGER) + ); + } + + @ParameterizedTest + @MethodSource("failingStateTransitionExamples") + void failingTransitions(ParsingState source, String triggerLine) throws ParserException { + ParserException ex = assertThrows(ParserException.class, () -> source.transitionState(triggerLine)); + } + + static Stream successfulStateTransitionExamples() { + return Stream.of( + Arguments.of(ParsingState.Init, Block.TRIGGER, ParsingState.MetadataBlock), + Arguments.of(ParsingState.MetadataBlock, Field.TRIGGER, ParsingState.Fields), + Arguments.of(ParsingState.Fields, ControlledVocabulary.TRIGGER, ParsingState.Vocabularies) + ); + } + + @ParameterizedTest + @MethodSource("successfulStateTransitionExamples") + void successfulTransitions(ParsingState source, String triggerLine, ParsingState expected) throws ParserException { + assertEquals(expected, source.transitionState(triggerLine)); + } + +} \ No newline at end of file