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.roster.rosterstore; 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.roster.packet.RosterPacket; 030import org.jivesoftware.smack.roster.packet.RosterPacket.Item; 031import org.jivesoftware.smack.util.FileUtils; 032import org.jivesoftware.smack.util.XmlStringBuilder; 033import org.jivesoftware.smack.util.stringencoder.Base32; 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 = Base32.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}