001/**
002 *
003 * Copyright 2013-2015 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.roster.rosterstore;
018
019import java.io.File;
020import java.io.FileFilter;
021import java.io.FileNotFoundException;
022import java.io.FileReader;
023import java.io.IOException;
024import java.io.Reader;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.List;
029import java.util.logging.Level;
030import java.util.logging.Logger;
031
032import org.jivesoftware.smack.roster.packet.RosterPacket.Item;
033import org.jivesoftware.smack.roster.provider.RosterPacketProvider;
034import org.jivesoftware.smack.util.FileUtils;
035import org.jivesoftware.smack.util.PacketParserUtils;
036import org.jivesoftware.smack.util.stringencoder.Base32;
037
038import org.jxmpp.jid.Jid;
039import org.xmlpull.v1.XmlPullParser;
040import org.xmlpull.v1.XmlPullParserException;
041
042/**
043 * Stores roster entries as specified by RFC 6121 for roster versioning
044 * in a set of files.
045 *
046 * @author Lars Noschinski
047 * @author Fabian Schuetz
048 * @author Florian Schmaus
049 */
050public final class DirectoryRosterStore implements RosterStore {
051
052    private final File fileDir;
053
054    private static final String ENTRY_PREFIX = "entry-";
055    private static final String VERSION_FILE_NAME = "__version__";
056    private static final String STORE_ID = "DEFAULT_ROSTER_STORE";
057    private static final Logger LOGGER = Logger.getLogger(DirectoryRosterStore.class.getName());
058
059    private static final FileFilter rosterDirFilter = new FileFilter() {
060
061        @Override
062        public boolean accept(File file) {
063            String name = file.getName();
064            return name.startsWith(ENTRY_PREFIX);
065        }
066
067    };
068
069    /**
070     * @param baseDir
071     *            will be the directory where all roster entries are stored. One
072     *            file for each entry, such that file.name = entry.username.
073     *            There is also one special file '__version__' that contains the
074     *            current version string.
075     */
076    private DirectoryRosterStore(final File baseDir) {
077        this.fileDir = baseDir;
078    }
079
080    /**
081     * Creates a new roster store on disk.
082     *
083     * @param baseDir
084     *            The directory to create the store in. The directory should
085     *            be empty
086     * @return A {@link DirectoryRosterStore} instance if successful,
087     *         <code>null</code> else.
088     */
089    public static DirectoryRosterStore init(final File baseDir) {
090        DirectoryRosterStore store = new DirectoryRosterStore(baseDir);
091        if (store.setRosterVersion("")) {
092            return store;
093        }
094        else {
095            return null;
096        }
097    }
098
099    /**
100     * Opens a roster store.
101     * @param baseDir
102     *            The directory containing the roster store.
103     * @return A {@link DirectoryRosterStore} instance if successful,
104     *         <code>null</code> else.
105     */
106    public static DirectoryRosterStore open(final File baseDir) {
107        DirectoryRosterStore store = new DirectoryRosterStore(baseDir);
108        String s = FileUtils.readFile(store.getVersionFile());
109        if (s != null && s.startsWith(STORE_ID + "\n")) {
110            return store;
111        }
112        else {
113            return null;
114        }
115    }
116
117    private File getVersionFile() {
118        return new File(fileDir, VERSION_FILE_NAME);
119    }
120
121    @Override
122    public List<Item> getEntries() {
123        List<Item> entries = new ArrayList<>();
124
125        for (File file : fileDir.listFiles(rosterDirFilter)) {
126            Item entry = readEntry(file);
127            if (entry == null) {
128                // Roster directory store corrupt. Abort and signal this by returning null.
129                return null;
130            }
131            entries.add(entry);
132        }
133
134        return entries;
135    }
136
137    @Override
138    public Item getEntry(Jid bareJid) {
139        return readEntry(getBareJidFile(bareJid));
140    }
141
142    @Override
143    public String getRosterVersion() {
144        String s = FileUtils.readFile(getVersionFile());
145        if (s == null) {
146            return null;
147        }
148        String[] lines = s.split("\n", 2);
149        if (lines.length < 2) {
150            return null;
151        }
152        return lines[1];
153    }
154
155    private boolean setRosterVersion(String version) {
156        return FileUtils.writeFile(getVersionFile(), STORE_ID + "\n" + version);
157    }
158
159    @Override
160    public boolean addEntry(Item item, String version) {
161        return addEntryRaw(item) && setRosterVersion(version);
162    }
163
164    @Override
165    public boolean removeEntry(Jid bareJid, String version) {
166        return getBareJidFile(bareJid).delete() && setRosterVersion(version);
167    }
168
169    @Override
170    public boolean resetEntries(Collection<Item> items, String version) {
171        for (File file : fileDir.listFiles(rosterDirFilter)) {
172            file.delete();
173        }
174        for (Item item : items) {
175            if (!addEntryRaw(item)) {
176                return false;
177            }
178        }
179        return setRosterVersion(version);
180    }
181
182
183    @Override
184    public void resetStore() {
185        resetEntries(Collections.<Item>emptyList(), "");
186    }
187
188    @SuppressWarnings("DefaultCharset")
189    private static Item readEntry(File file) {
190        Reader reader;
191        try {
192            // TODO: Should use Files.newBufferedReader() but it is not available on Android.
193            reader = new FileReader(file);
194        } catch (FileNotFoundException e) {
195            LOGGER.log(Level.FINE, "Roster entry file not found", e);
196            return null;
197        }
198
199        try {
200            XmlPullParser parser = PacketParserUtils.getParserFor(reader);
201            Item item = RosterPacketProvider.parseItem(parser);
202            reader.close();
203            return item;
204        } catch (XmlPullParserException | IOException e) {
205            boolean deleted = file.delete();
206            String message = "Exception while parsing roster entry.";
207            if (deleted) {
208                message += " File was deleted.";
209            }
210            LOGGER.log(Level.SEVERE, message, e);
211            return null;
212        }
213    }
214
215    private boolean addEntryRaw (Item item) {
216        return FileUtils.writeFile(getBareJidFile(item.getJid()), item.toXML());
217    }
218
219    private File getBareJidFile(Jid bareJid) {
220        String encodedJid = Base32.encode(bareJid.toString());
221        return new File(fileDir, ENTRY_PREFIX + encodedJid);
222    }
223
224}