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