Add (IQ|PacketExtension)IntrospectionProvider

This simplifies code as there is no longer a distinction between
"normal" providers and introspection providers in ProviderManager
necessary.

It's also easier to get an idea where introspection is used for parsing.
This commit is contained in:
Florian Schmaus 2014-12-10 11:06:04 +01:00
parent 00f5008794
commit 77f0fdc156
11 changed files with 284 additions and 212 deletions

View File

@ -17,13 +17,16 @@ There are two types of providers:
* jabber:iq:roster
* jabber:iq:register There are many more IQ types and extensions that are part of XMPP standards, and of course an endless number that can be added as custom extensions. To support this, an extensible parsing mechanism is provided via Smack and user build providers.
Whenever a packet extension is found in a packet, parsing will be passed to
the correct provider. Each provider can either implement the
PacketExtensionProvider interface or be a standard Java Bean. In the former
case, each extension provider is responsible for parsing the raw XML stream,
via the [XML Pull Parser](http://www.xmlpull.org/), to contruct an object. In
the latter case, bean introspection is used to try to automatically set the
properties of the class using the values in the packet extension sub-element.
Whenever a packet extension is found in a packet, parsing will be
passed to the correct provider. Each provider must implement the
PacketExtensionProvider interface. Each extension provider is
responsible for parsing the raw XML stream, via the
[XML Pull Parser](http://www.xmlpull.org/), to contruct an object.
You can also create an introspection provider
(`provider.IntrospectionProvider.PacketExtensionIntrospectionProvider`). Here,
bean introspection is used to try to automatically set the properties
of the class using the values in the packet extension sub-element.
When no extension provider is registered for an element name and namespace
combination, Smack will store all top-level elements of the sub-packet in the
@ -58,17 +61,17 @@ or
IQ Providers
------------
The IQ provider class can either implement the IQProvider interface, or extend
the IQ class. In the former case, each IQProvider is responsible for parsing
the raw XML stream to create an IQ instance. In the latter case, bean
introspection is used to try to automatically set properties of the IQ
instance using the values found in the IQ packet XML. For example, an XMPP
time packet resembles the following:
The IQ provider class must implement the IQProvider interface. Each
IQProvider is responsible for parsing the raw XML stream to create an
IQ instance.
### Introspection (DEPRECATED)
You can also create an introspection provider
(`provider.IntrospectionProvider.IQIntrospectionProvider`). Which
uses, bean introspection to try to automatically set properties of the
IQ instance using the values found in the IQ packet XML. For example,
an XMPP time packet resembles the following:
*Note*: This feature is deprecated, using introspection for parsing is not recommended.
Instead implement your own provider like shown in the next section.
### Introspection
_Time Packet_
@ -111,6 +114,18 @@ _Time IQ Class_
}
}
_Time Provider_
```java
public class TimeProvider extends IQIntrospectionProvider<Time> {
public TimeProvider() {
super(Time.class);
}
}
```
The introspection service will automatically try to convert the String value
from the XML into a boolean, int, long, float, double, or Class depending on
the type the IQ instance expects.

View File

@ -38,14 +38,4 @@ public final class ExtensionProviderInfo extends AbstractProviderInfo {
super(elementName, namespace, extProvider);
}
/**
* Defines an extension provider which is adheres to the JavaBean spec for parsing the extension.
*
* @param elementName Element that provider parses.
* @param namespace Namespace that provider parses.
* @param beanClass The provider bean class.
*/
public ExtensionProviderInfo(String elementName, String namespace, Class<?> beanClass) {
super(elementName, namespace, beanClass);
}
}

View File

@ -37,15 +37,4 @@ public final class IQProviderInfo extends AbstractProviderInfo {
public IQProviderInfo(String elementName, String namespace, IQProvider<IQ> iqProvider) {
super(elementName, namespace, iqProvider);
}
/**
* Defines an IQ class which can be used as a provider via introspection.
*
* @param elementName Element that provider parses.
* @param namespace Namespace that provider parses.
* @param iqProviderClass The IQ class being parsed.
*/
public IQProviderInfo(String elementName, String namespace, Class<? extends IQ> iqProviderClass) {
super(elementName, namespace, iqProviderClass);
}
}

View File

@ -0,0 +1,143 @@
/**
*
* Copyright © 2014 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.provider;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.util.ParserUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
public class IntrospectionProvider{
// Unfortunately, we have to create two introspection providers, with the exactly the same code here
public static abstract class IQIntrospectionProvider<I extends IQ> extends IQProvider<I> {
private final Class<I> elementClass;
protected IQIntrospectionProvider(Class<I> elementClass) {
this.elementClass = elementClass;
}
@SuppressWarnings("unchecked")
@Override
public I parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException,
SmackException {
try {
return (I) parseWithIntrospection(elementClass, parser, initialDepth);
}
catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException | ClassNotFoundException e) {
throw new SmackException(e);
}
}
}
public static abstract class PacketExtensionIntrospectionProvider<PE extends PacketExtension> extends PacketExtensionProvider<PE> {
private final Class<PE> elementClass;
protected PacketExtensionIntrospectionProvider(Class<PE> elementClass) {
this.elementClass = elementClass;
}
@SuppressWarnings("unchecked")
@Override
public PE parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException,
SmackException {
try {
return (PE) parseWithIntrospection(elementClass, parser, initialDepth);
}
catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException | ClassNotFoundException e) {
throw new SmackException(e);
}
}
}
public static Object parseWithIntrospection(Class<?> objectClass,
XmlPullParser parser, final int initialDepth) throws NoSuchMethodException, SecurityException,
InstantiationException, IllegalAccessException, XmlPullParserException,
IOException, IllegalArgumentException, InvocationTargetException,
ClassNotFoundException {
ParserUtils.assertAtStartTag(parser);
Object object = objectClass.newInstance();
outerloop: while (true) {
int eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
String name = parser.getName();
String stringValue = parser.nextText();
Class<?> propertyType = object.getClass().getMethod(
"get" + Character.toUpperCase(name.charAt(0)) + name.substring(1)).getReturnType();
// Get the value of the property by converting it from a
// String to the correct object type.
Object value = decode(propertyType, stringValue);
// Set the value of the bean.
object.getClass().getMethod(
"set" + Character.toUpperCase(name.charAt(0)) + name.substring(1),
propertyType).invoke(object, value);
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() == initialDepth) {
break outerloop;
}
break;
}
}
ParserUtils.assertAtEndTag(parser);
return object;
}
/**
* Decodes a String into an object of the specified type. If the object
* type is not supported, null will be returned.
*
* @param type the type of the property.
* @param value the encode String value to decode.
* @return the String value decoded into the specified type.
* @throws ClassNotFoundException
*/
private static Object decode(Class<?> type, String value) throws ClassNotFoundException {
if (type.getName().equals("java.lang.String")) {
return value;
}
if (type.getName().equals("boolean")) {
return Boolean.valueOf(value);
}
if (type.getName().equals("int")) {
return Integer.valueOf(value);
}
if (type.getName().equals("long")) {
return Long.valueOf(value);
}
if (type.getName().equals("float")) {
return Float.valueOf(value);
}
if (type.getName().equals("double")) {
return Double.valueOf(value);
}
if (type.getName().equals("java.lang.Class")) {
return Class.forName(value);
}
return null;
}
}

View File

@ -85,13 +85,8 @@ public class ProviderFileLoader implements ProviderLoader {
if (IQProvider.class.isAssignableFrom(provider)) {
iqProviders.add(new IQProviderInfo(elementName, namespace, (IQProvider<IQ>) provider.newInstance()));
}
else if (IQ.class.isAssignableFrom(provider)) {
iqProviders.add(new IQProviderInfo(elementName, namespace, (Class<? extends IQ>)provider));
}
else {
exceptions.add(new IllegalArgumentException(
className
+ " is neither IQProvider or IQ class"));
exceptions.add(new IllegalArgumentException(className + " is not a IQProvider"));
}
break;
case "extensionProvider":
@ -103,13 +98,9 @@ public class ProviderFileLoader implements ProviderLoader {
if (PacketExtensionProvider.class.isAssignableFrom(provider)) {
extProviders.add(new ExtensionProviderInfo(elementName, namespace, (PacketExtensionProvider<PacketExtension>) provider.newInstance()));
}
else if (PacketExtension.class.isAssignableFrom(provider)) {
extProviders.add(new ExtensionProviderInfo(elementName, namespace, provider));
}
else {
exceptions.add(new IllegalArgumentException(
className
+ " is neither PacketExtensionProvider or PacketExtension class"));
exceptions.add(new IllegalArgumentException(className
+ " is not a PacketExtensionProvider"));
}
break;
case "streamFeatureProvider":

View File

@ -18,7 +18,6 @@
package org.jivesoftware.smack.provider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -112,8 +111,6 @@ public final class ProviderManager {
private static final Map<String, PacketExtensionProvider<PacketExtension>> extensionProviders = new ConcurrentHashMap<String, PacketExtensionProvider<PacketExtension>>();
private static final Map<String, IQProvider<IQ>> iqProviders = new ConcurrentHashMap<String, IQProvider<IQ>>();
private static final Map<String, Class<?>> extensionIntrospectionProviders = new ConcurrentHashMap<String, Class<?>>();
private static final Map<String, Class<?>> iqIntrospectionProviders = new ConcurrentHashMap<String, Class<?>>();
private static final Map<String, PacketExtensionProvider<PacketExtension>> streamFeatureProviders = new ConcurrentHashMap<String, PacketExtensionProvider<PacketExtension>>();
static {
@ -171,11 +168,6 @@ public final class ProviderManager {
return iqProviders.get(key);
}
public static Class<?> getIQIntrospectionProvider(String elementName, String namespace) {
String key = getKey(elementName, namespace);
return iqIntrospectionProviders.get(key);
}
/**
* Returns an unmodifiable collection of all IQProvider instances. Each object
* in the collection will either be an IQProvider instance, or a Class object
@ -183,11 +175,10 @@ public final class ProviderManager {
*
* @return all IQProvider instances.
*/
public static List<Object> getIQProviders() {
List<Object> providers = new ArrayList<Object>(iqProviders.size() + iqIntrospectionProviders.size());
public static List<IQProvider<IQ>> getIQProviders() {
List<IQProvider<IQ>> providers = new ArrayList<>(iqProviders.size());
providers.addAll(iqProviders.values());
providers.addAll(iqIntrospectionProviders.values());
return Collections.unmodifiableList(providers);
return providers;
}
/**
@ -208,11 +199,8 @@ public final class ProviderManager {
String key = removeIQProvider(elementName, namespace);
if (provider instanceof IQProvider) {
iqProviders.put(key, (IQProvider<IQ>) provider);
} else if (provider instanceof Class && IQ.class.isAssignableFrom((Class<?>) provider)) {
iqIntrospectionProviders.put(key, (Class<?>) provider);
} else {
throw new IllegalArgumentException("Provider must be an IQProvider " +
"or a Class instance sublcassing IQ.");
throw new IllegalArgumentException("Provider must be an IQProvider");
}
}
@ -228,7 +216,6 @@ public final class ProviderManager {
public static String removeIQProvider(String elementName, String namespace) {
String key = getKey(elementName, namespace);
iqProviders.remove(key);
iqIntrospectionProviders.remove(key);
return key;
}
@ -256,11 +243,6 @@ public final class ProviderManager {
return extensionProviders.get(key);
}
public static Class<?> getExtensionIntrospectionProvider(String elementName, String namespace) {
String key = getKey(elementName, namespace);
return extensionIntrospectionProviders.get(key);
}
/**
* Adds an extension provider with the specified element name and name space. The provider
* will override any providers loaded through the classpath. The provider must be either
@ -279,11 +261,8 @@ public final class ProviderManager {
String key = removeExtensionProvider(elementName, namespace);
if (provider instanceof PacketExtensionProvider) {
extensionProviders.put(key, (PacketExtensionProvider<PacketExtension>) provider);
} else if (provider instanceof Class && PacketExtension.class.isAssignableFrom((Class<?>) provider)) {
extensionIntrospectionProviders.put(key, (Class<?>) provider);
} else {
throw new IllegalArgumentException("Provider must be a PacketExtensionProvider " +
"or a Class instance.");
throw new IllegalArgumentException("Provider must be a PacketExtensionProvider");
}
}
@ -299,7 +278,6 @@ public final class ProviderManager {
public static String removeExtensionProvider(String elementName, String namespace) {
String key = getKey(elementName, namespace);
extensionProviders.remove(key);
extensionIntrospectionProviders.remove(key);
return key;
}
@ -310,11 +288,10 @@ public final class ProviderManager {
*
* @return all PacketExtensionProvider instances.
*/
public static List<Object> getExtensionProviders() {
List<Object> providers = new ArrayList<Object>(extensionProviders.size() + extensionIntrospectionProviders.size());
public static List<PacketExtensionProvider<PacketExtension>> getExtensionProviders() {
List<PacketExtensionProvider<PacketExtension>> providers = new ArrayList<>(extensionProviders.size());
providers.addAll(extensionProviders.values());
providers.addAll(extensionIntrospectionProviders.values());
return Collections.unmodifiableList(providers);
return providers;
}
public static PacketExtensionProvider<PacketExtension> getStreamFeatureProvider(String elementName, String namespace) {

View File

@ -19,7 +19,6 @@ package org.jivesoftware.smack.util;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@ -627,23 +626,15 @@ public class PacketParserUtils {
IQProvider<IQ> provider = ProviderManager.getIQProvider(elementName, namespace);
if (provider != null) {
iqPacket = provider.parse(parser);
} else {
Class<?> introspectionProvider = ProviderManager.getIQIntrospectionProvider(
elementName, namespace);
if (introspectionProvider != null) {
iqPacket = (IQ) PacketParserUtils.parseWithIntrospection(
elementName, introspectionProvider,
parser);
}
// Only handle unknown IQs of type result. Types of 'get' and 'set' which are not understood
// have to be answered with an IQ error response. See the code a few lines below
// Note that if we reach this code, it is guranteed that the result IQ contained a child element
// (RFC 6120 § 8.2.3 6) because otherwhise we would have reached the END_TAG first.
else if (IQ.Type.result == type){
// No Provider found for the IQ stanza, parse it to an UnparsedIQ instance
// so that the content of the IQ can be examined later on
iqPacket = new UnparsedResultIQ(elementName, namespace, parseElement(parser));
}
}
// Only handle unknown IQs of type result. Types of 'get' and 'set' which are not understood
// have to be answered with an IQ error response. See the code a few lines below
// Note that if we reach this code, it is guranteed that the result IQ contained a child element
// (RFC 6120 § 8.2.3 6) because otherwhise we would have reached the END_TAG first.
else if (IQ.Type.result == type) {
// No Provider found for the IQ stanza, parse it to an UnparsedIQ instance
// so that the content of the IQ can be examined later on
iqPacket = new UnparsedResultIQ(elementName, namespace, parseElement(parser));
}
break;
}
@ -931,18 +922,6 @@ public class PacketParserUtils {
if (provider != null) {
return provider.parse(parser);
}
Class<?> introspectionProvider = ProviderManager.getExtensionIntrospectionProvider(elementName, namespace);
if (introspectionProvider != null) {
try {
return (PacketExtension)parseWithIntrospection(elementName, introspectionProvider, parser);
} catch (NoSuchMethodException | SecurityException
| InstantiationException | IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| ClassNotFoundException e) {
throw new SmackException(e);
}
}
final int initialDepth = parser.getDepth();
// No providers registered, so use a default extension.
@ -1013,37 +992,6 @@ public class PacketParserUtils {
return null;
}
public static Object parseWithIntrospection(String elementName, Class<?> objectClass,
XmlPullParser parser) throws NoSuchMethodException, SecurityException,
InstantiationException, IllegalAccessException, XmlPullParserException,
IOException, IllegalArgumentException, InvocationTargetException,
ClassNotFoundException {
boolean done = false;
Object object = objectClass.newInstance();
while (!done) {
int eventType = parser.next();
if (eventType == XmlPullParser.START_TAG) {
String name = parser.getName();
String stringValue = parser.nextText();
Class<?> propertyType = object.getClass().getMethod(
"get" + Character.toUpperCase(name.charAt(0)) + name.substring(1)).getReturnType();
// Get the value of the property by converting it from a
// String to the correct object type.
Object value = decode(propertyType, stringValue);
// Set the value of the bean.
object.getClass().getMethod(
"set" + Character.toUpperCase(name.charAt(0)) + name.substring(1),
propertyType).invoke(object, value);
}
else if (eventType == XmlPullParser.END_TAG) {
if (parser.getName().equals(elementName)) {
done = true;
}
}
}
return object;
}
public static void addPacketExtension(Packet packet, XmlPullParser parser) throws XmlPullParserException,
IOException, SmackException {
ParserUtils.assertAtStartTag(parser);
@ -1067,40 +1015,6 @@ public class PacketParserUtils {
collection.add(packetExtension);
}
/**
* Decodes a String into an object of the specified type. If the object
* type is not supported, null will be returned.
*
* @param type the type of the property.
* @param value the encode String value to decode.
* @return the String value decoded into the specified type.
* @throws ClassNotFoundException
*/
private static Object decode(Class<?> type, String value) throws ClassNotFoundException {
if (type.getName().equals("java.lang.String")) {
return value;
}
if (type.getName().equals("boolean")) {
return Boolean.valueOf(value);
}
if (type.getName().equals("int")) {
return Integer.valueOf(value);
}
if (type.getName().equals("long")) {
return Long.valueOf(value);
}
if (type.getName().equals("float")) {
return Float.valueOf(value);
}
if (type.getName().equals("double")) {
return Double.valueOf(value);
}
if (type.getName().equals("java.lang.Class")) {
return Class.forName(value);
}
return null;
}
/**
* This class represents and unparsed IQ of the type 'result'. Usually it's created when no IQProvider
* was found for the IQ element.

View File

@ -0,0 +1,28 @@
/**
*
* Copyright © 2014 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.time.provider;
import org.jivesoftware.smack.provider.IntrospectionProvider.IQIntrospectionProvider;
import org.jivesoftware.smackx.time.packet.Time;
public class TimeProvider extends IQIntrospectionProvider<Time> {
public TimeProvider() {
super(Time.class);
}
}

View File

@ -13,7 +13,7 @@
<iqProvider>
<elementName>time</elementName>
<namespace>urn:xmpp:time</namespace>
<className>org.jivesoftware.smackx.time.packet.Time</className>
<className>org.jivesoftware.smackx.time.provider.TimeProvider</className>
</iqProvider>
<!-- Chat State -->

View File

@ -17,14 +17,11 @@
package org.jivesoftware.smackx.time.packet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smackx.InitExtensions;
import org.junit.Test;
@ -58,37 +55,4 @@ public class TimeTest extends InitExtensions {
assertEquals("+8:30", time.getTzo());
}
@Test
public void parseTimeWithIntrospectionTest() throws Exception {
// @formatter:off
final String request =
"<iq type='get'"
+ "from='romeo@montague.net/orchard'"
+ "to='juliet@capulet.com/balcony'"
+ "id='time_1'>"
+ "<time xmlns='urn:xmpp:time'/>"
+ "</iq>";
// @formatter:on
IQ iqRequest = (IQ) PacketParserUtils.parseStanza(request);
assertTrue(iqRequest instanceof Time);
// @formatter:off
final String response =
"<iq type='result'"
+ "from='juliet@capulet.com/balcony'"
+ "to='romeo@montague.net/orchard'"
+ "id='time_1'>"
+ "<time xmlns='urn:xmpp:time'>"
+ "<tzo>-06:00</tzo>"
+ "<utc>2006-12-19T17:58:35Z</utc>"
+ "</time>"
+ "</iq>";
// @formatter:on
IQ iqResponse = (IQ) PacketParserUtils.parseStanza(response);
assertTrue(iqResponse instanceof Time);
Time time = (Time) iqResponse;
assertEquals("-06:00", time.getTzo());
assertEquals("2006-12-19T17:58:35Z", time.getUtc());
}
}

View File

@ -0,0 +1,61 @@
/**
*
* Copyright 2014 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.time.provider;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smackx.time.packet.Time;
import org.junit.Test;
public class TimeProviderTest {
@Test
public void parseTimeWithIntrospectionTest() throws Exception {
// @formatter:off
final String request =
"<iq type='get'"
+ "from='romeo@montague.net/orchard'"
+ "to='juliet@capulet.com/balcony'"
+ "id='time_1'>"
+ "<time xmlns='urn:xmpp:time'/>"
+ "</iq>";
// @formatter:on
IQ iqRequest = (IQ) PacketParserUtils.parseStanza(request);
assertTrue(iqRequest instanceof Time);
// @formatter:off
final String response =
"<iq type='result'"
+ "from='juliet@capulet.com/balcony'"
+ "to='romeo@montague.net/orchard'"
+ "id='time_1'>"
+ "<time xmlns='urn:xmpp:time'>"
+ "<tzo>-06:00</tzo>"
+ "<utc>2006-12-19T17:58:35Z</utc>"
+ "</time>"
+ "</iq>";
// @formatter:on
IQ iqResponse = (IQ) PacketParserUtils.parseStanza(response);
assertTrue(iqResponse instanceof Time);
Time time = (Time) iqResponse;
assertEquals("-06:00", time.getTzo());
assertEquals("2006-12-19T17:58:35Z", time.getUtc());
}
}