001    package ezvcard.types;
002    
003    import java.net.URI;
004    import java.net.URISyntaxException;
005    import java.util.ArrayList;
006    import java.util.List;
007    import java.util.regex.Matcher;
008    import java.util.regex.Pattern;
009    
010    import ezvcard.VCardSubTypes;
011    import ezvcard.VCardVersion;
012    import ezvcard.io.CompatibilityMode;
013    import ezvcard.io.SkipMeException;
014    import ezvcard.parameters.ImppTypeParameter;
015    import ezvcard.util.HCardElement;
016    import ezvcard.util.VCardStringUtils;
017    import ezvcard.util.XCardElement;
018    
019    /*
020     Copyright (c) 2012, Michael Angstadt
021     All rights reserved.
022    
023     Redistribution and use in source and binary forms, with or without
024     modification, are permitted provided that the following conditions are met: 
025    
026     1. Redistributions of source code must retain the above copyright notice, this
027     list of conditions and the following disclaimer. 
028     2. Redistributions in binary form must reproduce the above copyright notice,
029     this list of conditions and the following disclaimer in the documentation
030     and/or other materials provided with the distribution. 
031    
032     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
033     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
034     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
035     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
036     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
037     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
038     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
039     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
040     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
041     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042    
043     The views and conclusions contained in the software and documentation are those
044     of the authors and should not be interpreted as representing official policies, 
045     either expressed or implied, of the FreeBSD Project.
046     */
047    
048    /**
049     * An instant message handle. The handle is represented as a URI in the format "
050     * <code>&lt;IM-PROTOCOL&gt;:&lt;IM-HANDLE&gt;</code>". For example, someone
051     * with a Yahoo! Messenger handle of "johndoe@yahoo.com" would have an IMPP
052     * vCard property value of "ymsgr:johndoe@yahoo.com".
053     * 
054     * <pre>
055     * VCard vcard = new VCard();
056     * 
057     * //URI
058     * ImppType impp = new ImppType(&quot;aim:johndoe@aol.com&quot;);
059     * vcard.addImpp(impp);
060     * 
061     * //static helper constructors
062     * impp = ImppType.msn(&quot;janedoe@msn.com&quot;);
063     * vcard.addImpp(impp);
064     * </pre>
065     * 
066     * <p>
067     * vCard property name: IMPP
068     * </p>
069     * <p>
070     * vCard versions: 3.0, 4.0
071     * </p>
072     * @author Michael Angstadt
073     */
074    public class ImppType extends MultiValuedTypeParameterType<ImppTypeParameter> {
075            public static final String NAME = "IMPP";
076    
077            private static final String AIM = "aim";
078            private static final String ICQ = "icq";
079            private static final String IRC = "irc";
080            private static final String MSN = "msnim";
081            private static final String SIP = "sip";
082            private static final String SKYPE = "skype";
083            private static final String XMPP = "xmpp";
084            private static final String YAHOO = "ymsgr";
085    
086            /**
087             * List of recognized IM protocols that can be parsed from an HTML link
088             * (hCard).
089             */
090            private static final List<ImHtmlLink> htmlParseableProtocols = new ArrayList<ImHtmlLink>();
091            static {
092                    //http://en.wikipedia.org/wiki/AOL_Instant_Messenger#URI_scheme
093                    htmlParseableProtocols.add(new ImHtmlLink(AIM, "(goim|addbuddy)\\?.*?\\bscreenname=(.*?)(&|$)", 2, "goim?screenname=%s"));
094    
095                    //http://en.wikipedia.org/wiki/Yahoo!_Messenger#URI_scheme
096                    htmlParseableProtocols.add(new ImHtmlLink(YAHOO, "(sendim|addfriend|sendfile|call)\\?(.*)", 2, "sendim?%s"));
097    
098                    //http://developer.skype.com/skype-uri/skype-uri-ref-api
099                    htmlParseableProtocols.add(new ImHtmlLink(SKYPE, "(.*?)(\\?|$)", 1, "%s"));
100    
101                    //http://www.tech-recipes.com/rx/1157/msn-messenger-msnim-hyperlink-command-codes/
102                    htmlParseableProtocols.add(new ImHtmlLink(MSN, "(chat|add|voice|video)\\?contact=(.*?)(&|$)", 2, "chat?contact=%s"));
103    
104                    //http://www.tech-recipes.com/rx/1157/msn-messenger-msnim-hyperlink-command-codes/
105                    htmlParseableProtocols.add(new ImHtmlLink(XMPP, "(.*?)(\\?|$)", 1, "%s?message"));
106    
107                    //http://forums.miranda-im.org/showthread.php?26589-Add-support-to-quot-icq-message-uin-12345-quot-web-links
108                    htmlParseableProtocols.add(new ImHtmlLink(ICQ, "message\\?uin=(\\d+)", 1, "message?uin=%s"));
109    
110                    //SIP: http://en.wikipedia.org/wiki/Session_Initiation_Protocol
111                    //leave as-is
112                    htmlParseableProtocols.add(new ImHtmlLink(SIP));
113    
114                    //IRC: http://stackoverflow.com/questions/11970897/how-do-i-open-a-query-window-using-the-irc-uri-scheme
115                    //IRC handles are not globally unique, so leave as-is
116                    htmlParseableProtocols.add(new ImHtmlLink(IRC));
117            }
118    
119            private URI uri;
120    
121            public ImppType() {
122                    super(NAME);
123            }
124    
125            /**
126             * Constructs a new IMPP type. Note that this class has static convenience
127             * methods for creating IMPP types of common IM protocols.
128             * @param uri the IM URI (e.g. "aim:johndoe@aol.com")
129             * @throws IllegalArgumentException if the URI is not a valid URI
130             */
131            public ImppType(String uri) {
132                    this();
133                    setUri(uri);
134            }
135    
136            /**
137             * Constructs a new IMPP type. Note that this class has static convenience
138             * methods for creating IMPP types of common IM protocols.
139             * @param uri the IM URI (e.g. "aim:johndoe@aol.com")
140             */
141            public ImppType(URI uri) {
142                    this();
143                    setUri(uri);
144            }
145    
146            /**
147             * Constructs a new IMPP type. Note that this class has static convenience
148             * methods for creating IMPP types of common IM protocols.
149             * @param protocol the IM protocol (e.g. "aim")
150             * @param handle the IM handle (e.g. "johndoe@aol.com")
151             */
152            public ImppType(String protocol, String handle) {
153                    this();
154                    setUri(protocol, handle);
155            }
156    
157            /**
158             * Creates an IMPP property that contains a AOL Instant Messenger handle.
159             * @param handle the IM handle
160             * @return the IMPP property instance
161             */
162            public static ImppType aim(String handle) {
163                    return new ImppType(AIM, handle);
164            }
165    
166            /**
167             * Determines if this IMPP property contains an AOL Instant Messenger
168             * handle.
169             * @return true if it contains an AOL Instant Messenger handle, false if not
170             */
171            public boolean isAim() {
172                    return isProtocol(AIM);
173            }
174    
175            /**
176             * Creates an IMPP property that contains a Yahoo! Messenger handle.
177             * @param handle the IM handle
178             * @return the IMPP property instance
179             */
180            public static ImppType yahoo(String handle) {
181                    return new ImppType(YAHOO, handle);
182            }
183    
184            /**
185             * Determines if this IMPP property contains a Yahoo! Messenger handle.
186             * @return true if it contains a Yahoo! Messenger handle, false if not
187             */
188            public boolean isYahoo() {
189                    return isProtocol(YAHOO);
190            }
191    
192            /**
193             * Creates an IMPP property that contains an MSN IMPP property.
194             * @param handle the IM handle
195             * @return the IMPP property instance
196             */
197            public static ImppType msn(String handle) {
198                    return new ImppType(MSN, handle);
199            }
200    
201            /**
202             * Determines if this IMPP property contains an MSN handle.
203             * @return true if it contains an MSN handle, false if not
204             */
205            public boolean isMsn() {
206                    return isProtocol(MSN);
207            }
208    
209            /**
210             * Creates an IMPP property that contains an ICQ handle.
211             * @param handle the IM handle
212             * @return the IMPP property instance
213             */
214            public static ImppType icq(String handle) {
215                    return new ImppType(ICQ, handle);
216            }
217    
218            /**
219             * Determines if this IMPP property contains an ICQ handle.
220             * @return true if it contains an ICQ handle, false if not
221             */
222            public boolean isIcq() {
223                    return isProtocol(ICQ);
224            }
225    
226            /**
227             * Creates an IMPP property that contains an IRC handle.
228             * @param handle the IM handle
229             * @return the IMPP property instance
230             */
231            public static ImppType irc(String handle) {
232                    return new ImppType(IRC, handle);
233            }
234    
235            /**
236             * Determines if this IMPP property contains an IRC handle.
237             * @return true if it contains an IRC handle, false if not
238             */
239            public boolean isIrc() {
240                    return isProtocol(IRC);
241            }
242    
243            /**
244             * Creates an IMPP property that contains a Session Initiation Protocol
245             * handle.
246             * @param handle the IM handle
247             * @return the IMPP property instance
248             */
249            public static ImppType sip(String handle) {
250                    return new ImppType(SIP, handle);
251            }
252    
253            /**
254             * Determines if this IMPP property contains a Session Initiation Protocol
255             * handle.
256             * @return true if it contains a SIP handle, false if not
257             */
258            public boolean isSip() {
259                    return isProtocol(SIP);
260            }
261    
262            /**
263             * Creates an IMPP property that contains a Skype handle.
264             * @param handle the IM handle
265             * @return the IMPP property instance
266             */
267            public static ImppType skype(String handle) {
268                    return new ImppType(SKYPE, handle);
269            }
270    
271            /**
272             * Determines if this IMPP property contains a Skype handle.
273             * @return true if it contains a Skype handle, false if not
274             */
275            public boolean isSkype() {
276                    return isProtocol(SKYPE);
277            }
278    
279            /**
280             * Creates an IMPP property that contains an Extensible Messaging and
281             * Presence Protocol handle.
282             * @param handle the IM handle
283             * @return the IMPP property instance
284             */
285            public static ImppType xmpp(String handle) {
286                    return new ImppType(XMPP, handle);
287            }
288    
289            /**
290             * Determines if this IMPP property contains an Extensible Messaging and
291             * Presence Protocol handle.
292             * @return true if it contains an XMPP handle, false if not
293             */
294            public boolean isXmpp() {
295                    return isProtocol(XMPP);
296            }
297    
298            private boolean isProtocol(String protocol) {
299                    return uri != null && protocol.equals(uri.getScheme());
300            }
301    
302            /**
303             * Gets the IM URI.
304             * @return the IM URI
305             */
306            public URI getUri() {
307                    return uri;
308            }
309    
310            /**
311             * Sets the IM URI.
312             * @param uri the IM URI (e.g. "aim:theuser@aol.com")
313             * @throws IllegalArgumentException if the URI is not a valid URI
314             */
315            public void setUri(String uri) {
316                    setUri(URI.create(uri));
317            }
318    
319            /**
320             * Sets the IM URI.
321             * @param uri the IM URI (e.g. "aim:theuser@aol.com")
322             */
323            public void setUri(URI uri) {
324                    this.uri = uri;
325            }
326    
327            /**
328             * Sets the IM URI.
329             * @param protocol the IM protocol (e.g. "aim")
330             * @param handle the IM handle (e.g. "theuser@aol.com")
331             */
332            public void setUri(String protocol, String handle) {
333                    try {
334                            this.uri = new URI(protocol, handle, null);
335                    } catch (URISyntaxException e) {
336                            throw new IllegalArgumentException(e);
337                    }
338            }
339    
340            /**
341             * Gets the IM protocol. Use {@link #setUri(String, String)} to set the
342             * protocol.
343             * @return the IM protocol (e.g. "aim") or null if not set
344             */
345            public String getProtocol() {
346                    if (uri == null) {
347                            return null;
348                    }
349                    return uri.getScheme();
350            }
351    
352            /**
353             * Gets the IM handle. Use {@link #setUri(String, String)} to set the
354             * handle.
355             * @return the IM handle (e.g. "johndoe@aol.com") or null if not set
356             */
357            public String getHandle() {
358                    if (uri == null) {
359                            return null;
360                    }
361                    return uri.getSchemeSpecificPart();
362            }
363    
364            /**
365             * Gets the MEDIATYPE parameter.
366             * <p>
367             * vCard versions: 4.0
368             * </p>
369             * @return the media type or null if not set
370             */
371            public String getMediaType() {
372                    return subTypes.getMediaType();
373            }
374    
375            /**
376             * Sets the MEDIATYPE parameter.
377             * <p>
378             * vCard versions: 4.0
379             * </p>
380             * @param mediaType the media type or null to remove
381             */
382            public void setMediaType(String mediaType) {
383                    subTypes.setMediaType(mediaType);
384            }
385    
386            /**
387             * Gets all PID parameter values.
388             * <p>
389             * vCard versions: 4.0
390             * </p>
391             * @return the PID values or empty set if there are none
392             * @see VCardSubTypes#getPids
393             */
394            public List<Integer[]> getPids() {
395                    return subTypes.getPids();
396            }
397    
398            /**
399             * Adds a PID value.
400             * <p>
401             * vCard versions: 4.0
402             * </p>
403             * @param localId the local ID
404             * @param clientPidMapRef the ID used to reference the property's globally
405             * unique identifier in the CLIENTPIDMAP property.
406             * @see VCardSubTypes#addPid(int, int)
407             */
408            public void addPid(int localId, int clientPidMapRef) {
409                    subTypes.addPid(localId, clientPidMapRef);
410            }
411    
412            /**
413             * Removes all PID values.
414             * <p>
415             * vCard versions: 4.0
416             * </p>
417             * @see VCardSubTypes#removePids
418             */
419            public void removePids() {
420                    subTypes.removePids();
421            }
422    
423            /**
424             * Gets the preference value.
425             * <p>
426             * vCard versions: 4.0
427             * </p>
428             * @return the preference value or null if it doesn't exist
429             * @see VCardSubTypes#getPref
430             */
431            public Integer getPref() {
432                    return subTypes.getPref();
433            }
434    
435            /**
436             * Sets the preference value.
437             * <p>
438             * vCard versions: 4.0
439             * </p>
440             * @param pref the preference value or null to remove
441             * @see VCardSubTypes#setPref
442             */
443            public void setPref(Integer pref) {
444                    subTypes.setPref(pref);
445            }
446    
447            /**
448             * Gets the ALTID.
449             * <p>
450             * vCard versions: 4.0
451             * </p>
452             * @return the ALTID or null if it doesn't exist
453             * @see VCardSubTypes#getAltId
454             */
455            public String getAltId() {
456                    return subTypes.getAltId();
457            }
458    
459            /**
460             * Sets the ALTID.
461             * <p>
462             * vCard versions: 4.0
463             * </p>
464             * @param altId the ALTID or null to remove
465             * @see VCardSubTypes#setAltId
466             */
467            public void setAltId(String altId) {
468                    subTypes.setAltId(altId);
469            }
470    
471            @Override
472            protected ImppTypeParameter buildTypeObj(String type) {
473                    ImppTypeParameter param = ImppTypeParameter.valueOf(type);
474                    if (param == null) {
475                            param = new ImppTypeParameter(type);
476                    }
477                    return param;
478            }
479    
480            @Override
481            public VCardVersion[] getSupportedVersions() {
482                    return new VCardVersion[] { VCardVersion.V3_0, VCardVersion.V4_0 };
483            }
484    
485            @Override
486            protected void doMarshalText(StringBuilder sb, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode) {
487                    if (uri != null) {
488                            sb.append(VCardStringUtils.escape(uri.toString()));
489                    }
490            }
491    
492            @Override
493            protected void doUnmarshalText(String value, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode) {
494                    value = VCardStringUtils.unescape(value);
495                    try {
496                            setUri(value);
497                    } catch (IllegalArgumentException e) {
498                            throw new SkipMeException("Cannot parse URI \"" + value + "\": " + e.getMessage());
499                    }
500            }
501    
502            @Override
503            protected void doMarshalXml(XCardElement parent, List<String> warnings, CompatibilityMode compatibilityMode) {
504                    if (uri != null) {
505                            parent.uri(uri.toString());
506                    }
507            }
508    
509            @Override
510            protected void doUnmarshalXml(XCardElement element, List<String> warnings, CompatibilityMode compatibilityMode) {
511                    String value = element.uri();
512                    try {
513                            setUri(value);
514                    } catch (IllegalArgumentException e) {
515                            throw new SkipMeException("Cannot parse URI \"" + value + "\": " + e.getMessage());
516                    }
517            }
518    
519            @Override
520            protected void doUnmarshalHtml(HCardElement element, List<String> warnings) {
521                    String href = element.attr("href");
522                    if (href.length() == 0) {
523                            href = element.value();
524                    }
525    
526                    try {
527                            URI uri = parseUriFromLink(href);
528                            if (uri == null) {
529                                    throw new IllegalArgumentException();
530                            }
531                            setUri(uri);
532                    } catch (IllegalArgumentException e) {
533                            throw new SkipMeException("Could not parse instant messenger information out of link: " + href);
534                    }
535            }
536    
537            /**
538             * Parses an IM URI from an HTML link.
539             * @param linkUri the HTML link (e.g. "aim:goim?screenname=theuser")
540             * @return the IM URI or null if not recognized
541             */
542            protected static URI parseUriFromLink(String linkUri) {
543                    for (ImHtmlLink imLink : htmlParseableProtocols) {
544                            String handle = imLink.parseHandle(linkUri);
545                            if (handle != null) {
546                                    try {
547                                            return new URI(imLink.getProtocol(), handle, null);
548                                    } catch (URISyntaxException e) {
549                                            throw new IllegalArgumentException(e);
550                                    }
551                            }
552                    }
553                    return null;
554            }
555    
556            /**
557             * Builds a URI suitable for use as a link on a webpage.
558             * @return the link URI (e.g. "aim:goim?screenname=theuser") or null if the
559             * IMPP URI was never set
560             */
561            public String buildLink() {
562                    if (uri == null) {
563                            return null;
564                    }
565    
566                    String protocol = uri.getScheme();
567                    String handle = uri.getSchemeSpecificPart();
568    
569                    for (ImHtmlLink imLink : htmlParseableProtocols) {
570                            if (protocol.equals(imLink.getProtocol())) {
571                                    return imLink.buildLink(handle);
572                            }
573                    }
574                    return uri.toString();
575            }
576    
577            /**
578             * Helper class for parsing and building instant messenger links for
579             * webpages.
580             */
581            protected static class ImHtmlLink {
582                    private final Pattern linkRegex;
583                    private final String protocol;
584                    private final int handleGroup;
585                    private final String linkFormat;
586    
587                    /**
588                     * @param protocol the IM protocol (e.g. "aim")
589                     */
590                    public ImHtmlLink(String protocol) {
591                            this(protocol, "(.*)", 1, "%s");
592                    }
593    
594                    /**
595                     * @param protocol the IM protocol (e.g. "aim")
596                     * @param linkRegex the regular expression used to parse a link
597                     * @param handleGroup the group number from the regular expression that
598                     * contains the IM handle
599                     * @param linkFormat the format string for building a link
600                     */
601                    public ImHtmlLink(String protocol, String linkRegex, int handleGroup, String linkFormat) {
602                            this.linkRegex = Pattern.compile('^' + protocol + ':' + linkRegex, Pattern.CASE_INSENSITIVE);
603                            this.protocol = protocol;
604                            this.handleGroup = handleGroup;
605                            this.linkFormat = protocol + ':' + linkFormat;
606                    }
607    
608                    /**
609                     * Parses the IM handle out of a link.
610                     * @param linkUri the link
611                     * @return the IM handle or null if it can't be found
612                     */
613                    public String parseHandle(String linkUri) {
614                            Matcher m = linkRegex.matcher(linkUri);
615                            if (m.find()) {
616                                    return m.group(handleGroup);
617                            }
618                            return null;
619                    }
620    
621                    /**
622                     * Builds a link for inclusion in a webpage.
623                     * @param handle the IM handle
624                     * @return the link
625                     */
626                    public String buildLink(String handle) {
627                            return String.format(linkFormat, handle);
628                    }
629    
630                    public String getProtocol() {
631                            return protocol;
632                    }
633            }
634    }