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}