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