mirror of
https://github.com/vanitasvitae/Smack.git
synced 2025-09-10 17:49:38 +02:00
SMACK-272 Add support for XEP-0060 (pubsub)
git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@11346 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
parent
92ba2d7f33
commit
f7a1c750ad
73 changed files with 7214 additions and 0 deletions
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.Affiliation;
|
||||
|
||||
/**
|
||||
* Parses the affiliation element out of the reply stanza from the server
|
||||
* as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">affiliation schema</a>.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class AffiliationProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
return new Affiliation(attributeMap.get("node"), Affiliation.Type.valueOf(attributeMap.get("affiliation")));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.Affiliation;
|
||||
import org.jivesoftware.smackx.pubsub.AffiliationsExtension;
|
||||
|
||||
/**
|
||||
* Parses the affiliations element out of the reply stanza from the server
|
||||
* as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">affiliation schema</a>.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/public class AffiliationsProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
return new AffiliationsExtension((List<Affiliation>)content);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.packet.DataForm;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.ConfigurationEvent;
|
||||
import org.jivesoftware.smackx.pubsub.ConfigureForm;
|
||||
|
||||
/**
|
||||
* Parses the node configuration element out of the message event stanza from
|
||||
* the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">configuration schema</a>.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class ConfigEventProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
if (content.size() == 0)
|
||||
return new ConfigurationEvent(attMap.get("node"));
|
||||
else
|
||||
return new ConfigurationEvent(attMap.get("node"), new ConfigureForm((DataForm)content.iterator().next()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.EventElement;
|
||||
import org.jivesoftware.smackx.pubsub.EventElementType;
|
||||
import org.jivesoftware.smackx.pubsub.NodeExtension;
|
||||
|
||||
/**
|
||||
* Parses the event element out of the message stanza from
|
||||
* the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">event schema</a>.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class EventProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
return new EventElement(EventElementType.valueOf(content.get(0).getElementName()), (NodeExtension)content.get(0));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smack.provider.PacketExtensionProvider;
|
||||
import org.jivesoftware.smack.provider.ProviderManager;
|
||||
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||
import org.jivesoftware.smackx.pubsub.Item;
|
||||
import org.jivesoftware.smackx.pubsub.PayloadItem;
|
||||
import org.jivesoftware.smackx.pubsub.SimplePayload;
|
||||
import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Parses an <b>item</b> element as is defined in both the {@link PubSubNamespace#BASIC} and {@link PubSubNamespace#EVENT}
|
||||
* namespaces. To parse the item contents, it will use whatever {@link PacketExtensionProvider} is registered in
|
||||
* <b>smack.providers</b> for its element name and namespace. If no provider is registered, it will return a {@link SimplePayload}.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class ItemProvider implements PacketExtensionProvider
|
||||
{
|
||||
public PacketExtension parseExtension(XmlPullParser parser) throws Exception
|
||||
{
|
||||
String id = parser.getAttributeValue(null, "id");
|
||||
String elem = parser.getName();
|
||||
|
||||
int tag = parser.next();
|
||||
|
||||
if (tag == XmlPullParser.END_TAG)
|
||||
{
|
||||
return new Item(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
String payloadElemName = parser.getName();
|
||||
String payloadNS = parser.getNamespace();
|
||||
|
||||
if (ProviderManager.getInstance().getExtensionProvider(payloadElemName, payloadNS) == null)
|
||||
{
|
||||
boolean done = false;
|
||||
String payloadText = null;
|
||||
|
||||
while (!done)
|
||||
{
|
||||
if (tag == XmlPullParser.END_TAG && parser.getName().equals(elem))
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
else if (!((tag == XmlPullParser.START_TAG) && parser.isEmptyElementTag()))
|
||||
{
|
||||
if (payloadText == null)
|
||||
payloadText = parser.getText();
|
||||
else
|
||||
payloadText += parser.getText();
|
||||
}
|
||||
tag = parser.next();
|
||||
}
|
||||
return new PayloadItem<SimplePayload>(id, new SimplePayload(payloadElemName, payloadNS, payloadText));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PayloadItem<PacketExtension>(id, PacketParserUtils.parsePacketExtension(payloadElemName, payloadNS, parser));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.ItemsExtension;
|
||||
|
||||
/**
|
||||
* Parses the <b>items</b> element out of the message event stanza from
|
||||
* the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">items schema</a>.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class ItemsProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
return new ItemsExtension(ItemsExtension.ItemsElementType.items, attributeMap.get("node"), content, null);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smack.provider.IQProvider;
|
||||
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||
import org.jivesoftware.smackx.pubsub.packet.PubSub;
|
||||
import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Parses the root pubsub packet extensions of the {@link IQ} packet and returns
|
||||
* a {@link PubSub} instance.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class PubSubProvider implements IQProvider
|
||||
{
|
||||
public IQ parseIQ(XmlPullParser parser) throws Exception
|
||||
{
|
||||
PubSub pubsub = new PubSub();
|
||||
String namespace = parser.getNamespace();
|
||||
pubsub.setPubSubNamespace(PubSubNamespace.valueOfFromXmlns(namespace));
|
||||
boolean done = false;
|
||||
|
||||
while (!done)
|
||||
{
|
||||
int eventType = parser.next();
|
||||
|
||||
if (eventType == XmlPullParser.START_TAG)
|
||||
{
|
||||
PacketExtension ext = PacketParserUtils.parsePacketExtension(parser.getName(), namespace, parser);
|
||||
|
||||
if (ext != null)
|
||||
{
|
||||
pubsub.addExtension(ext);
|
||||
}
|
||||
}
|
||||
else if (eventType == XmlPullParser.END_TAG)
|
||||
{
|
||||
if (parser.getName().equals("pubsub"))
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return pubsub;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.RetractItem;
|
||||
|
||||
/**
|
||||
* Parses the <b>retract</b> element out of the message event stanza from
|
||||
* the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">retract schema</a>.
|
||||
* This element is a child of the <b>items</b> element.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class RetractEventProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
return new RetractItem(attributeMap.get("id"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.NodeExtension;
|
||||
import org.jivesoftware.smackx.pubsub.PubSubElementType;
|
||||
|
||||
/**
|
||||
* Parses simple elements that only contain a <b>node</b> attribute. This is common amongst many of the
|
||||
* elements defined in the pubsub specification. For this common case a {@link NodeExtension} is returned.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class SimpleNodeProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
return new NodeExtension(PubSubElementType.valueOfFromElemName(currentElement, currentNamespace), attributeMap.get("node"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smack.provider.PacketExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.Subscription;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Parses the <b>subscription</b> element out of the pubsub IQ message from
|
||||
* the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">subscription schema</a>.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class SubscriptionProvider implements PacketExtensionProvider
|
||||
{
|
||||
public PacketExtension parseExtension(XmlPullParser parser) throws Exception
|
||||
{
|
||||
String jid = parser.getAttributeValue(null, "jid");
|
||||
String nodeId = parser.getAttributeValue(null, "node");
|
||||
String subId = parser.getAttributeValue(null, "subid");
|
||||
String state = parser.getAttributeValue(null, "subscription");
|
||||
boolean isRequired = false;
|
||||
|
||||
int tag = parser.next();
|
||||
|
||||
if ((tag == XmlPullParser.START_TAG) && parser.getName().equals("subscribe-options"))
|
||||
{
|
||||
tag = parser.next();
|
||||
|
||||
if ((tag == XmlPullParser.START_TAG) && parser.getName().equals("required"))
|
||||
isRequired = true;
|
||||
|
||||
while (parser.next() != XmlPullParser.END_TAG && parser.getName() != "subscribe-options");
|
||||
}
|
||||
while (parser.getEventType() != XmlPullParser.END_TAG) parser.next();
|
||||
return new Subscription(jid, nodeId, subId, (state == null ? null : Subscription.State.valueOf(state)), isRequired);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* All rights reserved. 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.pubsub.provider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
|
||||
import org.jivesoftware.smackx.pubsub.Subscription;
|
||||
import org.jivesoftware.smackx.pubsub.SubscriptionsExtension;
|
||||
|
||||
/**
|
||||
* Parses the <b>subscriptions</b> element out of the pubsub IQ message from
|
||||
* the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">subscriptions schema</a>.
|
||||
*
|
||||
* @author Robin Collier
|
||||
*/
|
||||
public class SubscriptionsProvider extends EmbeddedExtensionProvider
|
||||
{
|
||||
@Override
|
||||
protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
|
||||
{
|
||||
return new SubscriptionsExtension(attributeMap.get("node"), (List<Subscription>)content);
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue