offer alternative access to elements and children
instead of Element.findChild(name, namespace) we can now use Element.getExtension(Extension.class) for registered extensions
This commit is contained in:
parent
49bf92f7ca
commit
9e7bbcc272
|
@ -55,6 +55,8 @@ dependencies {
|
||||||
|
|
||||||
implementation "androidx.security:security-crypto:1.0.0"
|
implementation "androidx.security:security-crypto:1.0.0"
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'org.robolectric:robolectric:4.9'
|
||||||
|
|
||||||
|
|
||||||
// legacy dependencies. Ideally everything below should be carefully reviewed and eventually moved up
|
// legacy dependencies. Ideally everything below should be carefully reviewed and eventually moved up
|
||||||
|
@ -137,6 +139,12 @@ android {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
universalApk true
|
universalApk true
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "4a70ff0733436f5a2a08e7abb8e6cc95",
|
"identityHash": "d16845c3eb73e5fdbc9902903b74428a",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "account",
|
"tableName": "account",
|
||||||
|
@ -590,7 +590,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "message",
|
"tableName": "message",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `chatId` INTEGER NOT NULL, `receivedAt` INTEGER, `sentAt` INTEGER, `bareTo` TEXT, `toResource` TEXT, `bareFrom` TEXT, `fromResource` TEXT, `occupantId` TEXT, `messageId` TEXT, `stanzaId` TEXT, FOREIGN KEY(`chatId`) REFERENCES `chat`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `chatId` INTEGER NOT NULL, `receivedAt` INTEGER, `sentAt` INTEGER, `bareTo` TEXT, `toResource` TEXT, `bareFrom` TEXT, `fromResource` TEXT, `occupantId` TEXT, `messageId` TEXT, `stanzaId` TEXT, `acknowledged` INTEGER NOT NULL, FOREIGN KEY(`chatId`) REFERENCES `chat`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -657,6 +657,12 @@
|
||||||
"columnName": "stanzaId",
|
"columnName": "stanzaId",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "acknowledged",
|
||||||
|
"columnName": "acknowledged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
|
@ -1189,7 +1195,7 @@
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4a70ff0733436f5a2a08e7abb8e6cc95')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd16845c3eb73e5fdbc9902903b74428a')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,18 +1,19 @@
|
||||||
package eu.siacs.conversations.xml;
|
package eu.siacs.conversations.xml;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Hashtable;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.utils.XmlHelper;
|
import eu.siacs.conversations.utils.XmlHelper;
|
||||||
import eu.siacs.conversations.xmpp.InvalidJid;
|
import eu.siacs.conversations.xmpp.InvalidJid;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Hashtable;
|
||||||
|
import java.util.List;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class Element {
|
public class Element {
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -65,6 +66,19 @@ public class Element {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <E extends Extension> E getExtension(final Class<E> clazz) {
|
||||||
|
final var extension = Iterables.find(this.children, clazz::isInstance);
|
||||||
|
if (extension == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return clazz.cast(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <E extends Extension> Collection<E> getExtensions(final Class<E> clazz) {
|
||||||
|
return Collections2.transform(
|
||||||
|
Collections2.filter(this.children, clazz::isInstance), clazz::cast);
|
||||||
|
}
|
||||||
|
|
||||||
public String findChildContent(String name) {
|
public String findChildContent(String name) {
|
||||||
Element element = findChild(name);
|
Element element = findChild(name);
|
||||||
return element == null ? null : element.getContent();
|
return element == null ? null : element.getContent();
|
||||||
|
|
|
@ -2,16 +2,14 @@ package eu.siacs.conversations.xml;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Xml;
|
import android.util.Xml;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
import im.conversations.android.xmpp.Extensions;
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
import eu.siacs.conversations.Config;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
public class XmlReader implements Closeable {
|
public class XmlReader implements Closeable {
|
||||||
private final XmlPullParser parser;
|
private final XmlPullParser parser;
|
||||||
|
@ -82,13 +80,22 @@ public class XmlReader implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
throw new IOException("xml parser mishandled "+throwable.getClass().getSimpleName()+"("+throwable.getMessage()+")", throwable);
|
throw new IOException(
|
||||||
|
"xml parser mishandled "
|
||||||
|
+ throwable.getClass().getSimpleName()
|
||||||
|
+ "("
|
||||||
|
+ throwable.getMessage()
|
||||||
|
+ ")",
|
||||||
|
throwable);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Element readElement(Tag currentTag) throws IOException {
|
public Element readElement(Tag currentTag) throws IOException {
|
||||||
Element element = new Element(currentTag.getName());
|
final var attributes = currentTag.getAttributes();
|
||||||
|
final var namespace = attributes.get("xmlns");
|
||||||
|
final var name = currentTag.getName();
|
||||||
|
final Element element = Extensions.create(name, namespace);
|
||||||
element.setAttributes(currentTag.getAttributes());
|
element.setAttributes(currentTag.getAttributes());
|
||||||
Tag nextTag = this.readTag();
|
Tag nextTag = this.readTag();
|
||||||
if (nextTag == null) {
|
if (nextTag == null) {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package im.conversations.android.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
public @interface XmlElement {
|
||||||
|
|
||||||
|
String name() default "";
|
||||||
|
|
||||||
|
String namespace() default "";
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package im.conversations.android.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.PACKAGE)
|
||||||
|
public @interface XmlPackage {
|
||||||
|
String namespace();
|
||||||
|
}
|
114
src/main/java/im/conversations/android/xmpp/Extensions.java
Normal file
114
src/main/java/im/conversations/android/xmpp/Extensions.java
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.annotation.XmlPackage;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import im.conversations.android.xmpp.model.roster.Item;
|
||||||
|
import im.conversations.android.xmpp.model.roster.Query;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class Extensions {
|
||||||
|
|
||||||
|
private static final List<Class<? extends Extension>> ELEMENTS =
|
||||||
|
Arrays.asList(Query.class, Item.class);
|
||||||
|
|
||||||
|
private static final Map<Id, Class<? extends Extension>> EXTENSION_CLASS_MAP;
|
||||||
|
|
||||||
|
static {
|
||||||
|
final var builder = new ImmutableMap.Builder<Id, Class<? extends Extension>>();
|
||||||
|
for (final Class<? extends Extension> clazz : ELEMENTS) {
|
||||||
|
builder.put(id(clazz), clazz);
|
||||||
|
}
|
||||||
|
EXTENSION_CLASS_MAP = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Id id(final Class<? extends Extension> clazz) {
|
||||||
|
final XmlElement xmlElement = clazz.getAnnotation(XmlElement.class);
|
||||||
|
final Package clazzPackage = clazz.getPackage();
|
||||||
|
final XmlPackage xmlPackage =
|
||||||
|
clazzPackage == null ? null : clazzPackage.getAnnotation(XmlPackage.class);
|
||||||
|
if (xmlElement == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format("%s is not annotated as @XmlElement", clazz.getName()));
|
||||||
|
}
|
||||||
|
final String packageNamespace = xmlPackage == null ? null : xmlPackage.namespace();
|
||||||
|
final String elementName = xmlElement.name();
|
||||||
|
final String elementNamespace = xmlElement.namespace();
|
||||||
|
final String namespace;
|
||||||
|
if (!Strings.isNullOrEmpty(elementNamespace)) {
|
||||||
|
namespace = elementNamespace;
|
||||||
|
} else if (!Strings.isNullOrEmpty(packageNamespace)) {
|
||||||
|
namespace = packageNamespace;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format("%s does not declare a namespace", clazz.getName()));
|
||||||
|
}
|
||||||
|
final String name;
|
||||||
|
if (Strings.isNullOrEmpty(elementName)) {
|
||||||
|
name = clazz.getSimpleName().toLowerCase(Locale.ROOT);
|
||||||
|
} else {
|
||||||
|
name = elementName;
|
||||||
|
}
|
||||||
|
return new Id(name, namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Element create(final String name, final String namespace) {
|
||||||
|
final Class<? extends Element> clazz = of(name, namespace);
|
||||||
|
if (clazz == null) {
|
||||||
|
return new Element(name, namespace);
|
||||||
|
}
|
||||||
|
final Constructor<? extends Element> constructor;
|
||||||
|
try {
|
||||||
|
constructor = clazz.getDeclaredConstructor();
|
||||||
|
} catch (final NoSuchMethodException e) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format("%s has no default constructor", clazz.getName()));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return constructor.newInstance();
|
||||||
|
} catch (final IllegalAccessException
|
||||||
|
| InstantiationException
|
||||||
|
| InvocationTargetException e) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format("%s has inaccessible default constructor", clazz.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<? extends Element> of(final String name, final String namespace) {
|
||||||
|
return EXTENSION_CLASS_MAP.get(new Id(name, namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Extensions() {}
|
||||||
|
|
||||||
|
public static class Id {
|
||||||
|
public final String name;
|
||||||
|
public final String namespace;
|
||||||
|
|
||||||
|
public Id(String name, String namespace) {
|
||||||
|
this.name = name;
|
||||||
|
this.namespace = namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Id id = (Id) o;
|
||||||
|
return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(name, namespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package im.conversations.android.xmpp.model;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
|
||||||
|
public class Extension extends Element {
|
||||||
|
public Extension(String name, String xmlns) {
|
||||||
|
super(name, xmlns);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package im.conversations.android.xmpp.model.roster;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement
|
||||||
|
public class Item extends Extension {
|
||||||
|
|
||||||
|
public Item() {
|
||||||
|
super("item", Namespace.ROSTER);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package im.conversations.android.xmpp.model.roster;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(name = "query", namespace = Namespace.ROSTER)
|
||||||
|
public class Query extends Extension {
|
||||||
|
|
||||||
|
public Query() {
|
||||||
|
super("query", Namespace.ROSTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(final String rosterVersion) {
|
||||||
|
this.setAttribute("ver", rosterVersion);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
@XmlPackage(namespace = Namespace.ROSTER)
|
||||||
|
package im.conversations.android.xmpp.model.roster;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlPackage;
|
|
@ -4,10 +4,11 @@ import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import im.conversations.android.xmpp.model.roster.Item;
|
||||||
|
import im.conversations.android.xmpp.model.roster.Query;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class BindProcessor extends AbstractBaseProcessor implements Consumer<Jid> {
|
public class BindProcessor extends AbstractBaseProcessor implements Consumer<Jid> {
|
||||||
|
@ -43,12 +44,27 @@ public class BindProcessor extends AbstractBaseProcessor implements Consumer<Jid
|
||||||
final var database = getDatabase();
|
final var database = getDatabase();
|
||||||
final String rosterVersion = database.accountDao().getRosterVersion(account.id);
|
final String rosterVersion = database.accountDao().getRosterVersion(account.id);
|
||||||
final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
|
final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
|
||||||
|
final Query rosterQuery = new Query();
|
||||||
|
iqPacket.addChild(rosterQuery);
|
||||||
if (Strings.isNullOrEmpty(rosterVersion)) {
|
if (Strings.isNullOrEmpty(rosterVersion)) {
|
||||||
Log.d(Config.LOGTAG, account.address + ": fetching roster");
|
Log.d(Config.LOGTAG, account.address + ": fetching roster");
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG, account.address + ": fetching roster version " + rosterVersion);
|
Log.d(Config.LOGTAG, account.address + ": fetching roster version " + rosterVersion);
|
||||||
|
rosterQuery.setVersion(rosterVersion);
|
||||||
}
|
}
|
||||||
iqPacket.query(Namespace.ROSTER).setAttribute("ver", rosterVersion);
|
connection.sendIqPacket(
|
||||||
connection.sendIqPacket(iqPacket, result -> {});
|
iqPacket,
|
||||||
|
result -> {
|
||||||
|
if (result.getType() != IqPacket.TYPE.RESULT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Query query = result.getExtension(Query.class);
|
||||||
|
if (query == null) {
|
||||||
|
// No query in result means further modifications are sent via pushes
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO delete entire roster
|
||||||
|
for (final Item item : query.getExtensions(Item.class)) {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xml.XmlElementReader;
|
||||||
|
import im.conversations.android.xmpp.model.roster.Item;
|
||||||
|
import im.conversations.android.xmpp.model.roster.Query;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collection;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.annotation.ConscryptMode;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@ConscryptMode(ConscryptMode.Mode.OFF)
|
||||||
|
public class XmlElementReaderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void readRosterItems() throws IOException {
|
||||||
|
final String xml =
|
||||||
|
"<query xmlns='jabber:iq:roster'><item subscription='none' jid='a@b.c'/><item"
|
||||||
|
+ " subscription='both' jid='d@e.f' ask='subscribe'/></query>";
|
||||||
|
final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
|
||||||
|
assertThat(element, instanceOf(Query.class));
|
||||||
|
final Query query = (Query) element;
|
||||||
|
final Collection<Item> items = query.getExtensions(Item.class);
|
||||||
|
assertEquals(2, items.size());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue