001/**
002 *
003 * Copyright 2018 Paul Schaub.
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.ox.store.filebased;
018
019import java.io.BufferedReader;
020import java.io.BufferedWriter;
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.InputStreamReader;
025import java.io.OutputStream;
026import java.io.OutputStreamWriter;
027import java.text.ParseException;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.Map;
031import java.util.logging.Level;
032import java.util.logging.Logger;
033
034import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpMetadataStore;
035import org.jivesoftware.smackx.ox.store.definition.OpenPgpMetadataStore;
036import org.jivesoftware.smackx.ox.util.FileUtils;
037import org.jivesoftware.smackx.ox.util.Util;
038
039import org.jxmpp.jid.BareJid;
040import org.jxmpp.util.XmppDateTime;
041import org.pgpainless.key.OpenPgpV4Fingerprint;
042
043/**
044 * Implementation of the {@link OpenPgpMetadataStore}, which stores metadata information in a file structure.
045 * The information is stored in the following directory structure:
046 *
047 * <pre>
048 * {@code
049 * <basePath>/
050 *     <userjid@server.tld>/
051 *         announced.list       // list of the users announced key fingerprints and modification dates
052 * }
053 * </pre>
054 */
055public class FileBasedOpenPgpMetadataStore extends AbstractOpenPgpMetadataStore {
056
057    public static final String ANNOUNCED = "announced.list";
058    public static final String RETRIEVED = "retrieved.list";
059
060    private static final Logger LOGGER = Logger.getLogger(FileBasedOpenPgpMetadataStore.class.getName());
061
062    private final File basePath;
063
064    public FileBasedOpenPgpMetadataStore(File basePath) {
065        this.basePath = basePath;
066    }
067
068    @Override
069    public Map<OpenPgpV4Fingerprint, Date> readAnnouncedFingerprintsOf(BareJid contact) throws IOException {
070        return readFingerprintsAndDates(getAnnouncedFingerprintsPath(contact));
071    }
072
073    @Override
074    public void writeAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> metadata)
075            throws IOException {
076        File destination = getAnnouncedFingerprintsPath(contact);
077        writeFingerprintsAndDates(metadata, destination);
078    }
079
080    static Map<OpenPgpV4Fingerprint, Date> readFingerprintsAndDates(File source) throws IOException {
081        if (!source.exists() || source.isDirectory()) {
082            return new HashMap<>();
083        }
084
085        BufferedReader reader = null;
086        try {
087            InputStream inputStream = FileUtils.prepareFileInputStream(source);
088            InputStreamReader isr = new InputStreamReader(inputStream, Util.UTF8);
089            reader = new BufferedReader(isr);
090            Map<OpenPgpV4Fingerprint, Date> fingerprintDateMap = new HashMap<>();
091
092            String line; int lineNr = 0;
093            while ((line = reader.readLine()) != null) {
094                lineNr++;
095
096                line = line.trim();
097                String[] split = line.split(" ");
098                if (split.length != 2) {
099                    LOGGER.log(Level.FINE, "Skipping invalid line " + lineNr + " in file " + source.getAbsolutePath());
100                    continue;
101                }
102
103                try {
104                    OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(split[0]);
105                    Date date = XmppDateTime.parseXEP0082Date(split[1]);
106                    fingerprintDateMap.put(fingerprint, date);
107                } catch (IllegalArgumentException | ParseException e) {
108                    LOGGER.log(Level.WARNING, "Error parsing fingerprint/date touple in line " + lineNr +
109                            " of file " + source.getAbsolutePath(), e);
110                }
111            }
112
113            reader.close();
114            return fingerprintDateMap;
115
116        } catch (IOException e) {
117            if (reader != null) {
118                try {
119                    reader.close();
120                } catch (IOException ignored) {
121                    // Don't care
122                }
123            }
124            throw e;
125        }
126    }
127
128    static void writeFingerprintsAndDates(Map<OpenPgpV4Fingerprint, Date> data, File destination)
129            throws IOException {
130
131        if (data == null || data.isEmpty()) {
132            if (!destination.exists()) {
133                return;
134            } else {
135                if (!destination.delete()) {
136                    throw new IOException("Cannot delete file " + destination.getAbsolutePath());
137                }
138            }
139            return;
140        }
141
142        if (!destination.exists()) {
143            File parent = destination.getParentFile();
144            if (!parent.exists() && !parent.mkdirs()) {
145                throw new IOException("Cannot create directory " + parent.getAbsolutePath());
146            }
147
148            if (!destination.createNewFile()) {
149                throw new IOException("Cannot create file " + destination.getAbsolutePath());
150            }
151        }
152
153        if (destination.isDirectory()) {
154            throw new IOException("File " + destination.getAbsolutePath() + " is a directory.");
155        }
156
157        BufferedWriter writer = null;
158        try {
159            OutputStream outputStream = FileUtils.prepareFileOutputStream(destination);
160            OutputStreamWriter osw = new OutputStreamWriter(outputStream, Util.UTF8);
161            writer = new BufferedWriter(osw);
162            for (OpenPgpV4Fingerprint fingerprint : data.keySet()) {
163                Date date = data.get(fingerprint);
164                String line = fingerprint.toString() + " " +
165                        (date != null ? XmppDateTime.formatXEP0082Date(date) : XmppDateTime.formatXEP0082Date(new Date()));
166                writer.write(line);
167                writer.newLine();
168            }
169            writer.flush();
170            writer.close();
171        } catch (IOException e) {
172            if (writer != null) {
173                try {
174                    writer.close();
175                } catch (IOException ignored) {
176                    // Don't care
177                }
178            }
179            throw e;
180        }
181    }
182
183    private File getAnnouncedFingerprintsPath(BareJid contact) {
184        return new File(FileBasedOpenPgpStore.getContactsPath(basePath, contact), ANNOUNCED);
185    }
186
187    private File getRetrievedFingerprintsPath(BareJid contact) {
188        return new File(FileBasedOpenPgpStore.getContactsPath(basePath, contact), RETRIEVED);
189    }
190}