1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smack;
22
23import org.jivesoftware.smack.filter.AndFilter;
24import org.jivesoftware.smack.filter.PacketFilter;
25import org.jivesoftware.smack.filter.PacketIDFilter;
26import org.jivesoftware.smack.filter.PacketTypeFilter;
27import org.jivesoftware.smack.packet.IQ;
28import org.jivesoftware.smack.packet.Registration;
29import org.jivesoftware.smack.util.StringUtils;
30
31import java.util.Collection;
32import java.util.Collections;
33import java.util.HashMap;
34import java.util.HashSet;
35import java.util.List;
36import java.util.Map;
37import java.util.Set;
38
39/**
40 * Allows creation and management of accounts on an XMPP server.
41 *
42 * @see Connection#getAccountManager()
43 * @author Matt Tucker
44 */
45public class AccountManager {
46
47    private Connection connection;
48    private Registration info = null;
49
50    /**
51     * Flag that indicates whether the server supports In-Band Registration.
52     * In-Band Registration may be advertised as a stream feature. If no stream feature
53     * was advertised from the server then try sending an IQ packet to discover if In-Band
54     * Registration is available.
55     */
56    private boolean accountCreationSupported = false;
57
58    /**
59     * Creates a new AccountManager instance.
60     *
61     * @param connection a connection to a XMPP server.
62     */
63    public AccountManager(Connection connection) {
64        this.connection = connection;
65    }
66
67    /**
68     * Sets whether the server supports In-Band Registration. In-Band Registration may be
69     * advertised as a stream feature. If no stream feature was advertised from the server
70     * then try sending an IQ packet to discover if In-Band Registration is available.
71     *
72     * @param accountCreationSupported true if the server supports In-Band Registration.
73     */
74    void setSupportsAccountCreation(boolean accountCreationSupported) {
75        this.accountCreationSupported = accountCreationSupported;
76    }
77
78    /**
79     * Returns true if the server supports creating new accounts. Many servers require
80     * that you not be currently authenticated when creating new accounts, so the safest
81     * behavior is to only create new accounts before having logged in to a server.
82     *
83     * @return true if the server support creating new accounts.
84     */
85    public boolean supportsAccountCreation() {
86        // Check if we already know that the server supports creating new accounts
87        if (accountCreationSupported) {
88            return true;
89        }
90        // No information is known yet (e.g. no stream feature was received from the server
91        // indicating that it supports creating new accounts) so send an IQ packet as a way
92        // to discover if this feature is supported
93        try {
94            if (info == null) {
95                getRegistrationInfo();
96                accountCreationSupported = info.getType() != IQ.Type.ERROR;
97            }
98            return accountCreationSupported;
99        }
100        catch (XMPPException xe) {
101            return false;
102        }
103    }
104
105    /**
106     * Returns an unmodifiable collection of the names of the required account attributes.
107     * All attributes must be set when creating new accounts. The standard set of possible
108     * attributes are as follows: <ul>
109     *      <li>name -- the user's name.
110     *      <li>first -- the user's first name.
111     *      <li>last -- the user's last name.
112     *      <li>email -- the user's email address.
113     *      <li>city -- the user's city.
114     *      <li>state -- the user's state.
115     *      <li>zip -- the user's ZIP code.
116     *      <li>phone -- the user's phone number.
117     *      <li>url -- the user's website.
118     *      <li>date -- the date the registration took place.
119     *      <li>misc -- other miscellaneous information to associate with the account.
120     *      <li>text -- textual information to associate with the account.
121     *      <li>remove -- empty flag to remove account.
122     * </ul><p>
123     *
124     * Typically, servers require no attributes when creating new accounts, or just
125     * the user's email address.
126     *
127     * @return the required account attributes.
128     */
129    public Collection<String> getAccountAttributes() {
130        try {
131            if (info == null) {
132                getRegistrationInfo();
133            }
134            Map<String, String> attributes = info.getAttributes();
135            if (attributes != null) {
136                return Collections.unmodifiableSet(attributes.keySet());
137            }
138        }
139        catch (XMPPException xe) {
140            xe.printStackTrace();
141        }
142        return Collections.emptySet();
143    }
144
145    /**
146     * Returns the value of a given account attribute or <tt>null</tt> if the account
147     * attribute wasn't found.
148     *
149     * @param name the name of the account attribute to return its value.
150     * @return the value of the account attribute or <tt>null</tt> if an account
151     * attribute wasn't found for the requested name.
152     */
153    public String getAccountAttribute(String name) {
154        try {
155            if (info == null) {
156                getRegistrationInfo();
157            }
158            return info.getAttributes().get(name);
159        }
160        catch (XMPPException xe) {
161            xe.printStackTrace();
162        }
163        return null;
164    }
165
166    /**
167     * Returns the instructions for creating a new account, or <tt>null</tt> if there
168     * are no instructions. If present, instructions should be displayed to the end-user
169     * that will complete the registration process.
170     *
171     * @return the account creation instructions, or <tt>null</tt> if there are none.
172     */
173    public String getAccountInstructions() {
174        try {
175            if (info == null) {
176                getRegistrationInfo();
177            }
178            return info.getInstructions();
179        }
180        catch (XMPPException xe) {
181            return null;
182        }
183    }
184
185    /**
186     * Creates a new account using the specified username and password. The server may
187     * require a number of extra account attributes such as an email address and phone
188     * number. In that case, Smack will attempt to automatically set all required
189     * attributes with blank values, which may or may not be accepted by the server.
190     * Therefore, it's recommended to check the required account attributes and to let
191     * the end-user populate them with real values instead.
192     *
193     * @param username the username.
194     * @param password the password.
195     * @throws XMPPException if an error occurs creating the account.
196     */
197    public void createAccount(String username, String password) throws XMPPException {
198        if (!supportsAccountCreation()) {
199            throw new XMPPException("Server does not support account creation.");
200        }
201        // Create a map for all the required attributes, but give them blank values.
202        Map<String, String> attributes = new HashMap<String, String>();
203        for (String attributeName : getAccountAttributes()) {
204            attributes.put(attributeName, "");
205        }
206        createAccount(username, password, attributes);
207    }
208
209    /**
210     * Creates a new account using the specified username, password and account attributes.
211     * The attributes Map must contain only String name/value pairs and must also have values
212     * for all required attributes.
213     *
214     * @param username the username.
215     * @param password the password.
216     * @param attributes the account attributes.
217     * @throws XMPPException if an error occurs creating the account.
218     * @see #getAccountAttributes()
219     */
220    public void createAccount(String username, String password, Map<String, String> attributes)
221            throws XMPPException
222    {
223        if (!supportsAccountCreation()) {
224            throw new XMPPException("Server does not support account creation.");
225        }
226        Registration reg = new Registration();
227        reg.setType(IQ.Type.SET);
228        reg.setTo(connection.getServiceName());
229        attributes.put("username",username);
230        attributes.put("password",password);
231        reg.setAttributes(attributes);
232        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
233                new PacketTypeFilter(IQ.class));
234        PacketCollector collector = connection.createPacketCollector(filter);
235        connection.sendPacket(reg);
236        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
237        // Stop queuing results
238        collector.cancel();
239        if (result == null) {
240            throw new XMPPException("No response from server.");
241        }
242        else if (result.getType() == IQ.Type.ERROR) {
243            throw new XMPPException(result.getError());
244        }
245    }
246
247    /**
248     * Changes the password of the currently logged-in account. This operation can only
249     * be performed after a successful login operation has been completed. Not all servers
250     * support changing passwords; an XMPPException will be thrown when that is the case.
251     *
252     * @throws IllegalStateException if not currently logged-in to the server.
253     * @throws XMPPException if an error occurs when changing the password.
254     */
255    public void changePassword(String newPassword) throws XMPPException {
256        Registration reg = new Registration();
257        reg.setType(IQ.Type.SET);
258        reg.setTo(connection.getServiceName());
259        Map<String, String> map = new HashMap<String, String>();
260        map.put("username",StringUtils.parseName(connection.getUser()));
261        map.put("password",newPassword);
262        reg.setAttributes(map);
263        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
264                new PacketTypeFilter(IQ.class));
265        PacketCollector collector = connection.createPacketCollector(filter);
266        connection.sendPacket(reg);
267        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
268        // Stop queuing results
269        collector.cancel();
270        if (result == null) {
271            throw new XMPPException("No response from server.");
272        }
273        else if (result.getType() == IQ.Type.ERROR) {
274            throw new XMPPException(result.getError());
275        }
276    }
277
278    /**
279     * Deletes the currently logged-in account from the server. This operation can only
280     * be performed after a successful login operation has been completed. Not all servers
281     * support deleting accounts; an XMPPException will be thrown when that is the case.
282     *
283     * @throws IllegalStateException if not currently logged-in to the server.
284     * @throws XMPPException if an error occurs when deleting the account.
285     */
286    public void deleteAccount() throws XMPPException {
287        if (!connection.isAuthenticated()) {
288            throw new IllegalStateException("Must be logged in to delete a account.");
289        }
290        Registration reg = new Registration();
291        reg.setType(IQ.Type.SET);
292        reg.setTo(connection.getServiceName());
293        Map<String, String> attributes = new HashMap<String, String>();
294        // To delete an account, we add a single attribute, "remove", that is blank.
295        attributes.put("remove", "");
296        reg.setAttributes(attributes);
297        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
298                new PacketTypeFilter(IQ.class));
299        PacketCollector collector = connection.createPacketCollector(filter);
300        connection.sendPacket(reg);
301        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
302        // Stop queuing results
303        collector.cancel();
304        if (result == null) {
305            throw new XMPPException("No response from server.");
306        }
307        else if (result.getType() == IQ.Type.ERROR) {
308            throw new XMPPException(result.getError());
309        }
310    }
311
312    /**
313     * Gets the account registration info from the server.
314     *
315     * @throws XMPPException if an error occurs.
316     */
317    private synchronized void getRegistrationInfo() throws XMPPException {
318        Registration reg = new Registration();
319        reg.setTo(connection.getServiceName());
320        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
321                new PacketTypeFilter(IQ.class));
322        PacketCollector collector = connection.createPacketCollector(filter);
323        connection.sendPacket(reg);
324        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
325        // Stop queuing results
326        collector.cancel();
327        if (result == null) {
328            throw new XMPPException("No response from server.");
329        }
330        else if (result.getType() == IQ.Type.ERROR) {
331            throw new XMPPException(result.getError());
332        }
333        else {
334            info = (Registration)result;
335        }
336    }
337}