1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-11-26 14:02:06 +01:00

1 - Small fix in Roster to return unavailable presence with from.

2 - Throw exception if parsing of vCard fails.

git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@7217 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
Derek DeMoro 2007-02-20 17:02:39 +00:00 committed by derek
parent 0bd96e5156
commit 1f8cb4452b
3 changed files with 115 additions and 97 deletions

View file

@ -36,16 +36,16 @@ import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* Represents a user's roster, which is the collection of users a person receives * Represents a user's roster, which is the collection of users a person receives
* presence updates for. Roster items are categorized into groups for easier management.<p> * presence updates for. Roster items are categorized into groups for easier management.<p>
* * <p/>
* Others users may attempt to subscribe to this user using a subscription request. Three * Others users may attempt to subscribe to this user using a subscription request. Three
* modes are supported for handling these requests: <ul> * modes are supported for handling these requests: <ul>
* <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li> * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li>
* <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li> * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li>
* <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li> * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li>
* </ul> * </ul>
* *
* @see XMPPConnection#getRoster()
* @author Matt Tucker * @author Matt Tucker
* @see XMPPConnection#getRoster()
*/ */
public class Roster { public class Roster {
@ -99,7 +99,7 @@ public class Roster {
*/ */
Roster(final XMPPConnection connection) { Roster(final XMPPConnection connection) {
this.connection = connection; this.connection = connection;
groups = new ConcurrentHashMap<String,RosterGroup>(); groups = new ConcurrentHashMap<String, RosterGroup>();
unfiledEntries = new CopyOnWriteArrayList<RosterEntry>(); unfiledEntries = new CopyOnWriteArrayList<RosterEntry>();
entries = new CopyOnWriteArrayList<RosterEntry>(); entries = new CopyOnWriteArrayList<RosterEntry>();
rosterListeners = new CopyOnWriteArrayList<RosterListener>(); rosterListeners = new CopyOnWriteArrayList<RosterListener>();
@ -141,7 +141,7 @@ public class Roster {
* Returns the subscription processing mode, which dictates what action * Returns the subscription processing mode, which dictates what action
* Smack will take when subscription requests from other users are made. * Smack will take when subscription requests from other users are made.
* The default subscription mode is {@link SubscriptionMode#accept_all}.<p> * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
* * <p/>
* If using the manual mode, a PacketListener should be registered that * If using the manual mode, a PacketListener should be registered that
* listens for Presence packets that have a type of * listens for Presence packets that have a type of
* {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
@ -156,7 +156,7 @@ public class Roster {
* Sets the subscription processing mode, which dictates what action * Sets the subscription processing mode, which dictates what action
* Smack will take when subscription requests from other users are made. * Smack will take when subscription requests from other users are made.
* The default subscription mode is {@link SubscriptionMode#accept_all}.<p> * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
* * <p/>
* If using the manual mode, a PacketListener should be registered that * If using the manual mode, a PacketListener should be registered that
* listens for Presence packets that have a type of * listens for Presence packets that have a type of
* {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
@ -184,7 +184,7 @@ public class Roster {
*/ */
public void addRosterListener(RosterListener rosterListener) { public void addRosterListener(RosterListener rosterListener) {
if (!rosterListeners.contains(rosterListener)) { if (!rosterListeners.contains(rosterListener)) {
rosterListeners.add(rosterListener); rosterListeners.add(rosterListener);
} }
} }
@ -200,7 +200,7 @@ public class Roster {
/** /**
* Creates a new group.<p> * Creates a new group.<p>
* * <p/>
* Note: you must add at least one entry to the group for the group to be kept * Note: you must add at least one entry to the group for the group to be kept
* after a logout/login. This is due to the way that XMPP stores group information. * after a logout/login. This is due to the way that XMPP stores group information.
* *
@ -220,13 +220,13 @@ public class Roster {
* Creates a new roster entry and presence subscription. The server will asynchronously * Creates a new roster entry and presence subscription. The server will asynchronously
* update the roster with the subscription status. * update the roster with the subscription status.
* *
* @param user the user. (e.g. johndoe@jabber.org) * @param user the user. (e.g. johndoe@jabber.org)
* @param name the nickname of the user. * @param name the nickname of the user.
* @param groups the list of group names the entry will belong to, or <tt>null</tt> if the * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the
* the roster entry won't belong to a group. * the roster entry won't belong to a group.
* @throws XMPPException if an XMPP exception occurs. * @throws XMPPException if an XMPP exception occurs.
*/ */
public void createEntry(String user, String name, String [] groups) throws XMPPException { public void createEntry(String user, String name, String[] groups) throws XMPPException {
// Create and send roster entry creation packet. // Create and send roster entry creation packet.
RosterPacket rosterPacket = new RosterPacket(); RosterPacket rosterPacket = new RosterPacket();
rosterPacket.setType(IQ.Type.SET); rosterPacket.setType(IQ.Type.SET);
@ -243,7 +243,7 @@ public class Roster {
PacketCollector collector = connection.createPacketCollector( PacketCollector collector = connection.createPacketCollector(
new PacketIDFilter(rosterPacket.getPacketID())); new PacketIDFilter(rosterPacket.getPacketID()));
connection.sendPacket(rosterPacket); connection.sendPacket(rosterPacket);
IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
collector.cancel(); collector.cancel();
if (response == null) { if (response == null) {
throw new XMPPException("No response from the server."); throw new XMPPException("No response from the server.");
@ -281,9 +281,9 @@ public class Roster {
item.setItemType(RosterPacket.ItemType.remove); item.setItemType(RosterPacket.ItemType.remove);
packet.addRosterItem(item); packet.addRosterItem(item);
PacketCollector collector = connection.createPacketCollector( PacketCollector collector = connection.createPacketCollector(
new PacketIDFilter(packet.getPacketID())); new PacketIDFilter(packet.getPacketID()));
connection.sendPacket(packet); connection.sendPacket(packet);
IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
collector.cancel(); collector.cancel();
if (response == null) { if (response == null) {
throw new XMPPException("No response from the server."); throw new XMPPException("No response from the server.");
@ -349,7 +349,7 @@ public class Roster {
* <tt>null</tt> if the user is not an entry in the roster. * <tt>null</tt> if the user is not an entry in the roster.
* *
* @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be
* in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource").
* @return the roster entry or <tt>null</tt> if it does not exist. * @return the roster entry or <tt>null</tt> if it does not exist.
*/ */
public RosterEntry getEntry(String user) { public RosterEntry getEntry(String user) {
@ -369,8 +369,8 @@ public class Roster {
* Returns true if the specified XMPP address is an entry in the roster. * Returns true if the specified XMPP address is an entry in the roster.
* *
* @param user the XMPP address of the user (eg "jsmith@example.com"). The * @param user the XMPP address of the user (eg "jsmith@example.com"). The
* address could be in any valid format (e.g. "domain/resource", * address could be in any valid format (e.g. "domain/resource",
* "user@domain" or "user@domain/resource"). * "user@domain" or "user@domain/resource").
* @return true if the XMPP address is an entry in the roster. * @return true if the XMPP address is an entry in the roster.
*/ */
public boolean contains(String user) { public boolean contains(String user) {
@ -385,7 +385,7 @@ public class Roster {
* @return the roster group with the specified name. * @return the roster group with the specified name.
*/ */
public RosterGroup getGroup(String name) { public RosterGroup getGroup(String name) {
return groups.get(name); return groups.get(name);
} }
/** /**
@ -410,14 +410,14 @@ public class Roster {
* Returns the presence info for a particular user. If the user is offline, or * Returns the presence info for a particular user. If the user is offline, or
* if no presence data is available (such as when you are not subscribed to the * if no presence data is available (such as when you are not subscribed to the
* user's presence updates), unavailable presence will be returned.<p> * user's presence updates), unavailable presence will be returned.<p>
* * <p/>
* If the user has several presences (one for each resource), then the presence with * If the user has several presences (one for each resource), then the presence with
* highest priority will be returned. If multiple presences have the same priority, * highest priority will be returned. If multiple presences have the same priority,
* the one with the "most available" presence mode will be returned. In order, * the one with the "most available" presence mode will be returned. In order,
* that's {@link Presence.Mode#chat free to chat}, {@link Presence.Mode#available available}, * that's {@link Presence.Mode#chat free to chat}, {@link Presence.Mode#available available},
* {@link Presence.Mode#away away}, {@link Presence.Mode#xa extended away}, and * {@link Presence.Mode#away away}, {@link Presence.Mode#xa extended away}, and
* {@link Presence.Mode#dnd do not disturb}.<p> * {@link Presence.Mode#dnd do not disturb}.<p>
* * <p/>
* Note that presence information is received asynchronously. So, just after logging * Note that presence information is received asynchronously. So, just after logging
* in to the server, presence values for users in the roster may be unavailable * in to the server, presence values for users in the roster may be unavailable
* even if they are actually online. In other words, the value returned by this * even if they are actually online. In other words, the value returned by this
@ -427,16 +427,18 @@ public class Roster {
* {@link RosterListener}. * {@link RosterListener}.
* *
* @param user an XMPP ID. The address could be in any valid format (e.g. * @param user an XMPP ID. The address could be in any valid format (e.g.
* "domain/resource", "user@domain" or "user@domain/resource"). Any resource * "domain/resource", "user@domain" or "user@domain/resource"). Any resource
* information that's part of the ID will be discarded. * information that's part of the ID will be discarded.
* @return the user's current presence, or unavailable presence if the user is offline * @return the user's current presence, or unavailable presence if the user is offline
* or if no presence information is available.. * or if no presence information is available..
*/ */
public Presence getPresence(String user) { public Presence getPresence(String user) {
String key = getPresenceMapKey(StringUtils.parseBareAddress(user)); String key = getPresenceMapKey(StringUtils.parseBareAddress(user));
Map<String, Presence> userPresences = presenceMap.get(key); Map<String, Presence> userPresences = presenceMap.get(key);
if (userPresences == null) { if (userPresences == null) {
return new Presence(Presence.Type.unavailable); Presence presence = new Presence(Presence.Type.unavailable);
presence.setFrom(user);
return presence;
} }
else { else {
// Find the resource with the highest priority // Find the resource with the highest priority
@ -467,7 +469,9 @@ public class Roster {
} }
} }
if (presence == null) { if (presence == null) {
return new Presence(Presence.Type.unavailable); presence = new Presence(Presence.Type.unavailable);
presence.setFrom(user);
return presence;
} }
else { else {
return presence; return presence;
@ -482,19 +486,23 @@ public class Roster {
* *
* @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource). * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource).
* @return the user's current presence, or unavailable presence if the user is offline * @return the user's current presence, or unavailable presence if the user is offline
* or if no presence information is available. * or if no presence information is available.
*/ */
public Presence getPresenceResource(String userWithResource) { public Presence getPresenceResource(String userWithResource) {
String key = getPresenceMapKey(userWithResource); String key = getPresenceMapKey(userWithResource);
String resource = StringUtils.parseResource(userWithResource); String resource = StringUtils.parseResource(userWithResource);
Map<String, Presence> userPresences = presenceMap.get(key); Map<String, Presence> userPresences = presenceMap.get(key);
if (userPresences == null) { if (userPresences == null) {
return new Presence(Presence.Type.unavailable); Presence presence = new Presence(Presence.Type.unavailable);
presence.setFrom(userWithResource);
return presence;
} }
else { else {
Presence presence = userPresences.get(resource); Presence presence = userPresences.get(resource);
if (presence == null) { if (presence == null) {
return new Presence(Presence.Type.unavailable); presence = new Presence(Presence.Type.unavailable);
presence.setFrom(userWithResource);
return presence;
} }
else { else {
return presence; return presence;
@ -510,14 +518,16 @@ public class Roster {
* *
* @param user a XMPP ID, e.g. jdoe@example.com. * @param user a XMPP ID, e.g. jdoe@example.com.
* @return an iterator (of Presence objects) for all the user's current presences, * @return an iterator (of Presence objects) for all the user's current presences,
* or an unavailable presence if the user is offline or if no presence information * or an unavailable presence if the user is offline or if no presence information
* is available. * is available.
*/ */
public Iterator<Presence> getPresences(String user) { public Iterator<Presence> getPresences(String user) {
String key = getPresenceMapKey(user); String key = getPresenceMapKey(user);
Map<String, Presence> userPresences = presenceMap.get(key); Map<String, Presence> userPresences = presenceMap.get(key);
if (userPresences == null) { if (userPresences == null) {
return Arrays.asList(new Presence(Presence.Type.unavailable)).iterator(); Presence presence = new Presence(Presence.Type.unavailable);
presence.setFrom(user);
return Arrays.asList(presence).iterator();
} }
else { else {
return userPresences.values().iterator(); return userPresences.values().iterator();
@ -541,7 +551,7 @@ public class Roster {
* since it will always contain one entry for the user. * since it will always contain one entry for the user.
* *
* @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or
* jdoe@example.com/Work. * jdoe@example.com/Work.
* @return the key to use in the presenceMap for the fully qualified XMPP ID. * @return the key to use in the presenceMap for the fully qualified XMPP ID.
*/ */
private String getPresenceMapKey(String user) { private String getPresenceMapKey(String user) {
@ -579,13 +589,12 @@ public class Roster {
* specified collections of contacts have been added, updated or deleted * specified collections of contacts have been added, updated or deleted
* from the roster. * from the roster.
* *
* @param addedEntries the collection of address of the added contacts. * @param addedEntries the collection of address of the added contacts.
* @param updatedEntries the collection of address of the updated contacts. * @param updatedEntries the collection of address of the updated contacts.
* @param deletedEntries the collection of address of the deleted contacts. * @param deletedEntries the collection of address of the deleted contacts.
*/ */
private void fireRosterChangedEvent(Collection<String> addedEntries, Collection<String> updatedEntries, private void fireRosterChangedEvent(Collection<String> addedEntries, Collection<String> updatedEntries,
Collection<String> deletedEntries) Collection<String> deletedEntries) {
{
for (RosterListener listener : rosterListeners) { for (RosterListener listener : rosterListeners) {
if (!addedEntries.isEmpty()) { if (!addedEntries.isEmpty()) {
listener.entriesAdded(addedEntries); listener.entriesAdded(addedEntries);
@ -619,7 +628,7 @@ public class Roster {
* Automatically accept all subscription and unsubscription requests. This is * Automatically accept all subscription and unsubscription requests. This is
* the default mode and is suitable for simple client. More complex client will * the default mode and is suitable for simple client. More complex client will
* likely wish to handle subscription requests manually. * likely wish to handle subscription requests manually.
*/ */
accept_all, accept_all,
/** /**
@ -640,8 +649,9 @@ public class Roster {
* Listens for all presence packets and processes them. * Listens for all presence packets and processes them.
*/ */
private class PresencePacketListener implements PacketListener { private class PresencePacketListener implements PacketListener {
public void processPacket(Packet packet) { public void processPacket(Packet packet) {
Presence presence = (Presence)packet; Presence presence = (Presence) packet;
String from = presence.getFrom(); String from = presence.getFrom();
String key = getPresenceMapKey(from); String key = getPresenceMapKey(from);

View file

@ -39,62 +39,69 @@ import java.util.List;
* vCard provider. * vCard provider.
* *
* @author Gaston Dombiak * @author Gaston Dombiak
* @author Derek DeMoro
*/ */
public class VCardProvider implements IQProvider { public class VCardProvider implements IQProvider {
private final static String PREFERRED_ENCODING = "UTF-8"; private static final String PREFERRED_ENCODING = "UTF-8";
public IQ parseIQ(XmlPullParser parser) throws Exception { public IQ parseIQ(XmlPullParser parser) throws Exception {
StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
try { try {
int event = parser.getEventType(); int event = parser.getEventType();
// get the content // get the content
while (true) { while (true) {
switch (event) { switch (event) {
case XmlPullParser.TEXT: case XmlPullParser.TEXT:
// We must re-escape the xml so that the DOM won't throw an exception // We must re-escape the xml so that the DOM won't throw an exception
sb.append(StringUtils.escapeForXML(parser.getText())); sb.append(StringUtils.escapeForXML(parser.getText()));
break; break;
case XmlPullParser.START_TAG: case XmlPullParser.START_TAG:
sb.append('<').append(parser.getName()).append('>'); sb.append('<').append(parser.getName()).append('>');
break; break;
case XmlPullParser.END_TAG: case XmlPullParser.END_TAG:
sb.append("</").append(parser.getName()).append('>'); sb.append("</").append(parser.getName()).append('>');
break; break;
default: default:
} }
if (event == XmlPullParser.END_TAG && "vCard".equals(parser.getName())) break; if (event == XmlPullParser.END_TAG && "vCard".equals(parser.getName())) break;
event = parser.next(); event = parser.next();
} }
} catch (XmlPullParserException e) { }
e.printStackTrace(); catch (XmlPullParserException e) {
} catch (IOException e) { e.printStackTrace();
e.printStackTrace(); }
} catch (IOException e) {
e.printStackTrace();
}
String xmlText = sb.toString(); String xmlText = sb.toString();
return _createVCardFromXml(xmlText); return createVCardFromXML(xmlText);
} }
public static VCard _createVCardFromXml(String xmlText) { /**
VCard vCard = new VCard(); * Builds a users vCard from xml file.
try { *
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); * @param xml the xml representing a users vCard.
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); * @return the VCard.
Document document = documentBuilder.parse( * @throws
new ByteArrayInputStream(xmlText.getBytes(PREFERRED_ENCODING))); */
public static VCard createVCardFromXML(String xml) throws Exception {
VCard vCard = new VCard();
new VCardReader(vCard, document).initializeFields(); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(
new ByteArrayInputStream(xml.getBytes(PREFERRED_ENCODING)));
} catch (Exception e) { new VCardReader(vCard, document).initializeFields();
e.printStackTrace(System.err); return vCard;
} }
return vCard;
}
private static class VCardReader { private static class VCardReader {
private final VCard vCard; private final VCard vCard;
private final Document document; private final Document document;
@ -158,8 +165,8 @@ public class VCardProvider implements IQProvider {
} }
if (code == null || value == null) continue; if (code == null || value == null) continue;
if ("HOME".equals(type)) { if ("HOME".equals(type)) {
vCard.setPhoneHome(code, value); vCard.setPhoneHome(code, value);
} }
else { // By default, setup work phone else { // By default, setup work phone
vCard.setPhoneWork(code, value); vCard.setPhoneWork(code, value);
} }
@ -180,18 +187,18 @@ public class VCardProvider implements IQProvider {
List code = new ArrayList(); List code = new ArrayList();
List value = new ArrayList(); List value = new ArrayList();
NodeList childNodes = addressNode.getChildNodes(); NodeList childNodes = addressNode.getChildNodes();
for(int j = 0; j < childNodes.getLength(); j++) { for (int j = 0; j < childNodes.getLength(); j++) {
Node node = childNodes.item(j); Node node = childNodes.item(j);
if (node.getNodeType() != Node.ELEMENT_NODE) continue; if (node.getNodeType() != Node.ELEMENT_NODE) continue;
String nodeName = node.getNodeName(); String nodeName = node.getNodeName();
if (isWorkHome(nodeName)) { if (isWorkHome(nodeName)) {
type = nodeName; type = nodeName;
} }
else { else {
code.add(nodeName); code.add(nodeName);
value.add(getTextContent(node)); value.add(getTextContent(node));
} }
} }
for (int j = 0; j < value.size(); j++) { for (int j = 0; j < value.size(); j++) {
if ("HOME".equals(type)) { if ("HOME".equals(type)) {
vCard.setAddressFieldHome((String) code.get(j), (String) value.get(j)); vCard.setAddressFieldHome((String) code.get(j), (String) value.get(j));
@ -221,7 +228,8 @@ public class VCardProvider implements IQProvider {
String field = element.getNodeName(); String field = element.getNodeName();
if (element.getChildNodes().getLength() == 0) { if (element.getChildNodes().getLength() == 0) {
vCard.setField(field, ""); vCard.setField(field, "");
} else if (element.getChildNodes().getLength() == 1 && }
else if (element.getChildNodes().getLength() == 1 &&
element.getChildNodes().item(0) instanceof Text) { element.getChildNodes().item(0) instanceof Text) {
vCard.setField(field, getTextContent(element)); vCard.setField(field, getTextContent(element));
} }

View file

@ -62,23 +62,23 @@ public class VCardTest extends SmackTestCase {
} }
public void testNoWorkHomeSpecifier_EMAIL() throws Throwable { public void testNoWorkHomeSpecifier_EMAIL() throws Throwable {
VCard card = VCardProvider._createVCardFromXml("<vcard><EMAIL><USERID>foo@fee.www.bar</USERID></EMAIL></vcard>"); VCard card = VCardProvider.createVCardFromXML("<vcard><EMAIL><USERID>foo@fee.www.bar</USERID></EMAIL></vcard>");
assertEquals("foo@fee.www.bar", card.getEmailHome()); assertEquals("foo@fee.www.bar", card.getEmailHome());
} }
public void testNoWorkHomeSpecifier_TEL() throws Throwable { public void testNoWorkHomeSpecifier_TEL() throws Throwable {
VCard card = VCardProvider._createVCardFromXml("<vcard><TEL><FAX/><NUMBER>3443233</NUMBER></TEL></vcard>"); VCard card = VCardProvider.createVCardFromXML("<vcard><TEL><FAX/><NUMBER>3443233</NUMBER></TEL></vcard>");
assertEquals("3443233", card.getPhoneWork("FAX")); assertEquals("3443233", card.getPhoneWork("FAX"));
} }
public void testNoWorkHomeSpecifier_ADDR() throws Throwable { public void testNoWorkHomeSpecifier_ADDR() throws Throwable {
VCard card = VCardProvider._createVCardFromXml("<vcard><ADR><STREET>Some street</STREET><FF>ddss</FF></ADR></vcard>"); VCard card = VCardProvider.createVCardFromXML("<vcard><ADR><STREET>Some street</STREET><FF>ddss</FF></ADR></vcard>");
assertEquals("Some street", card.getAddressFieldWork("STREET")); assertEquals("Some street", card.getAddressFieldWork("STREET"));
assertEquals("ddss", card.getAddressFieldWork("FF")); assertEquals("ddss", card.getAddressFieldWork("FF"));
} }
public void testFN() throws Throwable { public void testFN() throws Throwable {
VCard card = VCardProvider._createVCardFromXml("<vcard><FN>kir max</FN></vcard>"); VCard card = VCardProvider.createVCardFromXML("<vcard><FN>kir max</FN></vcard>");
assertEquals("kir max", card.getField("FN")); assertEquals("kir max", card.getField("FN"));
// assertEquals("kir max", card.getFullName()); // assertEquals("kir max", card.getFullName());
} }