Improve Privacy List code

notably add a cache for the active and default privacy list to avoid
IQ get/response round-trips.

Also add a few methods to PrivacyListManager to get the privacy list
names. The already existing methods always returned the whole list
together with the name, which caused two round-trips.

Simplified some code.

Properly escape Privacy XML.
This commit is contained in:
Florian Schmaus 2015-01-07 19:35:11 +01:00
parent 67c0a7089b
commit 3dd1365a5a
8 changed files with 299 additions and 18 deletions

View File

@ -1371,6 +1371,27 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
sendStanzaWithResponseCallback(iqRequest, replyFilter, callback, exceptionCallback, timeout);
}
@Override
public void addOneTimeSyncCallback(final PacketListener callback, final PacketFilter packetFilter) {
final PacketListener packetListener = new PacketListener() {
@Override
public void processPacket(Packet packet) throws NotConnectedException {
try {
callback.processPacket(packet);
} finally {
removeSyncPacketListener(this);
}
}
};
addSyncPacketListener(packetListener, packetFilter);
removeCallbacksService.schedule(new Runnable() {
@Override
public void run() {
removeSyncPacketListener(packetListener);
}
}, getPacketReplyTimeout(), TimeUnit.MILLISECONDS);
}
private long lastStanzaReceived;
public long getLastStanzaReceived() {

View File

@ -556,6 +556,15 @@ public interface XMPPConnection {
final ExceptionCallback exceptionCallback, long timeout)
throws NotConnectedException;
/**
* Add a callback that is called exactly once and synchronously with the incoming stanza that matches the given
* packet filter.
*
* @param callback the callback invoked once the packet filter matches a stanza.
* @param packetFilter the filter to match stanzas or null to match all.
*/
public void addOneTimeSyncCallback(PacketListener callback, PacketFilter packetFilter);
/**
* Returns the timestamp in milliseconds when the last stanza was received.
*

View File

@ -0,0 +1,41 @@
/**
*
* Copyright 2015 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.filter;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
/**
*
*/
public class IQResultReplyFilter extends IQReplyFilter {
public IQResultReplyFilter(IQ iqPacket, XMPPConnection conn) {
super(iqPacket, conn);
}
@Override
public boolean accept(Packet packet) {
if (!super.accept(packet)) {
return false;
}
return IQTypeFilter.RESULT.accept(packet);
}
}

View File

@ -70,4 +70,8 @@ public class PrivacyList {
return items;
}
@Override
public String toString() {
return "Privacy List: " + listName + "(active:" + isActiveList + ", default:" + isDefaultList + ")";
}
}

View File

@ -23,6 +23,7 @@ import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.jivesoftware.smack.AbstractConnectionListener;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
@ -32,25 +33,29 @@ import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.IQResultReplyFilter;
import org.jivesoftware.smack.filter.IQTypeFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.privacy.filter.SetActiveListFilter;
import org.jivesoftware.smackx.privacy.filter.SetDefaultListFilter;
import org.jivesoftware.smackx.privacy.packet.Privacy;
import org.jivesoftware.smackx.privacy.packet.PrivacyItem;
/**
* A PrivacyListManager is used by XMPP clients to block or allow communications from other
* users. Use the manager to: <ul>
* users. Use the manager to:
* <ul>
* <li>Retrieve privacy lists.
* <li>Add, remove, and edit privacy lists.
* <li>Set, change, or decline active lists.
* <li>Set, change, or decline the default list (i.e., the list that is active by default).
* </ul>
* Privacy Items can handle different kind of permission communications based on JID, group,
* subscription type or globally (@see PrivacyItem).
* subscription type or globally (see {@link PrivacyItem}).
*
* @author Francisco Vives
* @see <a href="http://xmpp.org/extensions/xep-0016.html">XEP-16: Privacy Lists</a>
@ -58,8 +63,11 @@ import org.jivesoftware.smackx.privacy.packet.PrivacyItem;
public class PrivacyListManager extends Manager {
public static final String NAMESPACE = Privacy.NAMESPACE;
private static final PacketFilter PACKET_FILTER = new AndFilter(IQTypeFilter.SET,
new PacketTypeFilter(Privacy.class));
public static final PacketFilter PRIVACY_FILTER = new PacketTypeFilter(Privacy.class);
private static final PacketFilter PRIVACY_SET = new AndFilter(IQTypeFilter.SET, PRIVACY_FILTER);
private static final PacketFilter PRIVACY_RESULT = new AndFilter(IQTypeFilter.RESULT, PRIVACY_FILTER);
// Keep the list of instances of this class.
private static final Map<XMPPConnection, PrivacyListManager> INSTANCES = new WeakHashMap<XMPPConnection, PrivacyListManager>();
@ -75,6 +83,10 @@ public class PrivacyListManager extends Manager {
});
}
// TODO implement: private final Map<String, PrivacyList> cachedPrivacyLists = new HashMap<>();
private volatile String cachedActiveListName;
private volatile String cachedDefaultListName;
/**
* Creates a new privacy manager to maintain the communication privacy. Note: no
* information is sent to or received from the server until you attempt to
@ -82,7 +94,7 @@ public class PrivacyListManager extends Manager {
*
* @param connection the XMPP connection.
*/
private PrivacyListManager(final XMPPConnection connection) {
private PrivacyListManager(XMPPConnection connection) {
super(connection);
connection.addSyncPacketListener(new PacketListener() {
@ -109,7 +121,78 @@ public class PrivacyListManager extends Manager {
IQ iq = IQ.createResultIQ(privacy);
connection().sendPacket(iq);
}
}, PACKET_FILTER);
}, PRIVACY_SET);
// cached(Active|Default)ListName handling
connection.addPacketSendingListener(new PacketListener() {
@Override
public void processPacket(Packet packet) throws NotConnectedException {
XMPPConnection connection = connection();
Privacy privacy = (Privacy) packet;
PacketFilter iqResultReplyFilter = new IQResultReplyFilter(privacy, connection);
final String activeListName = privacy.getActiveName();
final boolean declinceActiveList = privacy.isDeclineActiveList();
connection.addOneTimeSyncCallback(new PacketListener() {
@Override
public void processPacket(Packet packet) throws NotConnectedException {
if (declinceActiveList) {
cachedActiveListName = null;
}
else {
cachedActiveListName = activeListName;
}
return;
}
}, iqResultReplyFilter);
}
}, SetActiveListFilter.INSTANCE);
connection.addPacketSendingListener(new PacketListener() {
@Override
public void processPacket(Packet packet) throws NotConnectedException {
XMPPConnection connection = connection();
Privacy privacy = (Privacy) packet;
PacketFilter iqResultReplyFilter = new IQResultReplyFilter(privacy, connection);
final String defaultListName = privacy.getDefaultName();
final boolean declinceDefaultList = privacy.isDeclineDefaultList();
connection.addOneTimeSyncCallback(new PacketListener() {
@Override
public void processPacket(Packet packet) throws NotConnectedException {
if (declinceDefaultList) {
cachedDefaultListName = null;
}
else {
cachedDefaultListName = defaultListName;
}
return;
}
}, iqResultReplyFilter);
}
}, SetDefaultListFilter.INSTANCE);
connection.addSyncPacketListener(new PacketListener() {
@Override
public void processPacket(Packet packet) throws NotConnectedException {
Privacy privacy = (Privacy) packet;
// If a privacy IQ result stanza has an active or default list name set, then we use that
// as cached list name.
String activeList = privacy.getActiveName();
if (activeList != null) {
cachedActiveListName = activeList;
}
String defaultList = privacy.getDefaultName();
if (defaultList != null) {
cachedDefaultListName = defaultList;
}
}
}, PRIVACY_RESULT);
connection.addConnectionListener(new AbstractConnectionListener() {
@Override
public void reconnectionSuccessful() {
cachedActiveListName = cachedDefaultListName = null;
}
});
// XEP-0016 § 3.
ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE);
}
/**
@ -191,13 +274,26 @@ public class PrivacyListManager extends Manager {
public PrivacyList getActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException {
Privacy privacyAnswer = this.getPrivacyWithListNames();
String listName = privacyAnswer.getActiveName();
boolean isDefaultAndActive = privacyAnswer.getActiveName() != null
&& privacyAnswer.getDefaultName() != null
&& privacyAnswer.getActiveName().equals(
privacyAnswer.getDefaultName());
boolean isDefaultAndActive = listName != null && listName.equals(privacyAnswer.getDefaultName());
return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName));
}
/**
* Get the name of the active list.
*
* @return the name of the active list or null if there is none set.
* @throws NoResponseException
* @throws XMPPErrorException
* @throws NotConnectedException
* @since 4.1
*/
public String getActiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException {
if (cachedActiveListName != null) {
return cachedActiveListName;
}
return getPrivacyWithListNames().getActiveName();
}
/**
* Answer the default privacy list.
*
@ -209,13 +305,47 @@ public class PrivacyListManager extends Manager {
public PrivacyList getDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException {
Privacy privacyAnswer = this.getPrivacyWithListNames();
String listName = privacyAnswer.getDefaultName();
boolean isDefaultAndActive = privacyAnswer.getActiveName() != null
&& privacyAnswer.getDefaultName() != null
&& privacyAnswer.getActiveName().equals(
privacyAnswer.getDefaultName());
boolean isDefaultAndActive = listName != null && listName.equals(privacyAnswer.getActiveName());
return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName));
}
/**
* Get the name of the default list.
*
* @return the name of the default list or null if there is none set.
* @throws NoResponseException
* @throws XMPPErrorException
* @throws NotConnectedException
* @since 4.1
*/
public String getDefaultListName() throws NoResponseException, XMPPErrorException, NotConnectedException {
if (cachedDefaultListName != null) {
return cachedDefaultListName;
}
return getPrivacyWithListNames().getDefaultName();
}
/**
* Returns the name of the effective privacy list.
* <p>
* The effective privacy list is the one that is currently enforced on the connection. It's either the active
* privacy list, or, if the active privacy list is not set, the default privacy list.
* </p>
*
* @return the name of the effective privacy list or null if there is none set.
* @throws NoResponseException
* @throws XMPPErrorException
* @throws NotConnectedException
* @since 4.1
*/
public String getEffectiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException {
String activeListName = getActiveListName();
if (activeListName != null) {
return activeListName;
}
return getDefaultListName();
}
/**
* Answer the privacy list items under listName with the allowed and blocked permissions.
*

View File

@ -0,0 +1,38 @@
/**
*
* Copyright 2015 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.privacy.filter;
import org.jivesoftware.smack.filter.FlexiblePacketTypeFilter;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smackx.privacy.packet.Privacy;
public class SetActiveListFilter extends FlexiblePacketTypeFilter<Privacy> {
public static final SetActiveListFilter INSTANCE = new SetActiveListFilter();
private SetActiveListFilter() {
}
@Override
protected boolean acceptSpecific(Privacy privacy) {
if (privacy.getType() != Type.set) {
return false;
}
return privacy.getActiveName() != null || privacy.isDeclineActiveList();
}
}

View File

@ -0,0 +1,38 @@
/**
*
* Copyright 2015 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.privacy.filter;
import org.jivesoftware.smack.filter.FlexiblePacketTypeFilter;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smackx.privacy.packet.Privacy;
public class SetDefaultListFilter extends FlexiblePacketTypeFilter<Privacy> {
public static final SetDefaultListFilter INSTANCE = new SetDefaultListFilter();
private SetDefaultListFilter() {
}
@Override
protected boolean acceptSpecific(Privacy privacy) {
if (privacy.getType() != Type.set) {
return false;
}
return privacy.getDefaultName() != null || privacy.isDeclineDefaultList();
}
}

View File

@ -292,7 +292,7 @@ public class Privacy extends IQ {
buf.append("<active/>");
} else {
if (this.getActiveName() != null) {
buf.append("<active name=\"").append(this.getActiveName()).append("\"/>");
buf.append("<active name=\"").escape(getActiveName()).append("\"/>");
}
}
// Add the default tag
@ -300,7 +300,7 @@ public class Privacy extends IQ {
buf.append("<default/>");
} else {
if (this.getDefaultName() != null) {
buf.append("<default name=\"").append(this.getDefaultName()).append("\"/>");
buf.append("<default name=\"").escape(getDefaultName()).append("\"/>");
}
}
@ -310,9 +310,9 @@ public class Privacy extends IQ {
List<PrivacyItem> items = entry.getValue();
// Begin the list tag
if (items.isEmpty()) {
buf.append("<list name=\"").append(listName).append("\"/>");
buf.append("<list name=\"").escape(listName).append("\"/>");
} else {
buf.append("<list name=\"").append(listName).append("\">");
buf.append("<list name=\"").escape(listName).append("\">");
}
for (PrivacyItem item : items) {
// Append the item xml representation