/** * Copyright 2012 Voxbone SA/NV * * 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 com.voxbone.kelpie; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.util.Properties; import javax.sip.address.SipURI; import javax.sip.header.FromHeader; import javax.sip.header.ToHeader; import javax.sip.message.Request; import org.apache.log4j.Logger; import org.apache.xerces.parsers.DOMParser; import org.apache.xml.serialize.OutputFormat; import org.apache.xml.serialize.XMLSerializer; import org.jabberstudio.jso.Packet; import org.jabberstudio.jso.StreamElement; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; /** * Generic representation of presence of an endpoint * can be generated from either a SIP NOTIFY or an XMPP status message * */ public class Presence { // XML documents types @SuppressWarnings("unused") private static final String PIDF_XML = "pidf+xml"; // pidf elements and attributes private static final String PRESENCE_ELEMENT = "presence"; @SuppressWarnings("unused") private static final String NS_ELEMENT = "xmlns"; private static final String NS_VALUE = "urn:ietf:params:xml:ns:pidf"; private static final String ENTITY_ATTRIBUTE = "entity"; private static final String TUPLE_ELEMENT = "tuple"; private static final String ID_ATTRIBUTE = "id"; private static final String STATUS_ELEMENT = "status"; @SuppressWarnings("unused") private static final String ONLINE_STATUS = "open"; @SuppressWarnings("unused") private static final String OFFLINE_STATUS = "closed"; private static final String BASIC_ELEMENT = "basic"; @SuppressWarnings("unused") private static final String CONTACT_ELEMENT = "contact"; private static final String NOTE_ELEMENT = "note"; @SuppressWarnings("unused") private static final String PRIORITY_ATTRIBUTE = "priority"; // rpid elements and attributes @SuppressWarnings("unused") private static final String RPID_NS_ELEMENT = "xmlns:rpid"; private static final String RPID_NS_VALUE = "urn:ietf:params:xml:ns:pidf:rpid"; @SuppressWarnings("unused") private static final String DM_NS_ELEMENT = "xmlns:dm"; private static final String DM_NS_VALUE = "urn:ietf:params:xml:ns:pidf:data-model"; private static final String PERSON_ELEMENT = "person"; @SuppressWarnings("unused") private static final String NS_PERSON_ELT = "dm:person"; private static final String ACTIVITY_ELEMENT = "activities"; @SuppressWarnings("unused") private static final String NS_ACTIVITY_ELT = "rpid:activities"; private static final String AWAY_ELEMENT = "away"; @SuppressWarnings("unused") private static final String NS_AWAY_ELT = "rpid:away"; private static final String BUSY_ELEMENT = "busy"; @SuppressWarnings("unused") private static final String NS_BUSY_ELT = "rpid:busy"; private static final String OTP_ELEMENT = "on-the-phone"; @SuppressWarnings("unused") private static final String NS_OTP_ELT = "rpid:on-the-phone"; // namespace wildcard private static final String ANY_NS = "*"; String resource; String from; String to; String type; // sip: "open" "closed", jabber: null, "unavailable" String show; String note; static Logger logger = Logger.getLogger(Presence.class); private static String statusNoteOnline; private static String statusNoteUnknown; public static void configure(Properties properties) { statusNoteOnline = properties.getProperty("com.voxbone.kelpie.status_note.online", "Kelpie Phone"); statusNoteUnknown = properties.getProperty("com.voxbone.kelpie.status_note.unknown", "Unknown"); } private Presence() { this.resource = null; this.from = null; this.to = null; this.type = null; // sip: "open" "closed", jabber: null, "unavailable" this.show = null; this.note = null; } public Presence(Request req) throws UnsupportedEncodingException, SAXException, IOException { ToHeader th = (ToHeader) req.getHeader("To"); FromHeader fh = (FromHeader) req.getHeader("From"); from = ((SipURI) fh.getAddress().getURI()).getUser(); to = ((SipURI) th.getAddress().getURI()).getUser(); byte [] body = req.getRawContent(); parsePidf(body); } public Presence(Packet p) { if (p.getTo() != null) { p.getTo().getNode(); } this.from = UriMappings.toSipId(p.getFrom()); this.resource = p.getFrom().getResource(); if (p.getAttributeValue("type") != null && p.getAttributeValue("type").equals("unavailable")) { this.type = "closed"; } else { this.type = "open"; } StreamElement se = p.getFirstElement("show"); if (se != null) { this.show = se.normalizeText(); } se = p.getFirstElement("status"); if (se != null) { this.note = se.normalizeText(); } } private void parsePidf(byte [] body) throws SAXException, IOException, UnsupportedEncodingException { DOMParser parser = new DOMParser(); if (body.length > 0) { try { parser.parse(new InputSource(new StringReader(new String(body, 0, body.length, "UTF8")))); } catch (SAXException e) { logger.error("parsing of presence pidf failed : ", e); logger.debug("SAXException -> body contents : " + new String(body, "UTF8")); throw e; } Document doc = parser.getDocument(); NodeList presList = doc.getElementsByTagNameNS(NS_VALUE, PRESENCE_ELEMENT); Node presNode = presList.item(0); Element presence = (Element) presNode; // RPID area // due to a lot of changes in the past years to this functionality, // the namespace used by servers and clients are often wrong so we just // ignore namespaces here NodeList personList = presence.getElementsByTagNameNS(ANY_NS, PERSON_ELEMENT); Node personNode = personList.item(0); Element person = (Element) personNode; if (person != null) { NodeList activityList = person.getElementsByTagNameNS(ANY_NS, ACTIVITY_ELEMENT); if (activityList.getLength() > 0) { Element activity = null; // find the first correct activity for (int i = 0; i < activityList.getLength(); i++) { Node activityNode = activityList.item(i); if (activityNode.getNodeType() != Node.ELEMENT_NODE) { continue; } activity = (Element) activityNode; NodeList statusList = activity.getChildNodes(); for (int j = 0; j < statusList.getLength(); j++) { Node statusNode = statusList.item(j); if (statusNode.getNodeType() == Node.ELEMENT_NODE) { String statusname = statusNode.getLocalName(); if (statusname.equals(AWAY_ELEMENT)) { show = "away"; break; } else if (statusname.equals(BUSY_ELEMENT)) { show = "dnd"; break; } else if (statusname.equals(OTP_ELEMENT)) { show = "dnd"; break; } } } if (show != null) { break; } } } } NodeList tupleList = presence.getElementsByTagNameNS(ANY_NS, TUPLE_ELEMENT); Node tupleNode = tupleList.item(0); Element tuple = (Element) tupleNode; this.resource = tuple.getAttribute("id"); NodeList statuses = tuple.getElementsByTagNameNS(ANY_NS, STATUS_ELEMENT); Element statusElement = (Element) statuses.item(0); NodeList basicList = statusElement.getElementsByTagNameNS(ANY_NS, BASIC_ELEMENT); Element basic = (Element) basicList.item(0); if (basic != null) { Node node = basic.getFirstChild(); if (node != null) { this.type = node.getNodeValue(); } } NodeList nodeList = tuple.getElementsByTagNameNS(ANY_NS, NOTE_ELEMENT); if (nodeList.getLength() > 0 && nodeList.item(0).getFirstChild() != null) { this.note = nodeList.item(0).getFirstChild().getNodeValue(); } } } byte [] buildPidf(String domain) { OutputFormat of = new OutputFormat("XML", "UTF-8", true); ByteArrayOutputStream os = new ByteArrayOutputStream(); of.setIndent(1); of.setIndenting(true); XMLSerializer serializer = new XMLSerializer(os, of); try { ContentHandler hd = serializer.asContentHandler(); hd.startPrefixMapping("", NS_VALUE); hd.startPrefixMapping("rpid", RPID_NS_VALUE); hd.startPrefixMapping("dm", DM_NS_VALUE); hd.startDocument(); AttributesImpl atts = new AttributesImpl(); atts.addAttribute(NS_VALUE, ENTITY_ATTRIBUTE, "", "", "pres:" + this.from + "@" + domain); hd.startElement(NS_VALUE, PRESENCE_ELEMENT, "", atts); atts.clear(); atts.addAttribute(NS_VALUE, ID_ATTRIBUTE, "", "", this.resource); hd.startElement(NS_VALUE, TUPLE_ELEMENT, "", atts); atts.clear(); hd.startElement(NS_VALUE, STATUS_ELEMENT, "", atts); hd.startElement(NS_VALUE, BASIC_ELEMENT, "", atts); hd.characters(this.type.toCharArray(), 0, this.type.toCharArray().length); hd.endElement(NS_VALUE, BASIC_ELEMENT, ""); hd.endElement(NS_VALUE, STATUS_ELEMENT, ""); if (this.note != null) { hd.startElement(NS_VALUE, NOTE_ELEMENT, "", atts); hd.characters(this.note.toCharArray(), 0, this.note.toCharArray().length); hd.endElement(NS_VALUE, NOTE_ELEMENT, ""); } hd.endElement(NS_VALUE, TUPLE_ELEMENT, ""); atts.addAttribute(DM_NS_VALUE, ID_ATTRIBUTE, "", "", this.resource); hd.startElement(DM_NS_VALUE, PERSON_ELEMENT, "", atts); atts.clear(); hd.startElement(RPID_NS_VALUE, ACTIVITY_ELEMENT, "", atts); if (this.show != null) { if (this.show.equals("away")) { hd.startElement(RPID_NS_VALUE, AWAY_ELEMENT, "", atts); hd.endElement(RPID_NS_VALUE, AWAY_ELEMENT, ""); } else if (this.show.equals("dnd") || this.show.equals("xa")) { hd.startElement(RPID_NS_VALUE, BUSY_ELEMENT, "", atts); hd.endElement(RPID_NS_VALUE, BUSY_ELEMENT, ""); } } hd.endElement(RPID_NS_VALUE, ACTIVITY_ELEMENT, ""); hd.endElement(DM_NS_VALUE, PERSON_ELEMENT, ""); hd.endElement(NS_VALUE, PRESENCE_ELEMENT, ""); hd.endDocument(); os.close(); return os.toByteArray(); } catch (IOException e) { logger.error("Error building presence document", e); } catch (SAXException e) { logger.error("Error building presence document", e); } return null; } public static Presence buildOnlinePresence(String user, String dest, String domain) { String pidf = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\"" + " xmlns:dm=\"urn:ietf:params:xml:ns:pidf:data-model\"" + " xmlns:rpid=\"urn:ietf:params:xml:ns:pidf:rpid\"" + " entity=\"pres:" + user + "@" + domain + "\">" + "<tuple id=\"KelpiePhone\">" + "<status>" + "<basic>open</basic>" + "</status>" + "<contact>sip:" + user + "@" + domain + "</contact>" + "<note>" + statusNoteOnline + "</note>" + "</tuple>" + "</presence>"; Presence p = new Presence(); p.from = user; p.to = dest; try { p.parsePidf(pidf.getBytes()); } catch (UnsupportedEncodingException e) { logger.error("Error building presence document", e); } catch (SAXException e) { logger.error("Error building presence document", e); } catch (IOException e) { logger.error("Error building presence document", e); } return p; } public static Presence buildOfflinePresence(String user, String dest) { Presence p = new Presence(); p.from = user; p.to = dest; p.type = "closed"; return p; } public static Presence buildUnknownPresence(String user, String dest, String domain) { String pidf = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\"" + " xmlns:dm=\"urn:ietf:params:xml:ns:pidf:data-model\"" + " xmlns:rpid=\"urn:ietf:params:xml:ns:pidf:rpid\"" + " entity=\"pres:" + user + "@" + domain + "\">" + "<tuple id=\"KelpiePhone\">" + "<status>" + "<basic>open</basic>" + "</status>" + "<contact>sip:" + user + "@" + domain + "</contact>" + "<note>" + statusNoteUnknown + "</note>" + "</tuple>" + "</presence>"; Presence p = new Presence(); p.from = user; p.to = dest; try { p.parsePidf(pidf.getBytes()); } catch (UnsupportedEncodingException e) { logger.error("Error building presence document", e); } catch (SAXException e) { logger.error("Error building presence document", e); } catch (IOException e) { logger.error("Error building presence document", e); } return p; } }