001/**
002 *
003 * Copyright © 2011 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.io.Reader;
026import java.io.StringReader;
027import java.util.logging.Level;
028import java.util.logging.Logger;
029
030import org.jivesoftware.smack.packet.IQ;
031import org.jivesoftware.smack.provider.IQProvider;
032import org.jivesoftware.smack.util.Base32Encoder;
033import org.jivesoftware.smack.util.PacketParserUtils;
034import org.jivesoftware.smack.util.StringEncoder;
035import org.jivesoftware.smackx.caps.EntityCapsManager;
036import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
037import org.jivesoftware.smackx.disco.provider.DiscoverInfoProvider;
038import org.xmlpull.v1.XmlPullParser;
039import org.xmlpull.v1.XmlPullParserException;
040
041/**
042 * Simple implementation of an EntityCapsPersistentCache that uses a directory
043 * to store the Caps information for every known node. Every node is represented
044 * by a file.
045 * 
046 * @author Florian Schmaus
047 * 
048 */
049public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache {
050    private static final Logger LOGGER = Logger.getLogger(SimpleDirectoryPersistentCache.class.getName());
051    
052    private File cacheDir;
053    private StringEncoder filenameEncoder;
054
055    /**
056     * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the
057     * cacheDir exists and that it's an directory.
058     * <p>
059     * Default filename encoder {@link Base32Encoder}, as this will work on all 
060     * file systems, both case sensitive and case insensitive.  It does however 
061     * produce longer filenames.
062     * 
063     * @param cacheDir
064     */
065    public SimpleDirectoryPersistentCache(File cacheDir) {
066        this(cacheDir, Base32Encoder.getInstance());
067    }
068
069    /**
070     * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the
071     * cacheDir exists and that it's an directory.
072     * 
073     * If your cacheDir is case insensitive then make sure to set the
074     * StringEncoder to {@link Base32Encoder} (which is the default).
075     * 
076     * @param cacheDir The directory where the cache will be stored.
077     * @param filenameEncoder Encodes the node string into a filename.
078     */
079    public SimpleDirectoryPersistentCache(File cacheDir, StringEncoder filenameEncoder) {
080        if (!cacheDir.exists())
081            throw new IllegalStateException("Cache directory \"" + cacheDir + "\" does not exist");
082        if (!cacheDir.isDirectory())
083            throw new IllegalStateException("Cache directory \"" + cacheDir + "\" is not a directory");
084
085        this.cacheDir = cacheDir;
086        this.filenameEncoder = filenameEncoder;
087    }
088
089    @Override
090    public void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info) {
091        String filename = filenameEncoder.encode(node);
092        File nodeFile = new File(cacheDir, filename);
093        try {
094            if (nodeFile.createNewFile())
095                writeInfoToFile(nodeFile, info);
096        } catch (IOException e) {
097            LOGGER.log(Level.SEVERE, "Failed to write disco info to file", e);
098        }
099    }
100
101    @Override
102    public void replay() throws IOException {
103        File[] files = cacheDir.listFiles();
104        for (File f : files) {
105            String node = filenameEncoder.decode(f.getName());
106            DiscoverInfo info = restoreInfoFromFile(f);
107            if (info == null)
108                continue;
109
110            EntityCapsManager.addDiscoverInfoByNode(node, info);
111        }
112    }
113
114    public void emptyCache() {
115        File[] files = cacheDir.listFiles();
116        for (File f : files) {
117            f.delete();
118        }
119    }
120
121    /**
122     * Writes the DiscoverInfo packet to an file
123     * 
124     * @param file
125     * @param info
126     * @throws IOException
127     */
128    private static void writeInfoToFile(File file, DiscoverInfo info) throws IOException {
129        DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));
130        try {
131            dos.writeUTF(info.toXML().toString());
132        } finally {
133            dos.close();
134        }
135    }
136
137    /**
138     * Tries to restore an DiscoverInfo packet from a file.
139     * 
140     * @param file
141     * @return the restored DiscoverInfo
142     * @throws IOException
143     */
144    private static DiscoverInfo restoreInfoFromFile(File file) throws IOException {
145        DataInputStream dis = new DataInputStream(new FileInputStream(file));
146        String fileContent = null;
147        String id;
148        String from;
149        String to;
150
151        try {
152            fileContent = dis.readUTF();
153        } finally {
154            dis.close();
155        }
156        if (fileContent == null)
157            return null;
158
159        Reader reader = new StringReader(fileContent);
160        XmlPullParser parser;
161        try {
162            parser = PacketParserUtils.newXmppParser();
163            parser.setInput(reader);
164        } catch (XmlPullParserException xppe) {
165            LOGGER.log(Level.SEVERE, "Exception initializing parser", xppe);
166            return null;
167        }
168
169        DiscoverInfo iqPacket;
170        IQProvider provider = new DiscoverInfoProvider();
171
172        // Parse the IQ, we only need the id
173        try {
174            parser.next();
175            id = parser.getAttributeValue("", "id");
176            from = parser.getAttributeValue("", "from");
177            to = parser.getAttributeValue("", "to");
178            parser.next();
179        } catch (XmlPullParserException e1) {
180            return null;
181        }
182
183        try {
184            iqPacket = (DiscoverInfo) provider.parseIQ(parser);
185        } catch (Exception e) {
186            return null;
187        }
188
189        iqPacket.setPacketID(id);
190        iqPacket.setFrom(from);
191        iqPacket.setTo(to);
192        iqPacket.setType(IQ.Type.RESULT);
193        return iqPacket;
194    }
195}