001/* 002 * 003 * Copyright © 2011-2025 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.smackx.caps.cache; 018 019import java.io.DataInputStream; 020import java.io.DataOutputStream; 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.FileOutputStream; 024import java.io.IOException; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import org.jivesoftware.smack.util.PacketParserUtils; 029import org.jivesoftware.smack.util.stringencoder.Base32; 030import org.jivesoftware.smack.util.stringencoder.StringEncoder; 031 032import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 033 034import org.jxmpp.JxmppContext; 035 036/** 037 * Simple implementation of an EntityCapsPersistentCache that uses a directory 038 * to store the Caps information for every known node. Every node is represented 039 * by a file. 040 * 041 * @author Florian Schmaus 042 * 043 */ 044public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache { 045 private static final Logger LOGGER = Logger.getLogger(SimpleDirectoryPersistentCache.class.getName()); 046 047 private final File cacheDir; 048 private final StringEncoder<String> filenameEncoder; 049 050 /** 051 * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the 052 * cacheDir exists and that it's an directory. 053 * <p> 054 * Default filename encoder {@link Base32}, as this will work on all 055 * file systems, both case sensitive and case insensitive. It does however 056 * produce longer filenames. 057 * 058 * @param cacheDir TODO javadoc me please 059 */ 060 public SimpleDirectoryPersistentCache(File cacheDir) { 061 this(cacheDir, Base32.getStringEncoder()); 062 } 063 064 /** 065 * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the 066 * cacheDir exists and that it's an directory. 067 * 068 * If your cacheDir is case insensitive then make sure to set the 069 * StringEncoder to {@link Base32} (which is the default). 070 * 071 * @param cacheDir The directory where the cache will be stored. 072 * @param filenameEncoder Encodes the node string into a filename. 073 */ 074 public SimpleDirectoryPersistentCache(File cacheDir, StringEncoder<String> filenameEncoder) { 075 if (!cacheDir.exists()) 076 throw new IllegalStateException("Cache directory \"" + cacheDir + "\" does not exist"); 077 if (!cacheDir.isDirectory()) 078 throw new IllegalStateException("Cache directory \"" + cacheDir + "\" is not a directory"); 079 080 this.cacheDir = cacheDir; 081 this.filenameEncoder = filenameEncoder; 082 } 083 084 @Override 085 public void addDiscoverInfoByNodePersistent(String nodeVer, DiscoverInfo info) { 086 File nodeFile = getFileFor(nodeVer); 087 try { 088 if (nodeFile.createNewFile()) 089 writeInfoToFile(nodeFile, info); 090 } catch (IOException e) { 091 LOGGER.log(Level.SEVERE, "Failed to write disco info to file", e); 092 } 093 } 094 095 @Override 096 public DiscoverInfo lookup(String nodeVer) { 097 File nodeFile = getFileFor(nodeVer); 098 if (!nodeFile.isFile()) { 099 return null; 100 } 101 DiscoverInfo info = null; 102 try { 103 info = restoreInfoFromFile(nodeFile); 104 } 105 catch (Exception e) { 106 LOGGER.log(Level.WARNING, "Coud not restore info from file", e); 107 } 108 return info; 109 } 110 111 private File getFileFor(String nodeVer) { 112 String filename = filenameEncoder.encode(nodeVer); 113 return new File(cacheDir, filename); 114 } 115 116 @Override 117 public void emptyCache() { 118 File[] files = cacheDir.listFiles(); 119 if (files == null) { 120 return; 121 } 122 for (File f : files) { 123 f.delete(); 124 } 125 } 126 127 /** 128 * Writes the DiscoverInfo stanza to an file 129 * 130 * @param file TODO javadoc me please 131 * @param info TODO javadoc me please 132 * @throws IOException if an I/O error occurred. 133 */ 134 private static void writeInfoToFile(File file, DiscoverInfo info) throws IOException { 135 try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) { 136 dos.writeUTF(info.toXML().toString()); 137 } 138 } 139 140 /** 141 * Tries to restore an DiscoverInfo stanza from a file. 142 * 143 * @param file TODO javadoc me please 144 * @return the restored DiscoverInfo 145 * @throws Exception if an exception occurs. 146 */ 147 private static DiscoverInfo restoreInfoFromFile(File file) throws Exception { 148 String fileContent; 149 try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) { 150 fileContent = dis.readUTF(); 151 } 152 if (fileContent == null) { 153 return null; 154 } 155 var parser = PacketParserUtils.getParserFor(fileContent); 156 return (DiscoverInfo) PacketParserUtils.parseIQ(parser, null, JxmppContext.getDefaultContext()); 157 } 158}