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