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}