001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.vcardtemp.packet;
019
020import java.io.BufferedInputStream;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.lang.reflect.Field;
025import java.lang.reflect.Modifier;
026import java.net.URL;
027import java.security.MessageDigest;
028import java.security.NoSuchAlgorithmException;
029import java.util.HashMap;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034
035import org.jivesoftware.smack.SmackException.NoResponseException;
036import org.jivesoftware.smack.SmackException.NotConnectedException;
037import org.jivesoftware.smack.XMPPConnection;
038import org.jivesoftware.smack.XMPPException.XMPPErrorException;
039import org.jivesoftware.smack.packet.IQ;
040import org.jivesoftware.smack.util.StringUtils;
041import org.jivesoftware.smack.util.stringencoder.Base64;
042import org.jivesoftware.smackx.vcardtemp.VCardManager;
043
044/**
045 * A VCard class for use with the
046 * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p>
047 * <p/>
048 * You should refer to the
049 * <a href="http://www.xmpp.org/extensions/jep-0054.html" target="_blank">XEP-54 documentation</a>.<p>
050 * <p/>
051 * Please note that this class is incomplete but it does provide the most commonly found
052 * information in vCards. Also remember that VCard transfer is not a standard, and the protocol
053 * may change or be replaced.<p>
054 * <p/>
055 * <b>Usage:</b>
056 * <pre>
057 * <p/>
058 * // To save VCard:
059 * <p/>
060 * VCard vCard = new VCard();
061 * vCard.setFirstName("kir");
062 * vCard.setLastName("max");
063 * vCard.setEmailHome("foo@fee.bar");
064 * vCard.setJabberId("jabber@id.org");
065 * vCard.setOrganization("Jetbrains, s.r.o");
066 * vCard.setNickName("KIR");
067 * <p/>
068 * vCard.setField("TITLE", "Mr");
069 * vCard.setAddressFieldHome("STREET", "Some street");
070 * vCard.setAddressFieldWork("CTRY", "US");
071 * vCard.setPhoneWork("FAX", "3443233");
072 * <p/>
073 * vCard.save(connection);
074 * <p/>
075 * // To load VCard:
076 * <p/>
077 * VCard vCard = new VCard();
078 * vCard.load(conn); // load own VCard
079 * vCard.load(conn, "joe@foo.bar"); // load someone's VCard
080 * </pre>
081 *
082 * @author Kirill Maximov (kir@maxkir.com)
083 */
084public class VCard extends IQ {
085    public static final String ELEMENT = "vCard";
086    public static final String NAMESPACE = "vcard-temp";
087
088    private static final Logger LOGGER = Logger.getLogger(VCard.class.getName());
089    
090    private static final String DEFAULT_MIME_TYPE = "image/jpeg";
091    
092    /**
093     * Phone types:
094     * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF?
095     */
096    private Map<String, String> homePhones = new HashMap<String, String>();
097    private Map<String, String> workPhones = new HashMap<String, String>();
098
099    /**
100     * Address types:
101     * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?,
102     * REGION?, PCODE?, CTRY?
103     */
104    private Map<String, String> homeAddr = new HashMap<String, String>();
105    private Map<String, String> workAddr = new HashMap<String, String>();
106
107    private String firstName;
108    private String lastName;
109    private String middleName;
110
111    private String emailHome;
112    private String emailWork;
113
114    private String organization;
115    private String organizationUnit;
116
117    private String photoMimeType;
118    private String photoBinval;
119
120    /**
121     * Such as DESC ROLE GEO etc.. see XEP-0054
122     */
123    private Map<String, String> otherSimpleFields = new HashMap<String, String>();
124
125    // fields that, as they are should not be escaped before forwarding to the server
126    private Map<String, String> otherUnescapableFields = new HashMap<String, String>();
127
128    public VCard() {
129        super(ELEMENT, NAMESPACE);
130    }
131
132    /**
133     * Set generic VCard field.
134     *
135     * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ,
136     *              GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC.
137     */
138    public String getField(String field) {
139        return otherSimpleFields.get(field);
140    }
141
142    /**
143     * Set generic VCard field.
144     *
145     * @param value value of field
146     * @param field field to set. See {@link #getField(String)}
147     * @see #getField(String)
148     */
149    public void setField(String field, String value) {
150        setField(field, value, false);
151    }
152
153    /**
154     * Set generic, unescapable VCard field. If unescabale is set to true, XML maybe a part of the
155     * value.
156     *
157     * @param value         value of field
158     * @param field         field to set. See {@link #getField(String)}
159     * @param isUnescapable True if the value should not be escaped, and false if it should.
160     */
161    public void setField(String field, String value, boolean isUnescapable) {
162        if (!isUnescapable) {
163            otherSimpleFields.put(field, value);
164        }
165        else {
166            otherUnescapableFields.put(field, value);
167        }
168    }
169
170    public String getFirstName() {
171        return firstName;
172    }
173
174    public void setFirstName(String firstName) {
175        this.firstName = firstName;
176        // Update FN field
177        updateFN();
178    }
179
180    public String getLastName() {
181        return lastName;
182    }
183
184    public void setLastName(String lastName) {
185        this.lastName = lastName;
186        // Update FN field
187        updateFN();
188    }
189
190    public String getMiddleName() {
191        return middleName;
192    }
193
194    public void setMiddleName(String middleName) {
195        this.middleName = middleName;
196        // Update FN field
197        updateFN();
198    }
199
200    public String getNickName() {
201        return otherSimpleFields.get("NICKNAME");
202    }
203
204    public void setNickName(String nickName) {
205        otherSimpleFields.put("NICKNAME", nickName);
206    }
207
208    public String getEmailHome() {
209        return emailHome;
210    }
211
212    public void setEmailHome(String email) {
213        this.emailHome = email;
214    }
215
216    public String getEmailWork() {
217        return emailWork;
218    }
219
220    public void setEmailWork(String emailWork) {
221        this.emailWork = emailWork;
222    }
223
224    public String getJabberId() {
225        return otherSimpleFields.get("JABBERID");
226    }
227
228    public void setJabberId(String jabberId) {
229        otherSimpleFields.put("JABBERID", jabberId);
230    }
231
232    public String getOrganization() {
233        return organization;
234    }
235
236    public void setOrganization(String organization) {
237        this.organization = organization;
238    }
239
240    public String getOrganizationUnit() {
241        return organizationUnit;
242    }
243
244    public void setOrganizationUnit(String organizationUnit) {
245        this.organizationUnit = organizationUnit;
246    }
247
248    /**
249     * Get home address field
250     *
251     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
252     *                  LOCALITY, REGION, PCODE, CTRY
253     */
254    public String getAddressFieldHome(String addrField) {
255        return homeAddr.get(addrField);
256    }
257
258    /**
259     * Set home address field
260     *
261     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
262     *                  LOCALITY, REGION, PCODE, CTRY
263     */
264    public void setAddressFieldHome(String addrField, String value) {
265        homeAddr.put(addrField, value);
266    }
267
268    /**
269     * Get work address field
270     *
271     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
272     *                  LOCALITY, REGION, PCODE, CTRY
273     */
274    public String getAddressFieldWork(String addrField) {
275        return workAddr.get(addrField);
276    }
277
278    /**
279     * Set work address field
280     *
281     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
282     *                  LOCALITY, REGION, PCODE, CTRY
283     */
284    public void setAddressFieldWork(String addrField, String value) {
285        workAddr.put(addrField, value);
286    }
287
288
289    /**
290     * Set home phone number
291     *
292     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
293     * @param phoneNum  phone number
294     */
295    public void setPhoneHome(String phoneType, String phoneNum) {
296        homePhones.put(phoneType, phoneNum);
297    }
298
299    /**
300     * Get home phone number
301     *
302     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
303     */
304    public String getPhoneHome(String phoneType) {
305        return homePhones.get(phoneType);
306    }
307
308    /**
309     * Set work phone number
310     *
311     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
312     * @param phoneNum  phone number
313     */
314    public void setPhoneWork(String phoneType, String phoneNum) {
315        workPhones.put(phoneType, phoneNum);
316    }
317
318    /**
319     * Get work phone number
320     *
321     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
322     */
323    public String getPhoneWork(String phoneType) {
324        return workPhones.get(phoneType);
325    }
326
327    /**
328     * Set the avatar for the VCard by specifying the url to the image.
329     *
330     * @param avatarURL the url to the image(png,jpeg,gif,bmp)
331     */
332    public void setAvatar(URL avatarURL) {
333        byte[] bytes = new byte[0];
334        try {
335            bytes = getBytes(avatarURL);
336        }
337        catch (IOException e) {
338            LOGGER.log(Level.SEVERE, "Error getting bytes from URL: " + avatarURL, e);
339        }
340
341        setAvatar(bytes);
342    }
343
344    /**
345     * Removes the avatar from the vCard
346     *
347     *  This is done by setting the PHOTO value to the empty string as defined in XEP-0153
348     */
349    public void removeAvatar() {
350        // Remove avatar (if any)
351        photoBinval = null;
352        photoMimeType = null;
353    }
354
355    /**
356     * Specify the bytes of the JPEG for the avatar to use.
357     * If bytes is null, then the avatar will be removed.
358     * 'image/jpeg' will be used as MIME type.
359     *
360     * @param bytes the bytes of the avatar, or null to remove the avatar data
361     */
362    public void setAvatar(byte[] bytes) {
363        setAvatar(bytes, DEFAULT_MIME_TYPE);
364    }
365
366    /**
367     * Specify the bytes for the avatar to use as well as the mime type.
368     *
369     * @param bytes the bytes of the avatar.
370     * @param mimeType the mime type of the avatar.
371     */
372    public void setAvatar(byte[] bytes, String mimeType) {
373        // If bytes is null, remove the avatar
374        if (bytes == null) {
375            removeAvatar();
376            return;
377        }
378
379        // Otherwise, add to mappings.
380        String encodedImage = Base64.encodeToString(bytes);
381
382        setAvatar(encodedImage, mimeType);
383    }
384
385    /**
386     * Specify the Avatar used for this vCard.
387     *
388     * @param encodedImage the Base64 encoded image as String
389     * @param mimeType the MIME type of the image
390     */
391    public void setAvatar(String encodedImage, String mimeType) {
392        photoBinval = encodedImage;
393        photoMimeType = mimeType;
394    }
395
396    /**
397     * Set the encoded avatar string. This is used by the provider.
398     *
399     * @param encodedAvatar the encoded avatar string.
400     * @deprecated Use {@link #setAvatar(String, String)} instead.
401     */
402    @Deprecated
403    public void setEncodedImage(String encodedAvatar) {
404        setAvatar(encodedAvatar, DEFAULT_MIME_TYPE);
405    }
406    
407    /**
408     * Return the byte representation of the avatar(if one exists), otherwise returns null if
409     * no avatar could be found.
410     * <b>Example 1</b>
411     * <pre>
412     * // Load Avatar from VCard
413     * byte[] avatarBytes = vCard.getAvatar();
414     * <p/>
415     * // To create an ImageIcon for Swing applications
416     * ImageIcon icon = new ImageIcon(avatar);
417     * <p/>
418     * // To create just an image object from the bytes
419     * ByteArrayInputStream bais = new ByteArrayInputStream(avatar);
420     * try {
421     *   Image image = ImageIO.read(bais);
422     *  }
423     *  catch (IOException e) {
424     *    e.printStackTrace();
425     * }
426     * </pre>
427     *
428     * @return byte representation of avatar.
429     */
430    public byte[] getAvatar() {
431        if (photoBinval == null) {
432            return null;
433        }
434        return Base64.decode(photoBinval);
435    }
436
437    /**
438     * Returns the MIME Type of the avatar or null if none is set
439     *
440     * @return the MIME Type of the avatar or null
441     */
442    public String getAvatarMimeType() {
443        return photoMimeType;
444    }
445
446    /**
447     * Common code for getting the bytes of a url.
448     *
449     * @param url the url to read.
450     */
451    public static byte[] getBytes(URL url) throws IOException {
452        final String path = url.getPath();
453        final File file = new File(path);
454        if (file.exists()) {
455            return getFileBytes(file);
456        }
457
458        return null;
459    }
460
461    private static byte[] getFileBytes(File file) throws IOException {
462        BufferedInputStream bis = null;
463        try {
464            bis = new BufferedInputStream(new FileInputStream(file));
465            int bytes = (int) file.length();
466            byte[] buffer = new byte[bytes];
467            int readBytes = bis.read(buffer);
468            if (readBytes != buffer.length) {
469                throw new IOException("Entire file not read");
470            }
471            return buffer;
472        }
473        finally {
474            if (bis != null) {
475                bis.close();
476            }
477        }
478    }
479
480    /**
481     * Returns the SHA-1 Hash of the Avatar image.
482     *
483     * @return the SHA-1 Hash of the Avatar image.
484     */
485    public String getAvatarHash() {
486        byte[] bytes = getAvatar();
487        if (bytes == null) {
488            return null;
489        }
490
491        MessageDigest digest;
492        try {
493            digest = MessageDigest.getInstance("SHA-1");
494        }
495        catch (NoSuchAlgorithmException e) {
496            LOGGER.log(Level.SEVERE, "Failed to get message digest", e);
497            return null;
498        }
499
500        digest.update(bytes);
501        return StringUtils.encodeHex(digest.digest());
502    }
503
504    private void updateFN() {
505        StringBuilder sb = new StringBuilder();
506        if (firstName != null) {
507            sb.append(StringUtils.escapeForXML(firstName)).append(' ');
508        }
509        if (middleName != null) {
510            sb.append(StringUtils.escapeForXML(middleName)).append(' ');
511        }
512        if (lastName != null) {
513            sb.append(StringUtils.escapeForXML(lastName));
514        }
515        setField("FN", sb.toString());
516    }
517
518    /**
519     * Save this vCard for the user connected by 'connection'. XMPPConnection should be authenticated
520     * and not anonymous.
521     *
522     * @param connection the XMPPConnection to use.
523     * @throws XMPPErrorException thrown if there was an issue setting the VCard in the server.
524     * @throws NoResponseException if there was no response from the server.
525     * @throws NotConnectedException 
526     * @deprecated use {@link VCardManager#saveVCard(VCard)} instead.
527     */
528    @Deprecated
529    public void save(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException {
530        VCardManager.getInstanceFor(connection).saveVCard(this);
531    }
532
533    /**
534     * Load VCard information for a connected user. XMPPConnection should be authenticated
535     * and not anonymous.
536     * @throws XMPPErrorException 
537     * @throws NoResponseException 
538     * @throws NotConnectedException 
539     * @deprecated use {@link VCardManager#loadVCard()} instead.
540     */
541    @Deprecated
542    public void load(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException  {
543        load(connection, null);
544    }
545
546    /**
547     * Load VCard information for a given user. XMPPConnection should be authenticated and not anonymous.
548     * @throws XMPPErrorException 
549     * @throws NoResponseException if there was no response from the server.
550     * @throws NotConnectedException 
551     * @deprecated use {@link VCardManager#loadVCard(String)} instead.
552     */
553    @Deprecated
554    public void load(XMPPConnection connection, String user) throws NoResponseException, XMPPErrorException, NotConnectedException {
555        VCard result = VCardManager.getInstanceFor(connection).loadVCard(user);
556        copyFieldsFrom(result);
557    }
558
559    @Override
560    protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
561        if (!hasContent()) {
562            xml.setEmptyElement();
563            return xml;
564        }
565        xml.rightAngleBracket();
566        if (hasNameField()) {
567            xml.openElement("N");
568            xml.optElement("FAMILY", lastName);
569            xml.optElement("GIVEN", firstName);
570            xml.optElement("MIDDLE", middleName);
571            xml.closeElement("N");
572        }
573        if (hasOrganizationFields()) {
574            xml.openElement("ORG");
575            xml.optElement("ORGNAME", organization);
576            xml.optElement("ORGUNIT", organizationUnit);
577            xml.closeElement("ORG");
578        }
579        for (Entry<String, String> entry : otherSimpleFields.entrySet()) {
580            xml.optElement(entry.getKey(), entry.getValue());
581        }
582        for (Entry<String, String> entry : otherUnescapableFields.entrySet()) {
583            final String value = entry.getValue();
584            if (value == null) {
585                continue;
586            }
587            xml.openElement(entry.getKey());
588            xml.append(value);
589            xml.closeElement(entry.getKey());
590        }
591        if (photoBinval != null) {
592            xml.openElement("PHOTO");
593            xml.escapedElement("BINVAL", photoBinval);
594            xml.element("TYPE", photoMimeType);
595            xml.closeElement("PHOTO");
596        }
597        if (emailWork != null) {
598            xml.openElement("EMAIL");
599            xml.emptyElement("WORK");
600            xml.emptyElement("INTERNET");
601            xml.emptyElement("PREF");
602            xml.element("USERID", emailWork);
603            xml.closeElement("EMAIL");
604        }
605        if (emailHome != null) {
606            xml.openElement("EMAIL");
607            xml.emptyElement("HOME");
608            xml.emptyElement("INTERNET");
609            xml.emptyElement("PREF");
610            xml.element("USERID", emailHome);
611            xml.closeElement("EMAIL");
612        }
613        for (Entry<String, String> phone : workPhones.entrySet()) {
614            final String number = phone.getValue();
615            if (number == null) {
616                continue;
617            }
618            xml.openElement("TEL");
619            xml.emptyElement("WORK");
620            xml.emptyElement(phone.getKey());
621            xml.element("NUMBER", number);
622            xml.closeElement("TEL");
623        }
624        for (Entry<String, String> phone : homePhones.entrySet()) {
625            final String number = phone.getValue();
626            if (number == null) {
627                continue;
628            }
629            xml.openElement("TEL");
630            xml.emptyElement("HOME");
631            xml.emptyElement(phone.getKey());
632            xml.element("NUMBER", number);
633            xml.closeElement("TEL");
634        }
635        if (!workAddr.isEmpty()) {
636            xml.openElement("ADR");
637            xml.emptyElement("WORK");
638            for (Entry<String, String> entry : workAddr.entrySet()) {
639                final String value = entry.getValue();
640                if (value == null) {
641                    continue;
642                }
643                xml.element(entry.getKey(), value);
644            }
645            xml.closeElement("ADR");
646        }
647        if (!homeAddr.isEmpty()) {
648            xml.openElement("ADR");
649            xml.emptyElement("HOME");
650            for (Entry<String, String> entry : homeAddr.entrySet()) {
651                final String value = entry.getValue();
652                if (value == null) {
653                    continue;
654                }
655                xml.element(entry.getKey(), value);
656            }
657            xml.closeElement("ADR");
658        }
659        return xml;
660    }
661
662    private void copyFieldsFrom(VCard from) {
663        Field[] fields = VCard.class.getDeclaredFields();
664        for (Field field : fields) {
665            if (field.getDeclaringClass() == VCard.class &&
666                    !Modifier.isFinal(field.getModifiers())) {
667                try {
668                    field.setAccessible(true);
669                    field.set(this, field.get(from));
670                }
671                catch (IllegalAccessException e) {
672                    throw new RuntimeException("This cannot happen:" + field, e);
673                }
674            }
675        }
676    }
677
678    private boolean hasContent() {
679        //noinspection OverlyComplexBooleanExpression
680        return hasNameField()
681                || hasOrganizationFields()
682                || emailHome != null
683                || emailWork != null
684                || otherSimpleFields.size() > 0
685                || otherUnescapableFields.size() > 0
686                || homeAddr.size() > 0
687                || homePhones.size() > 0
688                || workAddr.size() > 0
689                || workPhones.size() > 0
690                || photoBinval != null
691                ;
692    }
693
694    private boolean hasNameField() {
695        return firstName != null || lastName != null || middleName != null;
696    }
697
698    private boolean hasOrganizationFields() {
699        return organization != null || organizationUnit != null;
700    }
701
702    // Used in tests:
703
704    public boolean equals(Object o) {
705        if (this == o) return true;
706        if (o == null || getClass() != o.getClass()) return false;
707
708        final VCard vCard = (VCard) o;
709
710        if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) {
711            return false;
712        }
713        if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) {
714            return false;
715        }
716        if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) {
717            return false;
718        }
719        if (!homeAddr.equals(vCard.homeAddr)) {
720            return false;
721        }
722        if (!homePhones.equals(vCard.homePhones)) {
723            return false;
724        }
725        if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) {
726            return false;
727        }
728        if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) {
729            return false;
730        }
731        if (organization != null ?
732                !organization.equals(vCard.organization) : vCard.organization != null) {
733            return false;
734        }
735        if (organizationUnit != null ?
736                !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) {
737            return false;
738        }
739        if (!otherSimpleFields.equals(vCard.otherSimpleFields)) {
740            return false;
741        }
742        if (!workAddr.equals(vCard.workAddr)) {
743            return false;
744        }
745        if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) {
746            return false;
747        }
748
749        return workPhones.equals(vCard.workPhones);
750    }
751
752    public int hashCode() {
753        int result;
754        result = homePhones.hashCode();
755        result = 29 * result + workPhones.hashCode();
756        result = 29 * result + homeAddr.hashCode();
757        result = 29 * result + workAddr.hashCode();
758        result = 29 * result + (firstName != null ? firstName.hashCode() : 0);
759        result = 29 * result + (lastName != null ? lastName.hashCode() : 0);
760        result = 29 * result + (middleName != null ? middleName.hashCode() : 0);
761        result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0);
762        result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0);
763        result = 29 * result + (organization != null ? organization.hashCode() : 0);
764        result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0);
765        result = 29 * result + otherSimpleFields.hashCode();
766        result = 29 * result + (photoBinval != null ? photoBinval.hashCode() : 0);
767        return result;
768    }
769
770}
771