001/**
002 *
003 * Copyright 2013 the original author or authors
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 */
017package org.jivesoftware.smack;
018
019import java.io.File;
020import java.io.FileFilter;
021import java.io.IOException;
022import java.io.StringReader;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.List;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.jivesoftware.smack.packet.RosterPacket;
030import org.jivesoftware.smack.packet.RosterPacket.Item;
031import org.jivesoftware.smack.util.Base32Encoder;
032import org.jivesoftware.smack.util.FileUtils;
033import org.jivesoftware.smack.util.XmlStringBuilder;
034import org.xmlpull.v1.XmlPullParserFactory;
035import org.xmlpull.v1.XmlPullParser;
036import org.xmlpull.v1.XmlPullParserException;
037
038/**
039 * Stores roster entries as specified by RFC 6121 for roster versioning
040 * in a set of files.
041 *
042 * @author Lars Noschinski
043 * @author Fabian Schuetz
044 */
045public class DirectoryRosterStore implements RosterStore {
046
047    private final File fileDir;
048
049    private static final String ENTRY_PREFIX = "entry-";
050    private static final String VERSION_FILE_NAME = "__version__";
051    private static final String STORE_ID = "DEFAULT_ROSTER_STORE";
052    private static final Logger LOGGER = Logger.getLogger(DirectoryRosterStore.class.getName());
053
054    private static final FileFilter rosterDirFilter = new FileFilter() {
055
056        @Override
057        public boolean accept(File file) {
058            String name = file.getName();
059            return name.startsWith(ENTRY_PREFIX);
060        }
061
062    };
063
064    /**
065     * @param baseDir
066     *            will be the directory where all roster entries are stored. One
067     *            file for each entry, such that file.name = entry.username.
068     *            There is also one special file '__version__' that contains the
069     *            current version string.
070     */
071    private DirectoryRosterStore(final File baseDir) {
072        this.fileDir = baseDir;
073    }
074
075    /**
076     * Creates a new roster store on disk
077     *
078     * @param baseDir
079     *            The directory to create the store in. The directory should
080     *            be empty
081     * @return A {@link DirectoryRosterStore} instance if successful,
082     *         <code>null</code> else.
083     */
084    public static DirectoryRosterStore init(final File baseDir) {
085        DirectoryRosterStore store = new DirectoryRosterStore(baseDir);
086        if (store.setRosterVersion("")) {
087            return store;
088        }
089        else {
090            return null;
091        }
092    }
093
094    /**
095     * Opens a roster store
096     * @param baseDir
097     *            The directory containing the roster store.
098     * @return A {@link DirectoryRosterStore} instance if successful,
099     *         <code>null</code> else.
100     */
101    public static DirectoryRosterStore open(final File baseDir) {
102        DirectoryRosterStore store = new DirectoryRosterStore(baseDir);
103        String s = FileUtils.readFile(store.getVersionFile());
104        if (s != null && s.startsWith(STORE_ID + "\n")) {
105            return store;
106        }
107        else {
108            return null;
109        }
110    }
111
112    private File getVersionFile() {
113        return new File(fileDir, VERSION_FILE_NAME);
114    }
115
116    @Override
117    public List<Item> getEntries() {
118        List<Item> entries = new ArrayList<RosterPacket.Item>();
119
120        for (File file : fileDir.listFiles(rosterDirFilter)) {
121            Item entry = readEntry(file);
122            if (entry == null) {
123                log("Roster store file '" + file + "' is invalid.");
124            }
125            else {
126                entries.add(entry);
127            }
128        }
129
130        return entries;
131    }
132
133    @Override
134    public Item getEntry(String bareJid) {
135        return readEntry(getBareJidFile(bareJid));
136    }
137
138    @Override
139    public String getRosterVersion() {
140        String s = FileUtils.readFile(getVersionFile());
141        if (s == null) {
142            return null;
143        }
144        String[] lines = s.split("\n", 2);
145        if (lines.length < 2) {
146            return null;
147        }
148        return lines[1];
149    }
150
151    private boolean setRosterVersion(String version) {
152        return FileUtils.writeFile(getVersionFile(), STORE_ID + "\n" + version);
153    }
154
155    @Override
156    public boolean addEntry(Item item, String version) {
157        return addEntryRaw(item) && setRosterVersion(version);
158    }
159
160    @Override
161    public boolean removeEntry(String bareJid, String version) {
162        return getBareJidFile(bareJid).delete() && setRosterVersion(version);
163    }
164
165    @Override
166    public boolean resetEntries(Collection<Item> items, String version) {
167        for (File file : fileDir.listFiles(rosterDirFilter)) {
168            file.delete();
169        }
170        for (Item item : items) {
171            if (!addEntryRaw(item)) {
172                return false;
173            }
174        }
175        return setRosterVersion(version);
176    }
177
178    private Item readEntry(File file) {
179        String s = FileUtils.readFile(file);
180        if (s == null) {
181            return null;
182        }
183
184        String parserName;
185        String user = null;
186        String name = null;
187        String type = null;
188        String status = null;
189
190        List<String> groupNames = new ArrayList<String>();
191
192        try {
193            XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
194            parser.setInput(new StringReader(s));
195
196            boolean done = false;
197            while (!done) {
198                int eventType = parser.next();
199                parserName = parser.getName();
200                if (eventType == XmlPullParser.START_TAG) {
201                    if (parserName.equals("item")) {
202                        user = name = type = status = null;
203                    }
204                    else if (parserName.equals("user")) {
205                        parser.next();
206                        user = parser.getText();
207                    }
208                    else if (parserName.equals("name")) {
209                        parser.next();
210                        name = parser.getText();
211                    }
212                    else if (parserName.equals("type")) {
213                        parser.next();
214                        type = parser.getText();
215                    }
216                    else if (parserName.equals("status")) {
217                        parser.next();
218                        status = parser.getText();
219                    }
220                    else if (parserName.equals("group")) {
221                        parser.next();
222                        parser.next();
223                        String group = parser.getText();
224                        if (group != null) {
225                            groupNames.add(group);
226                        }
227                        else {
228                            log("Invalid group entry in store entry file "
229                                    + file);
230                        }
231                    }
232                }
233                else if (eventType == XmlPullParser.END_TAG) {
234                    if (parserName.equals("item")) {
235                        done = true;
236                    }
237                }
238            }
239        }
240        catch (IOException e) {
241            LOGGER.log(Level.SEVERE, "readEntry()", e);
242            return null;
243        }
244        catch (XmlPullParserException e) {
245            log("Invalid group entry in store entry file "
246                    + file);
247            LOGGER.log(Level.SEVERE, "readEntry()", e);
248            return null;
249        }
250
251        if (user == null) {
252            return null;
253        }
254        RosterPacket.Item item = new RosterPacket.Item(user, name);
255        for (String groupName : groupNames) {
256            item.addGroupName(groupName);
257        }
258
259        if (type != null) {
260            try {
261                item.setItemType(RosterPacket.ItemType.valueOf(type));
262            }
263            catch (IllegalArgumentException e) {
264                log("Invalid type in store entry file " + file);
265                return null;
266            }
267            if (status != null) {
268                RosterPacket.ItemStatus itemStatus = RosterPacket.ItemStatus
269                        .fromString(status);
270                if (itemStatus == null) {
271                    log("Invalid status in store entry file " + file);
272                    return null;
273                }
274                item.setItemStatus(itemStatus);
275            }
276        }
277
278        return item;
279    }
280
281
282    private boolean addEntryRaw (Item item) {
283        XmlStringBuilder xml = new XmlStringBuilder();
284        xml.openElement("item");
285        xml.element("user", item.getUser());
286        xml.optElement("name", item.getName());
287        xml.optElement("type", item.getItemType());
288        xml.optElement("status", item.getItemStatus());
289        for (String groupName : item.getGroupNames()) {
290            xml.openElement("group");
291            xml.element("groupName", groupName);
292            xml.closeElement("group");
293        }
294        xml.closeElement("item");
295
296        return FileUtils.writeFile(getBareJidFile(item.getUser()), xml.toString());
297    }
298
299
300    private File getBareJidFile(String bareJid) {
301        String encodedJid = Base32Encoder.getInstance().encode(bareJid);
302        return new File(fileDir, ENTRY_PREFIX + encodedJid);
303    }
304
305    private void log(String error) {
306        System.err.println(error);
307    }
308}