xmppconnection vcard load

//==================================

/**

 

  • $RCSfile$

 

  • $Revision$

 

  • $Date: 2006-01-18 00:29:16 +0300 (Wed, 18 Jan 2006) $

*

 

  • Copyright 2003-2004 Jive Software.

*

 

  • All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");

 

  • you may not use this file except in compliance with the License.

 

  • You may obtain a copy of the License at

*

 

*

 

  • Unless required by applicable law or agreed to in writing, software

 

  • distributed under the License is distributed on an "AS IS" BASIS,

 

  • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 

  • See the License for the specific language governing permissions and

 

  • limitations under the License.

*/

 

package org.jivesoftware.smackx.packet;

 

import org.jivesoftware.smack.PacketCollector;

import org.jivesoftware.smack.SmackConfiguration;

import org.jivesoftware.smack.XMPPConnection;

import org.jivesoftware.smack.XMPPException;

import org.jivesoftware.smack.filter.PacketIDFilter;

import org.jivesoftware.smack.packet.IQ;

import org.jivesoftware.smack.packet.Packet;

import org.jivesoftware.smack.packet.PacketExtension;

import org.jivesoftware.smack.packet.XMPPError;

import org.jivesoftware.smack.util.StringUtils;

 

import java.io.BufferedInputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.net.URL;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

 

/**

 

  • A VCard class for use with the

 

 

  • <p/>

 

  • You should refer to the

 

 

  • <p/>

 

  • Please note that this class is incomplete but it does provide the most commonly found

 

  • information in vCards. Also remember that VCard transfer is not a standard, and the protocol

 

  • may change or be replaced.<p>

 

  • <p/>

 

  • <b>Usage:</b>

 

  • <pre>

 

  • <p/>

 

  • // To save VCard:

 

  • <p/>

 

  • VCard vCard = new VCard();

 

  • vCard.setFirstName("kir");

 

  • vCard.setLastName("max");

 

  • vCard.setEmailHome("foo@fee.bar");

 

 

  • vCard.setOrganization("Jetbrains, s.r.o");

 

  • vCard.setNickName("KIR");

 

  • <p/>

 

  • vCard.setField("TITLE", "Mr");

 

  • vCard.setAddressFieldHome("STREET", "Some street");

 

  • vCard.setAddressFieldWork("CTRY", "US");

 

  • vCard.setPhoneWork("FAX", "3443233");

 

  • <p/>

 

  • vCard.save(connection);

 

  • <p/>

 

  • // To load VCard:

 

  • <p/>

 

  • VCard vCard = new VCard();

 

  • vCard.load(conn); // load own VCard

 

  • vCard.load(conn, "joe@foo.bar"); // load someone''s VCard

 

  • </pre>

*

 

*/

public class VCard extends IQ {

 

    /**

      

  • Phone types:

      

  • VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF?

     */

    private Map homePhones = new HashMap();

    private Map workPhones = new HashMap();

 

 

    /**

      

  • Address types:

      

  • POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?,

      

  • REGION?, PCODE?, CTRY?

     */

    private Map homeAddr = new HashMap();

    private Map workAddr = new HashMap();

 

    private String firstName;

    private String lastName;

    private String middleName;

 

    private String emailHome;

    private String emailWork;

 

    private String organization;

    private String organizationUnit;

 

    private String avatar;

 

    /**

      

  • Such as DESC ROLE GEO etc.. see JEP-0054

     */

    private Map otherSimpleFields = new HashMap();

 

    public VCard() {

    }

 

    /**

      

  • Set generic VCard field.

     *

      

  • @param field value of field. Possible values: FN, NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ,

      

  •              GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC.

     */

    public String getField(String field) {

        if ("FN".equals(field)) {

            return buildFullName();

        }

        return (String) otherSimpleFields.get(field);

    }

 

    private String buildFullName() {

        if (otherSimpleFields.containsKey("FN")) {

             return otherSimpleFields.get("FN").toString().trim();

        }

        else {

            StringBuffer sb = new StringBuffer();

            if (firstName != null) {

             &n bsp;  sb.append(firstName).append('' '');

            }

            if (middleName != null) {

             &n bsp;  sb.append(middleName).append('' '');

            }

            if (lastName != null) {

             &n bsp;  sb.append(lastName);

            }

            return sb.toString().trim();

        }

    }

 

    /**

      

  • Set generic VCard field.

     *

      

  • @param value value of field

      

  • @param field field to set. See {@link #getField(String)}

      

  • @see #getField(String)

     */

    public void setField(String field, String value) {

        otherSimpleFields.put(field, value);

    }

 

    public String getFirstName() {

        return firstName;

    }

 

    public void setFirstName(String firstName) {

        this.firstName = firstName;

    }

 

    public String getLastName() {

        return lastName;

    }

 

    public void setLastName(String lastName) {

        this.lastName = lastName;

    }

 

    public String getMiddleName() {

        return middleName;

    }

 

    /**

      

  • Returns the full name of the user, associated with this VCard.

     */

    public String getFullName() {

        return getField("FN");

    }

 

    public void setMiddleName(String middleName) {

        this.middleName = middleName;

    }

 

    public String getNickName() {

        return (String) otherSimpleFields.get("NICKNAME");

    }

 

    public void setNickName(String nickName) {

        otherSimpleFields.put("NICKNAME", nickName);

    }

 

    public String getEmailHome() {

        return emailHome;

    }

 

    public void setEmailHome(String email) {

        this.emailHome = email;

    }

 

    public String getEmailWork() {

        return emailWork;

    }

 

    public void setEmailWork(String emailWork) {

        this.emailWork = emailWork;

    }

 

    public String getJabberId() {

        return (String) otherSimpleFields.get("JABBERID");

    }

 

    public void setJabberId(String jabberId) {

        otherSimpleFields.put("JABBERID", jabberId);

    }

 

    public String getOrganization() {

        return organization;

    }

 

    public void setOrganization(String organization) {

        this.organization = organization;

    }

 

    public String getOrganizationUnit() {

        return organizationUnit;

    }

 

    public void setOrganizationUnit(String organizationUnit) {

        this.organizationUnit = organizationUnit;

    }

 

    /**

      

  • Get home address field

     *

      

  • @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,

      

  •              &n bsp;   LOCALITY, REGION, PCODE, CTRY

     */

    public String getAddressFieldHome(String addrField) {

        return (String) homeAddr.get(addrField);

    }

 

    /**

      

  • Set home address field

     *

      

  • @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,

      

  •              &n bsp;   LOCALITY, REGION, PCODE, CTRY

     */

    public void setAddressFieldHome(String addrField, String value) {

        homeAddr.put(addrField, value);

    }

 

    /**

      

  • Get work address field

     *

      

  • @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,

      

  •              &n bsp;   LOCALITY, REGION, PCODE, CTRY

     */

    public String getAddressFieldWork(String addrField) {

        return (String) workAddr.get(addrField);

    }

 

    /**

      

  • Set work address field

     *

      

  • @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,

      

  •              &n bsp;   LOCALITY, REGION, PCODE, CTRY

     */

    public void setAddressFieldWork(String addrField, String value) {

        workAddr.put(addrField, value);

    }

 

 

    /**

      

  • Set home phone number

     *

      

  • @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF

      

  • @param phoneNum  phone number

     */

    public void setPhoneHome(String phoneType, String phoneNum) {

        homePhones.put(phoneType, phoneNum);

    }

 

    /**

      

  • Get home phone number

     *

      

  • @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF

     */

    public String getPhoneHome(String phoneType) {

        return (String) homePhones.get(phoneType);

    }

 

    /**

      

  • Set work phone number

     *

      

  • @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF

      

  • @param phoneNum  phone number

     */

    public void setPhoneWork(String phoneType, String phoneNum) {

        workPhones.put(phoneType, phoneNum);

    }

 

    /**

      

  • Get work phone number

     *

      

  • @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF

     */

    public String getPhoneWork(String phoneType) {

        return (String) workPhones.get(phoneType);

    }

 

    /**

      

  • Set the avatar for the VCard by specifying the url to the image.

     *

      

  • @param avatarURL the url to the image(png,jpeg,gif,bmp)

     */

    public void setAvatar(URL avatarURL) {

        byte[] bytes = new byte[0];

        try {

            bytes = getBytes(avatarURL);

        }

        catch (IOException e) {

            e.printStackTrace();

        }

 

        String encodedImage = StringUtils.encodeBase64(bytes);

        avatar = encodedImage;

 

        setField("PHOTO", "");

    }

 

    /**

      

  • Specify the bytes for the avatar to use.

     *

      

  • @param bytes the bytes of the avatar.

     */

    public void setAvatar(byte[] bytes) {

        String encodedImage = StringUtils.encodeBase64(bytes);

        avatar = encodedImage;

 

        setField("PHOTO", "");

    }

 

    /**

      

  • Set the encoded avatar string. This is used by the provider.

     *

      

  • @param encodedAvatar the encoded avatar string.

     */

    public void setEncodedImage(String encodedAvatar) {

        //TODO Move VCard and VCardProvider into a vCard package.

        this.avatar = encodedAvatar;

    }

 

    /**

      

  • Return the byte representation of the avatar(if one exists), otherwise returns null if

      

  • no avatar could be found.

      

  • <b>Example 1</b>

      

  • <pre>

      

  • // Load Avatar from VCard

      

  • byte[] avatarBytes = vCard.getAvatar();

      

  • <p/>

      

  • // To create an ImageIcon for Swing applications

      

  • ImageIcon icon = new ImageIcon(avatar);

      

  • <p/>

      

  • // To create just an image object from the bytes

      

  • ByteArrayInputStream bais = new ByteArrayInputStream(avatar);

      

  • try {

      

  •   Image image = ImageIO.read(bais);

      

  • }

      

  • catch (IOException e) {

      

  •    e.printStackTrace();

      

  • }

      

  • </pre>

     *

      

  • @return byte representation of avatar.

     */

    public byte[] getAvatar() {

        if (avatar == null) {

            return null;

        }

        if (avatar != null) {

            return StringUtils.decodeBase64(avatar);

        }

        return null;

    }

 

    /**

      

  • Common code for getting the bytes of a url.

     *

      

  • @param url the url to read.

     */

    public static byte[] getBytes(URL url) throws IOException {

        final String path = url.getPath();

        final File file = new File(path);

        if (file.exists()) {

            return getFileBytes(file);

        }

 

        return null;

    }

 

    private static byte[] getFileBytes(File file) throws IOException {

        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));

        int bytes = (int) file.length();

        byte[] buffer = new byte[bytes];

        int readBytes = bis.read(buffer);

        bis.close();

        return buffer;

    }

 

    /**

      

  • Returns the SHA-1 Hash of the Avatar image.

     *

      

  • @return the SHA-1 Hash of the Avatar image.

     */

    public String getAvatarHash() {

        byte[] bytes = getAvatar();

        if (bytes == null) {

            return null;

        }

 

        MessageDigest digest = null;

        try {

            digest = MessageDigest.getInstance("SHA-1");

        }

        catch (NoSuchAlgorithmException e) {

            e.printStackTrace();

        }

 

        digest.update(bytes);

        return StringUtils.encodeHex(digest.digest());

    }

 

    /**

      

  • Save this vCard for the user connected by ''connection''. Connection should be authenticated

      

  • and not anonymous.<p>

      

  • <p/>

      

  • NOTE: the method is asynchronous and does not wait for the returned value.

      

  • @param connection the XMPPConnection to use.

      

  • @throws XMPPException thrown if there was an issue setting the VCard in the server.

     */

    public void save(XMPPConnection connection) throws XMPPException {

        checkAuthenticated(connection);

 

        setType(IQ.Type.SET);

        setFrom(connection.getUser());

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(getPacketID()));

        connection.sendPacket(this);

 

        Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

 

        // Cancel the collector.

        collector.cancel();

        if (response == null) {

            throw new XMPPException("No response from server on status set.");

        }

        if (response.getError() != null) {

            throw new XMPPException(response.getError());

        }

    }

 

    /**

      

  • Load VCard information for a connected user. Connection should be authenticated

      

  • and not anonymous.

     */

    public void load(XMPPConnection connection) throws XMPPException {

        checkAuthenticated(connection);

 

        setFrom(connection.getUser());

        doLoad(connection, connection.getUser());

    }

 

    /**

      

  • Load VCard information for a given user. Connection should be authenticated and not anonymous.

     */

    public void load(XMPPConnection connection, String user) throws XMPPException {

        checkAuthenticated(connection);

 

        setTo(user);

        doLoad(connection, user);

    }

 

    private void doLoad(XMPPConnection connection, String user) throws XMPPException {

        setType(Type.GET);

        PacketCollector collector = connection.createPacketCollector(

             &n bsp;  new PacketIDFilter(getPacketID()));

        connection.sendPacket(this);

 

        VCard result = null;

        try {

            result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

 

            if (result == null) {

             &n bsp;  throw new XMPPException(new XMPPError(408, "Timeout getting VCard information"));

            }

            if (result.getError() != null) {

             &n bsp;  throw new XMPPException(result.getError());

            }

        }

        catch (ClassCastException e) {

            System.out.println("No VCard for " + user);

        }

 

        copyFieldsFrom(result);

    }

 

    public String getChildElementXML() {

        StringBuffer sb = new StringBuffer();

        new VCardWriter(sb).write();

        return sb.toString();

    }

 

    private void copyFieldsFrom(VCard result) {

        if (result == null) result = new VCard();

 

        homeAddr = result.homeAddr;

        homePhones = result.homePhones;

 

        workAddr = result.workAddr;

        workPhones = result.workPhones;

 

        firstName = result.firstName;

        lastName = result.lastName;

        middleName = result.middleName;

 

        emailHome = result.emailHome;

        emailWork = result.emailWork;

 

        organization = result.organization;

        organizationUnit = result.organizationUnit;

 

        otherSimpleFields = result.otherSimpleFields;

        avatar = result.avatar;

 

        setType(result.getType());

 

        setError(result.getError());

        setFrom(result.getFrom());

        setTo(result.getTo());

        setPacketID(result.getPacketID());

        Iterator iterator = result.getExtensions();

        while (iterator.hasNext()) {

            addExtension((PacketExtension) iterator.next());

        }

 

        iterator = result.getPropertyNames();

        while(iterator.hasNext()) {

            String key = (String) iterator.next();

            setProperty(key, result.getProperty(key));

        }

    }

 

    private void checkAuthenticated(XMPPConnection connection) {

        if (connection == null) {

            new IllegalArgumentException("No connection was provided");

        }

        if (!connection.isAuthenticated()) {

            new IllegalArgumentException("Connection is not authenticated");

        }

        if (connection.isAnonymous()) {

            new IllegalArgumentException("Connection cannot be anonymous");

        }

    }

 

    private boolean hasContent() {

        //noinspection OverlyComplexBooleanExpression

        return hasNameField()

             &n bsp;  || hasOrganizationFields()

             &n bsp;  || emailHome != null

             &n bsp;  || emailWork != null

             &n bsp;  || otherSimpleFields.size() > 0

             &n bsp;  || homeAddr.size() > 0

             &n bsp;  || homePhones.size() > 0

             &n bsp;  || workAddr.size() > 0

             &n bsp;  || workPhones.size() > 0

             &n bsp;  ;

    }

 

    private boolean hasNameField() {

        return firstName != null || lastName != null || middleName != null || otherSimpleFields.containsKey("FN");

    }

 

    private boolean hasOrganizationFields() {

        return organization != null || organizationUnit != null;

    }

 

    // Used in tests:

 

    public boolean equals(Object o) {

        if (this == o) return true;

        if (o == null || getClass() != o.getClass()) return false;

 

        final VCard vCard = (VCard) o;

 

        if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) {

            return false;

        }

        if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) {

            return false;

        }

        if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) {

            return false;

        }

        if (!homeAddr.equals(vCard.homeAddr)) {

            return false;

        }

        if (!homePhones.equals(vCard.homePhones)) {

            return false;

        }

        if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) {

            return false;

        }

        if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) {

            return false;

        }

        if (organization != null ?

             &n bsp;  !organization.equals(vCard.organization) : vCard.organization != null) {

            return false;

        }

        if (organizationUnit != null ?

             &n bsp;  !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) {

            return false;

        }

        if (!otherSimpleFields.equals(vCard.otherSimpleFields)) {

            return false;

        }

        if (!workAddr.equals(vCard.workAddr)) {

            return false;

        }

        if (!workPhones.equals(vCard.workPhones)) {

            return false;

        }

 

        return true;

    }

 

    public int hashCode() {

        int result;

        result = homePhones.hashCode();

        result = 29 * result + workPhones.hashCode();

        result = 29 * result + homeAddr.hashCode();

        result = 29 * result + workAddr.hashCode();

        result = 29 * result + (firstName != null ? firstName.hashCode() : 0);

        result = 29 * result + (lastName != null ? lastName.hashCode() : 0);

        result = 29 * result + (middleName != null ? middleName.hashCode() : 0);

        result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0);

        result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0);

        result = 29 * result + (organization != null ? organization.hashCode() : 0);

        result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0);

        result = 29 * result + otherSimpleFields.hashCode();

        return result;

    }

 

    public String toString() {

        return getChildElementXML();

    }

 

    //==============================================================

 

    private class VCardWriter {

 

        private final StringBuffer sb;

 

        VCardWriter(StringBuffer sb) {

            this.sb = sb;

        }

 

        public void write() {

            appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() {

             &n bsp;  public void addTagContent() {

             &n bsp;      buildActualContent();

             &n bsp;  }

            });

        }

 

        private void buildActualContent() {

            if (hasNameField()) {

             &n bsp;  appendTag("FN", getFullName());

             &n bsp;  appendN();

            }

 

            appendOrganization();

            appendGenericFields();

 

            appendEmail(emailWork, "WORK");

            appendEmail(emailHome, "HOME");

 

            appendPhones(workPhones, "WORK");

            appendPhones(homePhones, "HOME");

 

            appendAddress(workAddr, "WORK");

            appendAddress(homeAddr, "HOME");

        }

 

        private void appendEmail(final String email, final String type) {

            if (email != null) {

             &n bsp;  appendTag("EMAIL", true, new ContentBuilder() {

             &n bsp;      public void addTagContent() {

             &n bsp;          appendEmptyTag(type);

             &n bsp;          appendEmptyTag("INTERNET");

             &n bsp;          appendEmptyTag("PREF");

             &n bsp;          appendTag("USERID", email);

             &n bsp;      }

             &n bsp;  });

            }

        }

 

        private void appendPhones(Map phones, final String code) {

            Iterator it = phones.entrySet().iterator();

            while (it.hasNext()) {

             &n bsp;  final Map.Entry entry = (Map.Entry) it.next();

             &n bsp;  appendTag("TEL", true, new ContentBuilder() {

             &n bsp;      public void addTagContent() {

             &n bsp;          appendEmptyTag(entry.getKey());

             &n bsp;          appendEmptyTag(code);

             &n bsp;          appendTag("NUMBER", (String) entry.getValue());

             &n bsp;      }

             &n bsp;  });

            }

        }

 

        private void appendAddress(final Map addr, final String code) {

            if (addr.size() > 0) {

             &n bsp;  appendTag("ADR", true, new ContentBuilder() {

             &n bsp;      public void addTagContent() {

             &n bsp;          appendEmptyTag(code);

 

             &n bsp;          Iterator it = addr.entrySet().iterator();

             &n bsp;          while (it.hasNext()) {

             &n bsp;            &nbs p; final Map.Entry entry = (Map.Entry) it.next();

             &n bsp;            &nbs p; appendTag((String) entry.getKey(), (String) entry.getValue());

             &n bsp;          }

             &n bsp;      }

             &n bsp;  });

            }

        }

 

        private void appendEmptyTag(Object tag) {

            sb.append(''<'').append(tag).append("/>");

        }

 

        private void appendGenericFields() {

            Iterator it = otherSimpleFields.entrySet().iterator();

            while (it.hasNext()) {

             &n bsp;  Map.Entry entry = (Map.Entry) it.next();

             &n bsp;  String tag = entry.getKey().toString();

             &n bsp;  if ("FN".equals(tag)) continue;

             &n bsp;  appendTag(tag, (String) entry.getValue());

            }

        }

 

        private void appendOrganization() {

            if (hasOrganizationFields()) {

             &n bsp;  appendTag("ORG", true, new ContentBuilder() {

             &n bsp;      public void addTagContent() {

             &n bsp;          appendTag("ORGNAME", organization);

             &n bsp;          appendTag("ORGUNIT", organizationUnit);

             &n bsp;      }

             &n bsp;  });

            }

        }

 

        private void appendN() {

            appendTag("N", true, new ContentBuilder() {

             &n bsp;  public void addTagContent() {

             &n bsp;      appendTag("FAMILY", lastName);

             &n bsp;      appendTag("GIVEN", firstName);

             &n bsp;      appendTag("MIDDLE", middleName);

             &n bsp;  }

            });

        }

 

        private void appendTag(String tag, String attr, String attrValue, boolean hasContent,

             &n bsp;  ContentBuilder builder) {

            sb.append(''<'').append(tag);

            if (attr != null) {

             &n bsp;  sb.append('' '').append(attr).append(''='').append(''/'''').append(attrValue).append(''/'''' );

            }

 

            if (hasContent) {

             &n bsp;  sb.append(''>'');

             &n bsp;  builder.addTagContent();

             &n bsp;  sb.append("</").append(tag).append(">/n");

            }

            else {

             &n bsp;  sb.append("/>/n");

            }

        }

 

        private void appendTag(String tag, boolean hasContent, ContentBuilder builder) {

            appendTag(tag, null, null, hasContent, builder);

        }

 

        private void appendTag(String tag, final String tagText) {

            if (tagText == null) return;

            final ContentBuilder contentBuilder = new ContentBuilder() {

             &n bsp;  public void addTagContent() {

             &n bsp;      sb.append(tagText.trim());

             &n bsp;  }

            };

            appendTag(tag, true, contentBuilder);

        }

 

    }

 

    //==============================================================

 

    private interface ContentBuilder {

 

        void addTagContent();

    }

 

    //==============================================================

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值