From: David ‘Bombe’ Roden Date: Sat, 24 Jan 2026 06:38:49 +0000 (+0100) Subject: 🧑‍💻 Use new and improved identity parser X-Git-Url: https://git.pterodactylus.net/?a=commitdiff_plain;h=d21b74d13b27aca5e8a6a66faffe399d09f54cb3;p=jFCPlib.git 🧑‍💻 Use new and improved identity parser --- diff --git a/src/main/java/net/pterodactylus/fcp/plugin/IdentityParser.java b/src/main/java/net/pterodactylus/fcp/plugin/IdentityParser.java deleted file mode 100644 index a97333f..0000000 --- a/src/main/java/net/pterodactylus/fcp/plugin/IdentityParser.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.pterodactylus.fcp.plugin; - -import java.util.Map; -import java.util.Set; - -/** - * Interface for parsers for WebOfTrust’s on-wire identity formats. - */ -interface IdentityParser { - - /** - * Whether this parser can parse an identity from the given fields. - * - * @param fields The fields to parse the identity from - * @param prefix A prefix used for all accessed fields, may be an empty String - * @return {@code true} if this parse can parse identities from the given - * fields, {@code false} otherwise - */ - boolean canParse(Map fields, String prefix); - - /** - * Parses an identity from the given fields. - * - * @param fields The fields to parse the identity from - * @param prefix A prefix used for all access fields, may be an empty String - * @param identityGenerator An identity generator - * @param The type of the identity to parse - * @return The generated identity - */ - I parseSingleIdentity(Map fields, String prefix, IdentityGenerator identityGenerator); - - /** - * Parses multiple identities from the given fields. - * - * @param fields The fields to parse the identities from - * @param prefix A prefix used for all identities, may be an empty String - * @param identityGenerator An identity generator - * @param The type of the identity to parse - * @return The generated identities - */ - Set parseMultipleIdentities(Map fields, String prefix, IdentityGenerator identityGenerator); - -} diff --git a/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV1.java b/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV1.java deleted file mode 100644 index a36c056..0000000 --- a/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV1.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.pterodactylus.fcp.plugin; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -/** - * {@link IdentityParser} for the first WebOfTrust identity format. This - * format is now deprecated for most of the transferred identities, but not - * all of them; e.g. {@code GetOwnIdentities} still uses this format. - */ -class IdentityParserV1 implements IdentityParser { - - @Override - public boolean canParse(Map fields, String prefix) { - return fields.containsKey(prefix + "Identity0"); - } - - @Override - public I parseSingleIdentity(Map fields, String prefix, IdentityGenerator identityGenerator) { - return parseIdentity(fields, field -> prefix + field, identityGenerator); - } - - @Override - public Set parseMultipleIdentities(Map fields, String prefix, IdentityGenerator identityGenerator) { - Set identities = new HashSet<>(); - for (int identityIndex = 0; fields.containsKey(prefix + "Identity" + identityIndex); identityIndex++) { - int index = identityIndex; - identities.add(parseIdentity(fields, field -> prefix + field + index, identityGenerator)); - } - return identities; - } - - private static I parseIdentity(Map fields, Function fieldPackager, IdentityGenerator identityGenerator) { - String id = fields.get(fieldPackager.apply("Identity")); - /* sometimes WoT doesn’t send an Identity field, but an ID field. */ - if (id == null) { - id = fields.get(fieldPackager.apply("ID")); - } - String name = fields.get(fieldPackager.apply("Nickname")); - String requestUri = fields.get(fieldPackager.apply("RequestURI")); - String insertUri = fields.get(fieldPackager.apply("InsertURI")); - Set contexts = new HashSet<>(); - for (int contextIndex = 0; fields.containsKey(fieldPackager.apply("Contexts") + ".Context" + contextIndex); contextIndex++) { - contexts.add(fields.get(fieldPackager.apply("Contexts") + ".Context" + contextIndex)); - } - Map properties = new HashMap<>(); - for (int propertyIndex = 0; fields.containsKey(fieldPackager.apply("Properties") + ".Property" + propertyIndex + ".Name"); propertyIndex++) { - String key = fields.get(fieldPackager.apply("Properties") + ".Property" + propertyIndex + ".Name"); - String value = fields.get(fieldPackager.apply("Properties") + ".Property" + propertyIndex + ".Value"); - properties.put(key, value); - } - return identityGenerator.createIdentity(id, name, requestUri, insertUri, contexts, properties); - } - -} diff --git a/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV2.java b/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV2.java deleted file mode 100644 index 714009f..0000000 --- a/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV2.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.pterodactylus.fcp.plugin; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -/** - * {@link IdentityParser} for the second (and current) WebOfTrust identity - * format. This format is more consequent in how the data is structured, but - * it is not yet used for all WebOfTrust messages. - */ -class IdentityParserV2 implements IdentityParser { - - @Override - public boolean canParse(Map fields, String prefix) { - return fields.containsKey(prefix + "Identities.0.ID"); - } - - @Override - public I parseSingleIdentity(Map fields, String prefix, IdentityGenerator identityGenerator) { - return parseIdentity(fields, field -> prefix + field, identityGenerator); - } - - @Override - public Set parseMultipleIdentities(Map fields, String prefix, IdentityGenerator identityGenerator) { - Set identities = new HashSet<>(); - for (int identityIndex = 0; fields.containsKey(prefix + "Identities." + identityIndex + ".ID"); identityIndex++) { - int index = identityIndex; - identities.add(parseIdentity(fields, field -> prefix + "Identities." + index + "." + field, identityGenerator)); - } - return identities; - } - - private I parseIdentity(Map fields, Function fieldPackager, IdentityGenerator identityGenerator) { - String id = fields.get(fieldPackager.apply("ID")); - String name = fields.get(fieldPackager.apply("Nickname")); - String requestUri = fields.get(fieldPackager.apply("RequestURI")); - String insertUri = fields.get(fieldPackager.apply("InsertURI")); - Set contexts = new HashSet<>(); - for (int contextIndex = 0; fields.containsKey(fieldPackager.apply("Contexts.") + contextIndex + ".Name"); contextIndex++) { - contexts.add(fields.get(fieldPackager.apply("Contexts.") + contextIndex + ".Name")); - } - Map properties = new HashMap<>(); - for (int propertyIndex = 0; fields.containsKey(fieldPackager.apply("Properties.") + propertyIndex + ".Name"); propertyIndex++) { - String key = fields.get(fieldPackager.apply("Properties.") + propertyIndex + ".Name"); - String value = fields.get(fieldPackager.apply("Properties.") + propertyIndex + ".Value"); - properties.put(key, value); - } - return identityGenerator.createIdentity(id, name, requestUri, insertUri, contexts, properties); - } - -} diff --git a/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV3.java b/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV3.java new file mode 100644 index 0000000..1ff9452 --- /dev/null +++ b/src/main/java/net/pterodactylus/fcp/plugin/IdentityParserV3.java @@ -0,0 +1,211 @@ +package net.pterodactylus.fcp.plugin; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Arrays.stream; + +/** + * Parser for Web of Trust identities, recognizing all formats the Web of + * Trust plugin writes (and trying them in order of least-deprecated) and + * allowing retrieval of per-identity extraneous values. + * + *

+ * This parser is called V3 because it succeeds two other, less successful + * parsers. + *

+ * + *

Usage

+ *

+ * This identity parser knows about all formats that the Web of Trust plugin + * writes identities (including contexts and properties) in; additional + * fields (such as the trust- and score-related ones) can be added by + * specifying {@link FieldNamer}s. + *

+ *
+ * var identityParser = new IdentityParserV3<Identity>(fields, identityGenerator,
+ *     new FieldNamer("Trust", "Identities.{0}.Trust.{1}.Value", "Trust{0}.Value", "Trust")
+ * );
+ * for (var iterator = identityParser.iterate(); iterator.hasNext();) {
+ *     Identity identity = iterator.next();
+ *     String trust = iterator.getExtraFieldValue("Trust");
+ *     identityTrusts.put(identity, trust);
+ * }
+ * 
+ * + * @param The type of identity to parse + */ +class IdentityParserV3 implements Iterable { + + public IdentityParserV3(Map fields, IdentityGenerator identityGenerator, FieldNamer... extraFields) { + this.fields = fields; + this.identityGenerator = identityGenerator; + this.extraFields = extraFields; + } + + public IdentityIterator iterator() { + return new IdentityIterator(); + } + + /** + * Specialized {@link Iterator} over parsed identities which can provide + * the values of additional fields. The extra fields are parsed together + * with the next identity, so they need to be accessed either before + * {@link #next()} is called, or after {@link #next()} but before the + * next {@link #next()} or {@link #hasNext()}. + */ + class IdentityIterator implements Iterator { + + @Override + public boolean hasNext() { + getNext(); + return nextIdentity != null; + } + + @Override + public I next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + try { + return nextIdentity; + } finally { + nextIdentity = null; + } + } + + public String getExtraField(String field) { + return extraFieldValues.get(field); + } + + private void getNext() { + if (nextIdentity == null) { + extraFieldValues.clear(); + nextIdentity = parseIdentity(identityIndex++, extraFieldValues); + } + } + + private int identityIndex = 0; + private I nextIdentity; + private final Map extraFieldValues = new HashMap<>(); + + } + + /** + * Defines where a field is located in a Web of Trust response. + */ + static class FieldNamer { + + /** + * Creates a new field namer. The additional field names can contain + * placeholders for indices. + * {@link MessageFormat#format(String, Object...)} is used to combine + * the field names and the indices; e.g. an additional field name of + * {@code Foo.{0}.Value{1}} together with the indices 3 and 5 will + * result in a field name of {@code Foo.3.Value5}. When multiple + * field names are specified, the first field name that is contained + * in the Web of Trust responses will be used. The name of the + * field is the name that can be used with + * {@link IdentityIterator#getExtraField(String)} to retrieve the + * value of the field. + * + * @param name The name of the field + * @param additionalFieldNames Field names using + * {@link MessageFormat}-style placeholders for indices + */ + public FieldNamer(String name, String... additionalFieldNames) { + this.name = name; + this.fieldNames = additionalFieldNames; + } + + public String getName() { + return name; + } + + public String getFieldValue(Map fields, Integer... indices) { + for (String fieldName : getFieldNames(indices)) { + if (fields.containsKey(fieldName)) { + return fields.get(fieldName); + } + } + return null; + } + + private List getFieldNames(Integer... indices) { + return Stream.of(fieldNames) + .map(fieldName -> MessageFormat.format(fieldName, (Object[]) indices)) + .collect(Collectors.toList()); + } + + private final String name; + private final String[] fieldNames; + + } + + private I parseIdentity(int identityIndex, Map extraFieldValues) { + I result = parseIdentity(identityIndex); + if (result == null) { + return null; + } + stream(extraFields).forEach(fieldNamer -> { + for (String fieldName : fieldNamer.getFieldNames(identityIndex)) { + extraFieldValues.putIfAbsent(fieldNamer.getName(), fields.get(fieldName)); + } + }); + return result; + } + + private I parseIdentity(int identityIndex) { + String insertUri = insertUriNamer.getFieldValue(fields, identityIndex); + String requestUri = requestUriNamer.getFieldValue(fields, identityIndex); + String nickname = nicknameNamer.getFieldValue(fields, identityIndex); + String id = identityNamer.getFieldValue(fields, identityIndex); + if (id == null) { + return null; + } + Set contexts = new HashSet<>(); + int contextIndex = 0; + while (true) { + String context = contextNamer.getFieldValue(fields, identityIndex, contextIndex); + if (context == null) { + break; + } + contexts.add(context); + contextIndex++; + } + Map properties = new HashMap<>(); + int propertyIndex = 0; + while (true) { + String name = propertyNameNamer.getFieldValue(fields, identityIndex, propertyIndex); + if (name == null) { + break; + } + String value = propertyValueNamer.getFieldValue(fields, identityIndex, propertyIndex); + properties.put(name, value); + propertyIndex++; + } + + return identityGenerator.createIdentity(id, nickname, requestUri, insertUri, contexts, properties); + } + + private final Map fields; + private final IdentityGenerator identityGenerator; + private final FieldNamer[] extraFields; + + private final FieldNamer insertUriNamer = new FieldNamer("InsertURI", "Identities.{0}.InsertURI", "InsertURI{0}", "InsertURI"); + private final FieldNamer requestUriNamer = new FieldNamer("RequestURI", "Identities.{0}.RequestURI", "RequestURI{0}", "RequestURI"); + private final FieldNamer nicknameNamer = new FieldNamer("Nickname", "Identities.{0}.Nickname", "Nickname{0}", "Nickname"); + private final FieldNamer identityNamer = new FieldNamer("Identity", "Identities.{0}.ID", "Identities.{0}.Identity", "ID{0}", "Identity{0}", "ID", "Identity"); + private final FieldNamer contextNamer = new FieldNamer("Context", "Identities.{0}.Contexts.{1}.Name", "Contexts{0}.Context{1}", "Context{1}"); + private final FieldNamer propertyNameNamer = new FieldNamer("Name", "Identities.{0}.Properties.{1}.Name", "Properties{0}.Property{1}.Name", "Property{1}.Name"); + private final FieldNamer propertyValueNamer = new FieldNamer("Value", "Identities.{0}.Properties.{1}.Value", "Properties{0}.Property{1}.Value", "Property{1}.Value"); + +} diff --git a/src/main/java/net/pterodactylus/fcp/plugin/WebOfTrustPlugin.java b/src/main/java/net/pterodactylus/fcp/plugin/WebOfTrustPlugin.java index 6df8149..25e3b2b 100644 --- a/src/main/java/net/pterodactylus/fcp/plugin/WebOfTrustPlugin.java +++ b/src/main/java/net/pterodactylus/fcp/plugin/WebOfTrustPlugin.java @@ -17,6 +17,7 @@ package net.pterodactylus.fcp.plugin; +import java.util.HashSet; import net.pterodactylus.fcp.highlevel.FcpClient; import net.pterodactylus.fcp.highlevel.FcpException; @@ -275,13 +276,14 @@ public class WebOfTrustPlugin { throw new FcpException("WebOfTrust Plugin did not reply with “Identities” message!"); } Map identityTrusts = new HashMap<>(); - for (int identityIndex = 0; replies.containsKey("Identity" + identityIndex); identityIndex++) { - String identifier = replies.get("Identity" + identityIndex); - String nickname = replies.get("Nickname" + identityIndex); - String requestUri = replies.get("RequestURI" + identityIndex); - byte trust = Byte.parseByte(replies.get("Value" + identityIndex)); - String comment = replies.get("Comment" + identityIndex); - identityTrusts.put(new Identity(identifier, nickname, requestUri, emptySet(), emptyMap()), new IdentityTrust(trust, comment)); + IdentityParserV3 parser = new IdentityParserV3<>(replies, WebOfTrustPlugin::createIdentity, + new IdentityParserV3.FieldNamer("Value", "Value{0}"), + new IdentityParserV3.FieldNamer("Comment", "Comment{0}") + ); + for (IdentityParserV3.IdentityIterator parsedIdentity = parser.iterator(); parsedIdentity.hasNext();) { + byte trust = Byte.parseByte(parsedIdentity.getExtraField("Value")); + String comment = parsedIdentity.getExtraField("Comment"); + identityTrusts.put(parsedIdentity.next(), new IdentityTrust(trust, comment)); } return identityTrusts; } @@ -446,18 +448,15 @@ public class WebOfTrustPlugin { } private static I parseIdentity(Map replies, IdentityGenerator identityGenerator) { - IdentityParser parser = v2IdentityParser.canParse(replies, "") ? v2IdentityParser : v1IdentityParser; - return parser.parseSingleIdentity(replies, "", identityGenerator); + return new IdentityParserV3<>(replies, identityGenerator).iterator().next(); } private static Set parseIdentities(Map replies, IdentityGenerator identityGenerator) { - IdentityParser parser = v2IdentityParser.canParse(replies, "") ? v2IdentityParser : v1IdentityParser; - return parser.parseMultipleIdentities(replies, "", identityGenerator); + Set identities = new HashSet<>(); + new IdentityParserV3<>(replies, identityGenerator).iterator().forEachRemaining(identities::add); + return identities; } - private static final IdentityParser v1IdentityParser = new IdentityParserV1(); - private static final IdentityParser v2IdentityParser = new IdentityParserV2(); - private static Identity createIdentity(String identity, String nickname, String requestUri, @SuppressWarnings("unused") String insertUri, Set contexts, Map properties) { return new Identity(identity, nickname, requestUri, contexts, properties); } diff --git a/src/test/java/net/pterodactylus/fcp/plugin/IdentityParserV3Test.java b/src/test/java/net/pterodactylus/fcp/plugin/IdentityParserV3Test.java new file mode 100644 index 0000000..dbd420e --- /dev/null +++ b/src/test/java/net/pterodactylus/fcp/plugin/IdentityParserV3Test.java @@ -0,0 +1,281 @@ +package net.pterodactylus.fcp.plugin; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.pterodactylus.fcp.plugin.IdentityParserV3.FieldNamer; +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; +import org.junit.Test; + +import static net.pterodactylus.fcp.test.IdentityMatchers.hasContexts; +import static net.pterodactylus.fcp.test.IdentityMatchers.hasId; +import static net.pterodactylus.fcp.test.IdentityMatchers.hasInsertUri; +import static net.pterodactylus.fcp.test.IdentityMatchers.hasNickname; +import static net.pterodactylus.fcp.test.IdentityMatchers.hasProperties; +import static net.pterodactylus.fcp.test.IdentityMatchers.hasRequestUri; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThrows; + +public class IdentityParserV3Test { + + @Test + public void identityParserCanParseFirstDeprecatedFormat() { + addFieldsWithIdentityAndWithoutSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("first", 0)); + }); + } + + @Test + public void identityParserCanParseFirstDeprecatedFormatWithID() { + addFieldsWithIDAndWithoutSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("first", 0)); + }); + } + + @Test + public void identityParserCanParseSecondAlsoDeprecatedFormat() { + addFieldsWithIdentityAndWithNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("second", 0)); + }); + } + + @Test + public void identityParserCanParseSecondAlsoDeprecatedFormatWithID() { + addFieldsWithIDAndWithNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("second", 0)); + }); + } + + @Test + public void identityParserCanParseThirdNotDeprecatedFormat() { + addFieldsWithIdentityAndWithDotNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("third", 0)); + }); + } + + @Test + public void identityParserCanParseThirdNotDeprecatedFormatWithID() { + addFieldsWithIDAndWithDotNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("third", 0)); + }); + } + + @Test + public void identityParserPrefersSecondOverFirstFormat() { + addFieldsWithIdentityAndWithoutSuffix(); + addFieldsWithIdentityAndWithNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("second", 0)); + }); + } + + @Test + public void identityParserPrefersThirdOverFirstFormat() { + addFieldsWithIdentityAndWithoutSuffix(); + addFieldsWithIdentityAndWithDotNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("third", 0)); + }); + } + + @Test + public void identityParserPrefersThirdOverSecondFormat() { + addFieldsWithIdentityAndWithNumericSuffix(); + addFieldsWithIdentityAndWithDotNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + assertThat(iterator.next(), matches("third", 0)); + }); + } + + @Test + public void identityParserParsesExactlyOneIdentity() { + addFieldsWithIdentityAndWithDotNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + iterator.next(); + assertThat(iterator.hasNext(), equalTo(false)); + }); + } + + @Test + public void identityParserThrowsExceptionWhenIteratingPastTheEndOfTheIterator() { + addFieldsWithIdentityAndWithDotNumericSuffix(); + createParserAndVerifyIterator(iterator -> { + iterator.next(); + assertThrows(NoSuchElementException.class, () -> iterator.next()); + }); + } + + @Test + public void identityParserParsesAllIdentitiesFromFields() { + addFieldsWithIdentityAndWithDotNumericSuffix("Identity", 5); + createParserAndVerify(parser -> { + assertThat(parser, containsInAnyOrder( + matches("third", 0), + matches("third", 1), + matches("third", 2), + matches("third", 3), + matches("third", 4) + )); + }); + } + + @Test + public void identityParserParsesExtraFieldsCorrectlyWithFirstFormat() { + addFieldsWithIDAndWithoutSuffix(); + fields.put("Extra", "extra"); + fields.put("Field", "field"); + IdentityParserV3 parser = new IdentityParserV3<>(fields, ownIdentityGenerator, + new FieldNamer("Extra", "Extras.{0}.Extra", "Extra{0}", "Extra"), + new FieldNamer("Field", "Extras.{0}.Field", "Field{0}", "Field") + ); + IdentityParserV3.IdentityIterator identityIterator = parser.iterator(); + identityIterator.next(); + assertThat(identityIterator.getExtraField("Extra"), equalTo("extra")); + assertThat(identityIterator.getExtraField("Field"), equalTo("field")); + } + + @Test + public void identityParserParsesExtraFieldsCorrectlyWithSecondFormat() { + addFieldsWithIDAndWithoutSuffix(); + fields.put("Extra0", "extra"); + fields.put("Field0", "field"); + IdentityParserV3 parser = new IdentityParserV3<>(fields, ownIdentityGenerator, + new FieldNamer("Extra", "Extras.{0}.Extra", "Extra{0}", "Extra"), + new FieldNamer("Field", "Extras.{0}.Field", "Field{0}", "Field") + ); + IdentityParserV3.IdentityIterator identityIterator = parser.iterator(); + identityIterator.next(); + assertThat(identityIterator.getExtraField("Extra"), equalTo("extra")); + assertThat(identityIterator.getExtraField("Field"), equalTo("field")); + } + + @Test + public void identityParserParsesExtraFieldsCorrectlyWithThirdFormat() { + addFieldsWithIDAndWithoutSuffix(); + fields.put("Extras.0.Extra", "extra"); + fields.put("Extras.0.Field", "field"); + IdentityParserV3 parser = new IdentityParserV3<>(fields, ownIdentityGenerator, + new FieldNamer("Extra", "Extras.{0}.Extra", "Extra{0}", "Extra"), + new FieldNamer("Field", "Extras.{0}.Field", "Field{0}", "Field") + ); + IdentityParserV3.IdentityIterator identityIterator = parser.iterator(); + identityIterator.next(); + MatcherAssert.assertThat(identityIterator.getExtraField("Extra"), equalTo("extra")); + MatcherAssert.assertThat(identityIterator.getExtraField("Field"), equalTo("field")); + } + + private Matcher matches(String prefix, int index) { + return allOf( + hasId(prefix + "-id" + index), + hasNickname(prefix + "-nick" + index), + hasRequestUri(prefix + "-r" + index), + hasInsertUri(prefix + "-i" + index), + hasContexts(containsInAnyOrder(prefix + "-context-" + index, prefix + "-context-" + (index + 1))), + hasProperties(allOf(hasEntry(prefix + "-name-" + index, prefix + "-value-" + index), hasEntry(prefix + "-name-" + (index + 1), prefix + "-value-" + (index + 1)))) + ); + } + + private void addFieldsWithIdentityAndWithoutSuffix() { + fields.put("Identity", "first-id0"); + addFieldsWithoutSuffix(); + } + + private void addFieldsWithIDAndWithoutSuffix() { + fields.put("ID", "first-id0"); + addFieldsWithoutSuffix(); + } + + private void addFieldsWithoutSuffix() { + fields.put("Nickname", "first-nick0"); + fields.put("RequestURI", "first-r0"); + fields.put("InsertURI", "first-i0"); + fields.put("Context0", "first-context-0"); + fields.put("Context1", "first-context-1"); + fields.put("Property0.Name", "first-name-0"); + fields.put("Property0.Value", "first-value-0"); + fields.put("Property1.Name", "first-name-1"); + fields.put("Property1.Value", "first-value-1"); + } + + private void addFieldsWithIdentityAndWithNumericSuffix() { + fields.put("Identity0", "second-id0"); + addFieldsWithNumericSuffix(); + } + + private void addFieldsWithIDAndWithNumericSuffix() { + fields.put("ID0", "second-id0"); + addFieldsWithNumericSuffix(); + } + + private void addFieldsWithNumericSuffix() { + fields.put("Nickname0", "second-nick0"); + fields.put("RequestURI0", "second-r0"); + fields.put("InsertURI0", "second-i0"); + fields.put("Contexts0.Context0", "second-context-0"); + fields.put("Contexts0.Context1", "second-context-1"); + fields.put("Properties0.Property0.Name", "second-name-0"); + fields.put("Properties0.Property0.Value", "second-value-0"); + fields.put("Properties0.Property1.Name", "second-name-1"); + fields.put("Properties0.Property1.Value", "second-value-1"); + } + + private void addFieldsWithIdentityAndWithDotNumericSuffix() { + addFieldsWithIdentityAndWithDotNumericSuffix("Identity", 1); + } + + private void addFieldsWithIDAndWithDotNumericSuffix() { + addFieldsWithIdentityAndWithDotNumericSuffix("ID", 1); + } + + private void addFieldsWithIdentityAndWithDotNumericSuffix(String idField, int count) { + Stream.iterate(0, index -> index + 1) + .limit(count) + .forEach(index -> { + fields.put("Identities." + index + "." + idField, "third-id" + index); + fields.put("Identities." + index + ".Nickname", "third-nick" + index); + fields.put("Identities." + index + ".RequestURI", "third-r" + index); + fields.put("Identities." + index + ".InsertURI", "third-i" + index); + fields.put("Identities." + index + ".Contexts.0.Name", "third-context-" + index); + fields.put("Identities." + index + ".Contexts.1.Name", "third-context-" + (index + 1)); + fields.put("Identities." + index + ".Properties.0.Name", "third-name-" + index); + fields.put("Identities." + index + ".Properties.0.Value", "third-value-" + index); + fields.put("Identities." + index + ".Properties.1.Name", "third-name-" + (index + 1)); + fields.put("Identities." + index + ".Properties.1.Value", "third-value-" + (index + 1)); + }); + } + + private void createParserAndVerify(Consumer> iteratorConsumer, FieldNamer... extraFields) { + IdentityParserV3 parser = new IdentityParserV3<>(fields, ownIdentityGenerator, extraFields); + iteratorConsumer.accept(parser); + } + + private void createParserAndVerifyIterator(Consumer> iteratorConsumer) { + createParserAndVerify(parser -> { + Iterator iterator = parser.iterator(); + iteratorConsumer.accept(iterator); + }); + } + + private OwnIdentity createOwnIdentity(String identity, String nickname, String requestUri, String insertUri, Set contexts, Map properties) { + return new OwnIdentity(identity, nickname, requestUri, insertUri, contexts, properties); + } + + private final Map fields = new HashMap<>(); + private final IdentityGenerator ownIdentityGenerator = this::createOwnIdentity; + +}