001package ezvcard.property;
002
003import java.util.ArrayList;
004import java.util.LinkedHashMap;
005import java.util.List;
006import java.util.Map;
007
008import ezvcard.VCard;
009import ezvcard.VCardVersion;
010import ezvcard.ValidationWarning;
011import ezvcard.parameter.AddressType;
012import ezvcard.parameter.Pid;
013import ezvcard.parameter.VCardParameters;
014import ezvcard.util.GeoUri;
015import ezvcard.util.StringUtils;
016
017/*
018 Copyright (c) 2012-2023, Michael Angstadt
019 All rights reserved.
020
021 Redistribution and use in source and binary forms, with or without
022 modification, are permitted provided that the following conditions are met: 
023
024 1. Redistributions of source code must retain the above copyright notice, this
025 list of conditions and the following disclaimer. 
026 2. Redistributions in binary form must reproduce the above copyright notice,
027 this list of conditions and the following disclaimer in the documentation
028 and/or other materials provided with the distribution. 
029
030 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
031 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
032 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
033 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
034 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
035 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
036 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
037 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
038 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
039 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
040
041 The views and conclusions contained in the software and documentation are those
042 of the authors and should not be interpreted as representing official policies, 
043 either expressed or implied, of the FreeBSD Project.
044 */
045
046/**
047 * <p>
048 * Defines a mailing address.
049 * </p>
050 * 
051 * <p>
052 * <b>Code sample (creating)</b>
053 * </p>
054 * 
055 * <pre class="brush:java">
056 * VCard vcard = new VCard();
057 * 
058 * Address adr = new Address();
059 * adr.setStreetAddress("123 Main St.");
060 * adr.setLocality("Austin");
061 * adr.setRegion("TX");
062 * adr.setPostalCode("12345");
063 * adr.setCountry("USA");
064 * adr.getTypes().add(AddressType.WORK);
065 * 
066 * //optionally, set the text to print on the mailing label
067 * adr.setLabel("123 Main St.\nAustin, TX 12345\nUSA");
068 * 
069 * vcard.addAddress(adr);
070 * </pre>
071 * 
072 * <p>
073 * <b>Code sample (retrieving)</b>
074 * </p>
075 * 
076 * <pre class="brush:java">
077 * VCard vcard = ...
078 * for (Address adr : vcard.getAddresses()) {
079 *   String street = adr.getStreetAddress();
080 *   String city = adr.getLocality();
081 *   //etc.
082 * }
083 * </pre>
084 * 
085 * <p>
086 * <b>Only part of the street address is being returned!</b>
087 * </p>
088 * <p>
089 * This usually means that the vCard you parsed contains unescaped comma
090 * characters. To get the full address, use the {@link #getStreetAddressFull}
091 * method.
092 * </p>
093 * 
094 * <p>
095 * <b>Property name:</b> {@code ADR}
096 * </p>
097 * <p>
098 * <b>Supported versions:</b> {@code 2.1, 3.0, 4.0}
099 * </p>
100 * @author Michael Angstadt
101 * @see <a href="http://tools.ietf.org/html/rfc6350#page-32">RFC 6350 p.32</a>
102 * @see <a href="http://tools.ietf.org/html/rfc2426#page-11">RFC 2426 p.11</a>
103 * @see <a href="http://www.imc.org/pdi/vcard-21.doc">vCard 2.1 p.11</a>
104 */
105public class Address extends VCardProperty implements HasAltId {
106        private final List<String> poBoxes;
107        private final List<String> extendedAddresses;
108        private final List<String> streetAddresses;
109        private final List<String> localities;
110        private final List<String> regions;
111        private final List<String> postalCodes;
112        private final List<String> countries;
113
114        public Address() {
115                poBoxes = new ArrayList<>(1);
116                extendedAddresses = new ArrayList<>(1);
117                streetAddresses = new ArrayList<>(1);
118                localities = new ArrayList<>(1);
119                regions = new ArrayList<>(1);
120                postalCodes = new ArrayList<>(1);
121                countries = new ArrayList<>(1);
122        }
123
124        /**
125         * Copy constructor.
126         * @param original the property to make a copy of
127         */
128        public Address(Address original) {
129                super(original);
130                poBoxes = new ArrayList<>(original.poBoxes);
131                extendedAddresses = new ArrayList<>(original.extendedAddresses);
132                streetAddresses = new ArrayList<>(original.streetAddresses);
133                localities = new ArrayList<>(original.localities);
134                regions = new ArrayList<>(original.regions);
135                postalCodes = new ArrayList<>(original.postalCodes);
136                countries = new ArrayList<>(original.countries);
137        }
138
139        /**
140         * Gets the P.O. (post office) box.
141         * @return the P.O. box or null if not set
142         */
143        public String getPoBox() {
144                return first(poBoxes);
145        }
146
147        /**
148         * Gets the list that holds the P.O. (post office) boxes that are assigned
149         * to this address. An address is unlikely to have more than one, but it's
150         * possible nonetheless.
151         * @return the P.O. boxes (this list is mutable)
152         */
153        public List<String> getPoBoxes() {
154                return poBoxes;
155        }
156
157        /**
158         * Sets the P.O. (post office) box.
159         * @param poBox the P.O. box or null to remove
160         */
161        public void setPoBox(String poBox) {
162                set(poBoxes, poBox);
163        }
164
165        /**
166         * Gets the extended address.
167         * @return the extended address (e.g. "Suite 200") or null if not set
168         */
169        public String getExtendedAddress() {
170                return first(extendedAddresses);
171        }
172
173        /**
174         * Gets the list that holds the extended addresses that are assigned to this
175         * address. An address is unlikely to have more than one, but it's possible
176         * nonetheless.
177         * @return the extended addresses (this list is mutable)
178         */
179        public List<String> getExtendedAddresses() {
180                return extendedAddresses;
181        }
182
183        /**
184         * Gets the extended address. Use this method when the ADR property of the
185         * vCard you are parsing contains unescaped comma characters.
186         * @return the extended address or null if not set
187         */
188        public String getExtendedAddressFull() {
189                return getAddressFull(extendedAddresses);
190        }
191
192        /**
193         * Sets the extended address.
194         * @param extendedAddress the extended address (e.g. "Suite 200") or null to
195         * remove
196         */
197        public void setExtendedAddress(String extendedAddress) {
198                set(extendedAddresses, extendedAddress);
199        }
200
201        /**
202         * Gets the street address
203         * @return the street address (e.g. "123 Main St")
204         */
205        public String getStreetAddress() {
206                return first(streetAddresses);
207        }
208
209        /**
210         * Gets the list that holds the street addresses that are assigned to this
211         * address. An address is unlikely to have more than one, but it's possible
212         * nonetheless.
213         * @return the street addresses (this list is mutable)
214         */
215        public List<String> getStreetAddresses() {
216                return streetAddresses;
217        }
218
219        /**
220         * Gets the street address. Use this method when the ADR property of the
221         * vCard you are parsing contains unescaped comma characters.
222         * @return the street address or null if not set
223         */
224        public String getStreetAddressFull() {
225                return getAddressFull(streetAddresses);
226        }
227
228        /**
229         * Sets the street address.
230         * @param streetAddress the street address (e.g. "123 Main St") or null to
231         * remove
232         */
233        public void setStreetAddress(String streetAddress) {
234                set(streetAddresses, streetAddress);
235        }
236
237        /**
238         * Gets the locality (city)
239         * @return the locality (e.g. "Boston") or null if not set
240         */
241        public String getLocality() {
242                return first(localities);
243        }
244
245        /**
246         * Gets the list that holds the localities that are assigned to this
247         * address. An address is unlikely to have more than one, but it's possible
248         * nonetheless.
249         * @return the localities (this list is mutable)
250         */
251        public List<String> getLocalities() {
252                return localities;
253        }
254
255        /**
256         * Sets the locality (city).
257         * @param locality the locality or null to remove
258         */
259        public void setLocality(String locality) {
260                set(localities, locality);
261        }
262
263        /**
264         * Gets the region (state).
265         * @return the region (e.g. "Texas") or null if not set
266         */
267        public String getRegion() {
268                return first(regions);
269        }
270
271        /**
272         * Gets the list that holds the regions that are assigned to this address.
273         * An address is unlikely to have more than one, but it's possible
274         * nonetheless.
275         * @return the regions (this list is mutable)
276         */
277        public List<String> getRegions() {
278                return regions;
279        }
280
281        /**
282         * Sets the region (state).
283         * @param region the region (e.g. "Texas") or null to remove
284         */
285        public void setRegion(String region) {
286                set(regions, region);
287        }
288
289        /**
290         * Gets the postal code (zip code).
291         * @return the postal code (e.g. "90210") or null if not set
292         */
293        public String getPostalCode() {
294                return first(postalCodes);
295        }
296
297        /**
298         * Gets the list that holds the postal codes that are assigned to this
299         * address. An address is unlikely to have more than one, but it's possible
300         * nonetheless.
301         * @return the postal codes (this list is mutable)
302         */
303        public List<String> getPostalCodes() {
304                return postalCodes;
305        }
306
307        /**
308         * Sets the postal code (zip code).
309         * @param postalCode the postal code (e.g. "90210") or null to remove
310         */
311        public void setPostalCode(String postalCode) {
312                set(postalCodes, postalCode);
313        }
314
315        /**
316         * Gets the country.
317         * @return the country (e.g. "USA") or null if not set
318         */
319        public String getCountry() {
320                return first(countries);
321        }
322
323        /**
324         * Gets the list that holds the countries that are assigned to this address.
325         * An address is unlikely to have more than one, but it's possible
326         * nonetheless.
327         * @return the countries (this list is mutable)
328         */
329        public List<String> getCountries() {
330                return countries;
331        }
332
333        /**
334         * Sets the country.
335         * @param country the country (e.g. "USA") or null to remove
336         */
337        public void setCountry(String country) {
338                set(countries, country);
339        }
340
341        /**
342         * Gets the list that stores this property's address types (TYPE
343         * parameters).
344         * @return the address types (e.g. "HOME", "WORK") (this list is mutable)
345         */
346        public List<AddressType> getTypes() {
347                return parameters.new TypeParameterList<AddressType>() {
348                        @Override
349                        protected AddressType _asObject(String value) {
350                                return AddressType.get(value);
351                        }
352                };
353        }
354
355        @Override
356        public String getLanguage() {
357                return super.getLanguage();
358        }
359
360        @Override
361        public void setLanguage(String language) {
362                super.setLanguage(language);
363        }
364
365        /**
366         * Gets the label of the address.
367         * @return the label or null if not set
368         */
369        public String getLabel() {
370                return parameters.getLabel();
371        }
372
373        /**
374         * Sets the label of the address.
375         * @param label the label or null to remove
376         */
377        public void setLabel(String label) {
378                parameters.setLabel(label);
379        }
380
381        /**
382         * <p>
383         * Gets the global positioning coordinates that are associated with this
384         * address.
385         * </p>
386         * <p>
387         * <b>Supported versions:</b> {@code 4.0}
388         * </p>
389         * @return the geo URI or not if not found
390         * @see VCardParameters#getGeo
391         */
392        public GeoUri getGeo() {
393                return parameters.getGeo();
394        }
395
396        /**
397         * <p>
398         * Sets the global positioning coordinates that are associated with this
399         * address.
400         * </p>
401         * <p>
402         * <b>Supported versions:</b> {@code 4.0}
403         * </p>
404         * @param uri the geo URI or null to remove
405         * @see VCardParameters#setGeo
406         */
407        public void setGeo(GeoUri uri) {
408                parameters.setGeo(uri);
409        }
410
411        @Override
412        public List<Pid> getPids() {
413                return super.getPids();
414        }
415
416        @Override
417        public Integer getPref() {
418                return super.getPref();
419        }
420
421        @Override
422        public void setPref(Integer pref) {
423                super.setPref(pref);
424        }
425
426        //@Override
427        public String getAltId() {
428                return parameters.getAltId();
429        }
430
431        //@Override
432        public void setAltId(String altId) {
433                parameters.setAltId(altId);
434        }
435
436        /**
437         * Gets the timezone that's associated with this address.
438         * <p>
439         * <b>Supported versions:</b> {@code 4.0}
440         * </p>
441         * @return the timezone (e.g. "America/New_York") or null if not set
442         */
443        public String getTimezone() {
444                return parameters.getTimezone();
445        }
446
447        /**
448         * Sets the timezone that's associated with this address.
449         * <p>
450         * <b>Supported versions:</b> {@code 4.0}
451         * </p>
452         * @param timezone the timezone (e.g. "America/New_York") or null to remove
453         */
454        public void setTimezone(String timezone) {
455                parameters.setTimezone(timezone);
456        }
457
458        @Override
459        protected void _validate(List<ValidationWarning> warnings, VCardVersion version, VCard vcard) {
460                for (AddressType type : getTypes()) {
461                        if (type == AddressType.PREF) {
462                                //ignore because it is converted to a PREF parameter for 4.0 vCards
463                                continue;
464                        }
465
466                        if (!type.isSupportedBy(version)) {
467                                warnings.add(new ValidationWarning(9, type.getValue()));
468                        }
469                }
470
471                /*
472                 * 2.1 does not allow multi-valued components.
473                 */
474                if (version == VCardVersion.V2_1) {
475                        //@formatter:off
476                        if (poBoxes.size() > 1 ||
477                                extendedAddresses.size() > 1 ||
478                                streetAddresses.size() > 1 ||
479                                localities.size() > 1 ||
480                                regions.size() > 1 ||
481                                postalCodes.size() > 1 ||
482                                countries.size() > 1) {
483                                warnings.add(new ValidationWarning(35));
484                        }
485                        //@formatter:on
486                }
487        }
488
489        @Override
490        protected Map<String, Object> toStringValues() {
491                Map<String, Object> values = new LinkedHashMap<>();
492                values.put("poBoxes", poBoxes);
493                values.put("extendedAddresses", extendedAddresses);
494                values.put("streetAddresses", streetAddresses);
495                values.put("localities", localities);
496                values.put("regions", regions);
497                values.put("postalCodes", postalCodes);
498                values.put("countries", countries);
499                return values;
500        }
501
502        @Override
503        public Address copy() {
504                return new Address(this);
505        }
506
507        @Override
508        public int hashCode() {
509                final int prime = 31;
510                int result = super.hashCode();
511                result = prime * result + countries.hashCode();
512                result = prime * result + extendedAddresses.hashCode();
513                result = prime * result + localities.hashCode();
514                result = prime * result + poBoxes.hashCode();
515                result = prime * result + postalCodes.hashCode();
516                result = prime * result + regions.hashCode();
517                result = prime * result + streetAddresses.hashCode();
518                return result;
519        }
520
521        @Override
522        public boolean equals(Object obj) {
523                if (this == obj) return true;
524                if (!super.equals(obj)) return false;
525                Address other = (Address) obj;
526                if (!countries.equals(other.countries)) return false;
527                if (!extendedAddresses.equals(other.extendedAddresses)) return false;
528                if (!localities.equals(other.localities)) return false;
529                if (!poBoxes.equals(other.poBoxes)) return false;
530                if (!postalCodes.equals(other.postalCodes)) return false;
531                if (!regions.equals(other.regions)) return false;
532                if (!streetAddresses.equals(other.streetAddresses)) return false;
533                return true;
534        }
535
536        private static String first(List<String> list) {
537                return list.isEmpty() ? null : list.get(0);
538        }
539
540        private static void set(List<String> list, String value) {
541                list.clear();
542                if (value != null) {
543                        list.add(value);
544                }
545        }
546
547        private static String getAddressFull(List<String> list) {
548                return list.isEmpty() ? null : StringUtils.join(list, ",");
549        }
550}