001    package ezvcard.parameter;
002    
003    import java.nio.charset.Charset;
004    import java.nio.charset.IllegalCharsetNameException;
005    import java.nio.charset.UnsupportedCharsetException;
006    import java.util.ArrayList;
007    import java.util.Collections;
008    import java.util.EnumSet;
009    import java.util.HashMap;
010    import java.util.HashSet;
011    import java.util.List;
012    import java.util.Map;
013    import java.util.Set;
014    
015    import ezvcard.VCardDataType;
016    import ezvcard.VCardVersion;
017    import ezvcard.Warning;
018    import ezvcard.property.Address;
019    import ezvcard.property.Organization;
020    import ezvcard.property.StructuredName;
021    import ezvcard.util.GeoUri;
022    import ezvcard.util.ListMultimap;
023    
024    /*
025     Copyright (c) 2013, Michael Angstadt
026     All rights reserved.
027    
028     Redistribution and use in source and binary forms, with or without
029     modification, are permitted provided that the following conditions are met: 
030    
031     1. Redistributions of source code must retain the above copyright notice, this
032     list of conditions and the following disclaimer. 
033     2. Redistributions in binary form must reproduce the above copyright notice,
034     this list of conditions and the following disclaimer in the documentation
035     and/or other materials provided with the distribution. 
036    
037     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
038     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
039     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
040     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
041     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
042     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
043     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
044     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
045     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
046     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
047    
048     The views and conclusions contained in the software and documentation are those
049     of the authors and should not be interpreted as representing official policies, 
050     either expressed or implied, of the FreeBSD Project.
051     */
052    
053    /**
054     * Holds the parameters (aka "sub types") of a vCard property.
055     * @author Michael Angstadt
056     */
057    public class VCardParameters extends ListMultimap<String, String> {
058            public static final String ALTID = "ALTID";
059            public static final String CALSCALE = "CALSCALE";
060            public static final String CHARSET = "CHARSET";
061            public static final String ENCODING = "ENCODING";
062            public static final String GEO = "GEO";
063            public static final String INDEX = "INDEX";
064            public static final String LABEL = "LABEL";
065            public static final String LANGUAGE = "LANGUAGE";
066            public static final String LEVEL = "LEVEL";
067            public static final String MEDIATYPE = "MEDIATYPE";
068            public static final String PID = "PID";
069            public static final String PREF = "PREF";
070            public static final String SORT_AS = "SORT-AS";
071            public static final String TYPE = "TYPE";
072            public static final String TZ = "TZ";
073            public static final String VALUE = "VALUE";
074    
075            private static final Map<String, Set<VCardVersion>> supportedVersions;
076            static {
077                    Map<String, Set<VCardVersion>> m = new HashMap<String, Set<VCardVersion>>();
078                    m.put(ALTID, EnumSet.of(VCardVersion.V4_0));
079                    m.put(CALSCALE, EnumSet.of(VCardVersion.V4_0));
080                    m.put(CHARSET, EnumSet.of(VCardVersion.V2_1));
081                    m.put(GEO, EnumSet.of(VCardVersion.V4_0));
082                    m.put(INDEX, EnumSet.of(VCardVersion.V4_0));
083    
084                    //don't check LABEL because this is removed and converted to LABEL properties for 2.1 and 3.0 vCards 
085                    //m.put(LABEL, EnumSet.of(VCardVersion.V4_0));
086    
087                    m.put(LEVEL, EnumSet.of(VCardVersion.V4_0));
088                    m.put(MEDIATYPE, EnumSet.of(VCardVersion.V4_0));
089                    m.put(PID, EnumSet.of(VCardVersion.V4_0));
090    
091                    //don't check PREF because this is removed and converted to "TYPE=PREF" for 2.1 and 3.0 vCards
092                    //m.put(PREF, EnumSet.of(VCardVersion.V4_0));
093    
094                    m.put(SORT_AS, EnumSet.of(VCardVersion.V4_0));
095                    m.put(TZ, EnumSet.of(VCardVersion.V4_0));
096    
097                    supportedVersions = Collections.unmodifiableMap(m);
098            }
099    
100            /**
101             * Creates a list of parameters.
102             */
103            public VCardParameters() {
104                    //empty
105            }
106    
107            /**
108             * Creates a copy of an existing parameter list.
109             * @param orig the object to copy
110             */
111            public VCardParameters(VCardParameters orig) {
112                    super(orig);
113            }
114    
115            /**
116             * <p>
117             * Gets the ENCODING parameter. This is used when the property value is
118             * encoded in a form other than plain text.
119             * </p>
120             * <p>
121             * <b>Supported versions:</b> {@code 2.1, 3.0}
122             * </p>
123             * @return the encoding or null if not found
124             */
125            public Encoding getEncoding() {
126                    String value = first(ENCODING);
127                    return (value == null) ? null : Encoding.get(value);
128            }
129    
130            /**
131             * <p>
132             * Sets the ENCODING parameter. This is used when the property value is
133             * encoded in a form other than plain text.
134             * </p>
135             * <p>
136             * <b>Supported versions:</b> {@code 2.1, 3.0}
137             * </p>
138             * @param encoding the encoding or null to remove
139             */
140            public void setEncoding(Encoding encoding) {
141                    replace(ENCODING, (encoding == null) ? null : encoding.getValue());
142            }
143    
144            /**
145             * <p>
146             * Gets the VALUE parameter. This defines what kind of data type the
147             * property has, such as "text" or "URI". Only used in text-based vCards.
148             * </p>
149             * <p>
150             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
151             * </p>
152             * @return the value or null if not found
153             */
154            public VCardDataType getValue() {
155                    String value = first(VALUE);
156                    return (value == null) ? null : VCardDataType.get(value);
157            }
158    
159            /**
160             * <p>
161             * Sets the VALUE parameter. This defines what kind of data type the
162             * property has, such as "text" or "URI". Only used in text-based vCards.
163             * </p>
164             * <p>
165             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
166             * </p>
167             * @param value the value or null to remove
168             */
169            public void setValue(VCardDataType value) {
170                    replace(VALUE, (value == null) ? null : value.getName());
171            }
172    
173            /**
174             * <p>
175             * Removes the VALUE parameter. This defines what kind of data type the
176             * property has, such as "text" or "URI". Only used in text-based vCards.
177             * </p>
178             * <p>
179             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
180             * </p>
181             */
182            public void removeValue() {
183                    removeAll(VALUE);
184            }
185    
186            /**
187             * <p>
188             * Gets the CHARSET parameter.
189             * </p>
190             * <p>
191             * <b>Supported versions:</b> {@code 2.1}
192             * </p>
193             * @return the value or null if not found
194             */
195            public String getCharset() {
196                    return first(CHARSET);
197            }
198    
199            /**
200             * <p>
201             * Sets the CHARSET parameter.
202             * </p>
203             * <p>
204             * <b>Supported versions:</b> {@code 2.1}
205             * </p>
206             * @param charset the value or null to remove
207             */
208            public void setCharset(String charset) {
209                    replace(CHARSET, charset);
210            }
211    
212            /**
213             * <p>
214             * Gets the LANGUAGE parameter.
215             * </p>
216             * <p>
217             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
218             * </p>
219             * @return the language (e.g. "en-US") or null if not set
220             * @see <a href="http://tools.ietf.org/html/rfc5646">RFC 5646</a>
221             */
222            public String getLanguage() {
223                    return first(LANGUAGE);
224            }
225    
226            /**
227             * <p>
228             * Sets the LANGUAGE parameter.
229             * </p>
230             * <p>
231             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
232             * </p>
233             * @param language the language (e.g "en-US") or null to remove
234             * @see <a href="http://tools.ietf.org/html/rfc5646">RFC 5646</a>
235             */
236            public void setLanguage(String language) {
237                    replace(LANGUAGE, language);
238            }
239    
240            /**
241             * <p>
242             * Gets the LABEL parameter.
243             * </p>
244             * <p>
245             * <b>Supported versions:</b> {@code 4.0}
246             * </p>
247             * @return the address label or null if not set
248             */
249            public String getLabel() {
250                    return first(LABEL);
251            }
252    
253            /**
254             * <p>
255             * Sets the LABEL parameter.
256             * </p>
257             * <p>
258             * <b>Supported versions:</b> {@code 4.0}
259             * </p>
260             * @param label the address label or null to remove
261             */
262            public void setLabel(String label) {
263                    replace(LABEL, label);
264            }
265    
266            /**
267             * <p>
268             * Gets the TZ parameter.
269             * </p>
270             * <p>
271             * <b>Supported versions:</b> {@code 4.0}
272             * </p>
273             * @return the timezone (e.g. "America/New_York") or null if not set
274             */
275            public String getTimezone() {
276                    return first(TZ);
277            }
278    
279            /**
280             * <p>
281             * Sets the TZ parameter.
282             * </p>
283             * <p>
284             * <b>Supported versions:</b> {@code 4.0}
285             * </p>
286             * @param tz the timezone (e.g. "America/New_York") or null to remove
287             */
288            public void setTimezone(String tz) {
289                    replace(TZ, tz);
290            }
291    
292            /**
293             * <p>
294             * Gets all TYPE parameters.
295             * </p>
296             * <p>
297             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
298             * </p>
299             * @return the values or empty set if not found
300             */
301            public Set<String> getTypes() {
302                    return new HashSet<String>(get(TYPE));
303            }
304    
305            /**
306             * <p>
307             * Adds a TYPE parameter.
308             * </p>
309             * <p>
310             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
311             * </p>
312             * @param type the value
313             */
314            public void addType(String type) {
315                    put(TYPE, type);
316            }
317    
318            /**
319             * <p>
320             * Gets the first TYPE parameter.
321             * </p>
322             * <p>
323             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
324             * </p>
325             * @return the value or null if not found.
326             */
327            public String getType() {
328                    Set<String> types = getTypes();
329                    return types.isEmpty() ? null : types.iterator().next();
330            }
331    
332            /**
333             * <p>
334             * Sets the TYPE parameter.
335             * </p>
336             * <p>
337             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
338             * </p>
339             * @param type the value or null to remove
340             */
341            public void setType(String type) {
342                    replace(TYPE, type);
343            }
344    
345            /**
346             * <p>
347             * Removes a TYPE parameter.
348             * </p>
349             * <p>
350             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
351             * </p>
352             * @param type the value to remove
353             */
354            public void removeType(String type) {
355                    remove(TYPE, type);
356            }
357    
358            /**
359             * <p>
360             * Removes all TYPE parameters.
361             * </p>
362             * <p>
363             * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
364             * </p>
365             */
366            public void removeTypes() {
367                    removeAll(TYPE);
368            }
369    
370            /**
371             * <p>
372             * Gets the preference value. The lower the number, the more preferred this
373             * property instance is compared with other properties in the same vCard of
374             * the same type. If a property doesn't have a preference value, then it is
375             * considered the least preferred.
376             * </p>
377             * 
378             * <p>
379             * In the vCard below, the address on the second row is the most preferred
380             * because it has the lowest PREF value.
381             * </p>
382             * 
383             * <pre>
384             * ADR;TYPE=work;PREF=2:
385             * ADR;TYPE=work;PREF=1:
386             * ADR;TYPE=home:
387             * </pre>
388             * 
389             * <p>
390             * Preference values must be numeric and must be between 1 and 100.
391             * </p>
392             * 
393             * <p>
394             * <b>Supported versions:</b> {@code 4.0}
395             * </p>
396             * @throws IllegalStateException if the parameter value is malformed and
397             * cannot be parsed
398             * @return the preference value or null if it doesn't exist or null if it
399             * couldn't be parsed into a number
400             */
401            public Integer getPref() {
402                    String pref = first(PREF);
403                    if (pref == null) {
404                            return null;
405                    }
406    
407                    try {
408                            return Integer.valueOf(pref);
409                    } catch (NumberFormatException e) {
410                            throw new IllegalStateException(PREF + " parameter value is malformed and could not be parsed. Retrieve its raw text value instead.", e);
411                    }
412            }
413    
414            /**
415             * <p>
416             * Sets the preference value. The lower the number, the more preferred this
417             * property instance is compared with other properties in the same vCard of
418             * the same type. If a property doesn't have a preference value, then it is
419             * considered the least preferred.
420             * </p>
421             * 
422             * <p>
423             * In the vCard below, the address on the second row is the most preferred
424             * because it has the lowest PREF value.
425             * </p>
426             * 
427             * <pre>
428             * ADR;TYPE=work;PREF=2:
429             * ADR;TYPE=work;PREF=1:
430             * ADR;TYPE=home:
431             * </pre>
432             * 
433             * <p>
434             * Preference values must be numeric and must be between 1 and 100.
435             * </p>
436             * 
437             * <p>
438             * <b>Supported versions:</b> {@code 4.0}
439             * </p>
440             * @param pref the preference value or null to remove
441             * @throws IllegalArgumentException if the value is not between 1 and 100
442             */
443            public void setPref(Integer pref) {
444                    if (pref != null && (pref < 1 || pref > 100)) {
445                            throw new IllegalArgumentException("Preference value must be between 1 and 100 inclusive.");
446                    }
447                    String value = (pref == null) ? null : pref.toString();
448                    replace(PREF, value);
449            }
450    
451            /**
452             * <p>
453             * Gets the ALTID parameter value. This is used to specify alternative
454             * representations of the same type.
455             * </p>
456             * 
457             * <p>
458             * For example, a vCard may contain multiple NOTE properties that each have
459             * the same ALTID. This means that each NOTE contains a different
460             * representation of the same information. In the example below, the first
461             * three NOTEs have the same ALTID. They each contain the same message, but
462             * each is written in a different language. The other NOTEs have different
463             * (or absent) ALTIDs, which means they are not associated with the top
464             * three.
465             * </p>
466             * 
467             * <pre>
468             * NOTE;ALTID=1;LANGUAGE=en: Hello world!
469             * NOTE;ALTID=1;LANGUAGE=fr: Bonjour tout le monde!
470             * NOTE;ALTID=1;LANGUAGE=es: �Hola, mundo!
471             * NOTE;ALTID=2;LANGUAGE=de: Meine Lieblingsfarbe ist blau.
472             * NOTE;ALTID=2;LANGUAGE=en: My favorite color is blue.
473             * NOTE: This vCard will self-destruct in 5 seconds.
474             * </pre>
475             * 
476             * <p>
477             * <b>Supported versions:</b> {@code 4.0}
478             * </p>
479             * @return the ALTID or null if it doesn't exist
480             */
481            public String getAltId() {
482                    return first(ALTID);
483            }
484    
485            /**
486             * <p>
487             * Sets the ALTID parameter value. This is used to specify alternative
488             * representations of the same type.
489             * </p>
490             * 
491             * <p>
492             * For example, a vCard may contain multiple NOTE properties that each have
493             * the same ALTID. This means that each NOTE contains a different
494             * representation of the same information. In the example below, the first
495             * three NOTEs have the same ALTID. They each contain the same message, but
496             * each is written in a different language. The other NOTEs have different
497             * (or absent) ALTIDs, which means they are not associated with the top
498             * three.
499             * </p>
500             * 
501             * <pre>
502             * NOTE;ALTID=1;LANGUAGE=en: Hello world!
503             * NOTE;ALTID=1;LANGUAGE=fr: Bonjour tout le monde!
504             * NOTE;ALTID=1;LANGUAGE=es: �Hola, mundo!
505             * NOTE;ALTID=2;LANGUAGE=de: Meine Lieblingsfarbe ist blau.
506             * NOTE;ALTID=2;LANGUAGE=en: My favorite color is blue.
507             * NOTE: This vCard will self-destruct in 5 seconds.
508             * </pre>
509             * 
510             * <p>
511             * <b>Supported versions:</b> {@code 4.0}
512             * </p>
513             * @param altId the ALTID or null to remove
514             */
515            public void setAltId(String altId) {
516                    replace(ALTID, altId);
517            }
518    
519            /**
520             * <p>
521             * Gets the GEO parameter value. This is used to associate global
522             * positioning information with a vCard property. It can be used with the
523             * {@link Address} property.
524             * </p>
525             * <p>
526             * <b>Supported versions:</b> {@code 4.0}
527             * </p>
528             * @throws IllegalStateException if the parameter value is malformed and
529             * cannot be parsed
530             * @return the latitude (index 0) and longitude (index 1) or null if not
531             * present or null if the parameter value was in an incorrect format
532             */
533            public double[] getGeo() {
534                    String value = first(GEO);
535                    if (value == null) {
536                            return null;
537                    }
538    
539                    try {
540                            GeoUri geoUri = GeoUri.parse(value);
541                            return new double[] { geoUri.getCoordA(), geoUri.getCoordB() };
542                    } catch (IllegalArgumentException e) {
543                            throw new IllegalStateException(GEO + " parameter value is malformed and could not be parsed. Retrieve its raw text value instead.", e);
544                    }
545            }
546    
547            /**
548             * <p>
549             * Sets the GEO parameter value. This is used to associate global
550             * positioning information with a vCard property. It can be used with the
551             * {@link Address} property.
552             * </p>
553             * <p>
554             * <b>Supported versions:</b> {@code 4.0}
555             * </p>
556             * @param latitude the latitude
557             * @param longitude the longitude
558             */
559            public void setGeo(double latitude, double longitude) {
560                    GeoUri geoUri = new GeoUri.Builder(latitude, longitude).build();
561                    replace(GEO, geoUri.toString());
562            }
563    
564            /**
565             * <p>
566             * Gets the SORT-AS parameter value(s). This contains typically two string
567             * values which the vCard should be sorted by (family and given names). This
568             * is useful if the person's last name (defined in the N property) starts
569             * with characters that should be ignored during sorting. It can be used
570             * with the {@link StructuredName} and {@link Organization} properties.
571             * </p>
572             * <p>
573             * <b>Supported versions:</b> {@code 4.0}
574             * </p>
575             * @return the name(s) (e.g. { "Aboville", "Christine" } if the family name
576             * is "d'Aboville" and the given name is "Christine") or empty list of the
577             * parameter doesn't exist
578             */
579            public List<String> getSortAs() {
580                    return get(SORT_AS);
581            }
582    
583            /**
584             * <p>
585             * Sets the SORT-AS parameter value(s). This is useful with the N property
586             * when the person's last name starts with characters that should be ignored
587             * during sorting. It can be used with the {@link StructuredName} and
588             * {@link Organization} properties.
589             * </p>
590             * <p>
591             * <b>Supported versions:</b> {@code 4.0}
592             * </p>
593             * @param names the names in the order they should be sorted in (e.g.
594             * ["Aboville", "Christine"] if the family name is "d'Aboville" and the
595             * given name is "Christine") or empty parameter list to remove
596             */
597            public void setSortAs(String... names) {
598                    removeAll(SORT_AS);
599                    if (names != null && names.length > 0) {
600                            for (String name : names) {
601                                    put(SORT_AS, name);
602                            }
603                    }
604            }
605    
606            /**
607             * <p>
608             * Gets the type of calendar that is used for a date or date-time property
609             * value.
610             * </p>
611             * <p>
612             * <b>Supported versions:</b> {@code 4.0}
613             * </p>
614             * @return the type of calendar or null if not found
615             */
616            public Calscale getCalscale() {
617                    String value = first(CALSCALE);
618                    return (value == null) ? null : Calscale.get(value);
619            }
620    
621            /**
622             * <p>
623             * Sets the type of calendar that is used for a date or date-time property
624             * value.
625             * </p>
626             * <p>
627             * <b>Supported versions:</b> {@code 4.0}
628             * </p>
629             * @param value the type of calendar or null to remove
630             */
631            public void setCalscale(Calscale value) {
632                    replace(CALSCALE, (value == null) ? null : value.getValue());
633            }
634    
635            /**
636             * <p>
637             * Gets all PID parameter values. PIDs can exist on any property where
638             * multiple instances are allowed (such as EMAIL or ADR, but not N because
639             * only 1 instance of N is allowed).
640             * </p>
641             * <p>
642             * When used in conjunction with the CLIENTPIDMAP property, it allows an
643             * individual property instance to be uniquely identifiable. This feature is
644             * made use of when two different versions of the same vCard have to be
645             * merged together (called "synchronizing").
646             * </p>
647             * <p>
648             * <b>Supported versions:</b> {@code 4.0}
649             * </p>
650             * @throws IllegalStateException if the parameter value is malformed and
651             * cannot be parsed
652             * @return the PID values or empty set if there are none. Index 0 is the
653             * local ID and index 1 is the ID used to reference the CLIENTPIDMAP
654             * property. Index 0 will never be null, but index 1 may be null.
655             */
656            public List<Integer[]> getPids() {
657                    List<String> values = get(PID);
658                    List<Integer[]> pids = new ArrayList<Integer[]>(values.size());
659                    for (String value : values) {
660                            String split[] = value.split("\\.");
661                            try {
662                                    Integer localId = Integer.valueOf(split[0]);
663                                    Integer clientPidMapRef = (split.length > 1) ? Integer.valueOf(split[1]) : null;
664                                    pids.add(new Integer[] { localId, clientPidMapRef });
665                            } catch (NumberFormatException e) {
666                                    throw new IllegalStateException(PID + " parameter value is malformed and could not be parsed. Retrieve its raw text value instead.", e);
667                            }
668                    }
669                    return pids;
670            }
671    
672            /**
673             * <p>
674             * Adds a PID parameter value. PIDs can exist on any property where multiple
675             * instances are allowed (such as EMAIL or ADR, but not N because only 1
676             * instance of N is allowed).
677             * </p>
678             * <p>
679             * When used in conjunction with the CLIENTPIDMAP property, it allows an
680             * individual property instance to be uniquely identifiable. This feature is
681             * made use of when two different versions of the same vCard have to be
682             * merged together (called "synchronizing").
683             * </p>
684             * <p>
685             * <b>Supported versions:</b> {@code 4.0}
686             * </p>
687             * @param localId the local ID
688             */
689            public void addPid(int localId) {
690                    put(PID, localId + "");
691            }
692    
693            /**
694             * <p>
695             * Adds a PID parameter value. PIDs can exist on any property where multiple
696             * instances are allowed (such as EMAIL or ADR, but not N because only 1
697             * instance of N is allowed).
698             * </p>
699             * <p>
700             * When used in conjunction with the CLIENTPIDMAP property, it allows an
701             * individual property instance to be uniquely identifiable. This feature is
702             * made use of when two different versions of the same vCard have to be
703             * merged together (called "synchronizing").
704             * </p>
705             * <p>
706             * <b>Supported versions:</b> {@code 4.0}
707             * </p>
708             * @param localId the local ID
709             * @param clientPidMapRef the ID used to reference the property's globally
710             * unique identifier in the CLIENTPIDMAP property.
711             */
712            public void addPid(int localId, int clientPidMapRef) {
713                    put(PID, localId + "." + clientPidMapRef);
714            }
715    
716            /**
717             * <p>
718             * Removes all PID values.
719             * </p>
720             * <p>
721             * <b>Supported versions:</b> {@code 4.0}
722             * </p>
723             */
724            public void removePids() {
725                    removeAll(PID);
726            }
727    
728            /**
729             * <p>
730             * Gets the MEDIATYPE parameter. This is used in properties that have a URL
731             * as a value, such as PHOTO and SOUND. It defines the content type of the
732             * referenced resource.
733             * </p>
734             * <p>
735             * <b>Supported versions:</b> {@code 4.0}
736             * </p>
737             * @return the media type (e.g. "image/jpeg") or null if it doesn't exist
738             */
739            public String getMediaType() {
740                    return first(MEDIATYPE);
741            }
742    
743            /**
744             * <p>
745             * Sets the MEDIATYPE parameter. This is used in properties that have a URL
746             * as a value, such as PHOTO and SOUND. It defines the content type of the
747             * referenced resource.
748             * </p>
749             * <p>
750             * <b>Supported versions:</b> {@code 4.0}
751             * </p>
752             * @param mediaType the media type (e.g. "image/jpeg") or null to remove
753             */
754            public void setMediaType(String mediaType) {
755                    replace(MEDIATYPE, mediaType);
756            }
757    
758            /**
759             * <p>
760             * Gets the LEVEL parameter. This is used to define the level of skill or
761             * level of interest the person has towards something.
762             * </p>
763             * <p>
764             * <b>Supported versions:</b> {@code 4.0}
765             * </p>
766             * @return the level (e.g. "beginner") or null if not found
767             * @see <a href="http://tools.ietf.org/html/rfc6715">RFC 6715</a>
768             */
769            public String getLevel() {
770                    return first(LEVEL);
771            }
772    
773            /**
774             * <p>
775             * Sets the LEVEL parameter. This is used to define the level of skill or
776             * level of interest the person has towards something.
777             * </p>
778             * <p>
779             * <b>Supported versions:</b> {@code 4.0}
780             * </p>
781             * @param level the level (e.g. "beginner") or null to remove
782             * @see <a href="http://tools.ietf.org/html/rfc6715">RFC 6715</a>
783             */
784            public void setLevel(String level) {
785                    replace(LEVEL, level);
786            }
787    
788            /**
789             * <p>
790             * Gets the sorted position of this property when it is grouped together
791             * with other properties of the same type. Properties with low index values
792             * are put at the beginning of the sorted list and properties with high
793             * index values are put at the end of the list.
794             * </p>
795             * 
796             * <p>
797             * <b>Supported versions:</b> {@code 4.0}
798             * </p>
799             * @throws IllegalStateException if the parameter value is malformed and
800             * cannot be parsed
801             * @return the INDEX value or null if it doesn't exist or null if it
802             * couldn't be parsed into a number
803             * @see <a href="http://tools.ietf.org/html/rfc6715">RFC 6715</a>
804             */
805            public Integer getIndex() {
806                    String index = first(INDEX);
807                    if (index == null) {
808                            return null;
809                    }
810    
811                    try {
812                            return Integer.valueOf(index);
813                    } catch (NumberFormatException e) {
814                            throw new IllegalStateException(INDEX + " parameter value is malformed and could not be parsed. Retrieve its raw text value instead.", e);
815                    }
816            }
817    
818            /**
819             * <p>
820             * Sets the sorted position of this property when it is grouped together
821             * with other properties of the same type. Properties with low index values
822             * are put at the beginning of the sorted list and properties with high
823             * index values are put at the end of the list.
824             * </p>
825             * 
826             * <p>
827             * <b>Supported versions:</b> {@code 4.0}
828             * </p>
829             * @param index the INDEX value (must be greater than 0) or null to remove
830             * @see <a href="http://tools.ietf.org/html/rfc6715">RFC 6715</a>
831             * @throws IllegalArgumentException if the value is not greater than 0
832             */
833            public void setIndex(Integer index) {
834                    if (index != null && index <= 0) {
835                            throw new IllegalArgumentException("Index value must be greater than 0.");
836                    }
837                    String value = (index == null) ? null : index.toString();
838                    replace(INDEX, value);
839            }
840    
841            /**
842             * Checks this parameters list for data consistency problems or deviations
843             * from the spec. These problems will not prevent the vCard from being
844             * written to a data stream, but may prevent it from being parsed correctly
845             * by the consuming application.
846             * @param version the vCard version to validate against
847             * @return a list of warnings or an empty list if no problems were found
848             */
849            public List<Warning> validate(VCardVersion version) {
850                    List<Warning> warnings = new ArrayList<Warning>(0);
851    
852                    {
853                            int nonStandardCode = 3;
854                            int valueNotSupportedCode = 4;
855    
856                            String value = first(CALSCALE);
857                            if (value != null && Calscale.find(value) == null) {
858                                    warnings.add(new Warning(nonStandardCode, CALSCALE, value, Calscale.all()));
859                            }
860    
861                            value = first(ENCODING);
862                            if (value != null) {
863                                    Encoding encoding = Encoding.find(value);
864                                    if (encoding == null) {
865                                            warnings.add(new Warning(nonStandardCode, ENCODING, value, Encoding.all()));
866                                    } else if (!encoding.isSupported(version)) {
867                                            warnings.add(new Warning(valueNotSupportedCode, ENCODING, value));
868                                    }
869                            }
870    
871                            value = first(VALUE);
872                            if (value != null) {
873                                    VCardDataType dataType = VCardDataType.find(value);
874                                    if (dataType == null) {
875                                            warnings.add(new Warning(nonStandardCode, VALUE, value, VCardDataType.all()));
876                                    } else if (!dataType.isSupported(version)) {
877                                            warnings.add(new Warning(valueNotSupportedCode, VALUE, value));
878                                    }
879                            }
880                    }
881    
882                    {
883                            int malformedCode = 5;
884    
885                            try {
886                                    getGeo();
887                            } catch (IllegalStateException e) {
888                                    warnings.add(new Warning(malformedCode, GEO, first(GEO)));
889                            }
890    
891                            try {
892                                    getIndex();
893                            } catch (IllegalStateException e) {
894                                    warnings.add(new Warning(malformedCode, INDEX, first(INDEX)));
895                            }
896    
897                            try {
898                                    getPids();
899                            } catch (IllegalStateException e) {
900                                    warnings.add(new Warning(malformedCode, PID, first(PID)));
901                            }
902    
903                            try {
904                                    getPref();
905                            } catch (IllegalStateException e) {
906                                    warnings.add(new Warning(malformedCode, PREF, first(PREF)));
907                            }
908                    }
909    
910                    {
911                            int paramNotSupportedCode = 6;
912                            for (Map.Entry<String, Set<VCardVersion>> entry : supportedVersions.entrySet()) {
913                                    String name = entry.getKey();
914                                    String value = first(name);
915                                    if (value == null) {
916                                            continue;
917                                    }
918    
919                                    Set<VCardVersion> versions = entry.getValue();
920                                    if (!versions.contains(version)) {
921                                            warnings.add(new Warning(paramNotSupportedCode, name));
922                                    }
923                            }
924                    }
925    
926                    {
927                            int invalidCharsetCode = 22;
928                            String charsetStr = getCharset();
929                            if (charsetStr != null) {
930                                    try {
931                                            Charset.forName(charsetStr);
932                                    } catch (IllegalCharsetNameException e) {
933                                            warnings.add(new Warning(invalidCharsetCode, charsetStr));
934                                    } catch (UnsupportedCharsetException e) {
935                                            warnings.add(new Warning(invalidCharsetCode, charsetStr));
936                                    }
937                            }
938                    }
939    
940                    return warnings;
941            }
942    
943            @Override
944            protected String sanitizeKey(String key) {
945                    return (key == null) ? null : key.toUpperCase();
946            }
947    }