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.smack.util.CloseableUtil;
035import org.jivesoftware.smack.util.FileUtils;
036
037import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpMetadataStore;
038import org.jivesoftware.smackx.ox.store.definition.OpenPgpMetadataStore;
039import org.jivesoftware.smackx.ox.util.Util;
040
041import org.jxmpp.jid.BareJid;
042import org.jxmpp.util.XmppDateTime;
043import org.pgpainless.key.OpenPgpV4Fingerprint;
044
045/**
046 * Implementation of the {@link OpenPgpMetadataStore}, which stores metadata information in a file structure.
047 * The information is stored in the following directory structure:
048 *
049 * <pre>
050 * {@code
051 * <basePath>/
052 *     <userjid@server.tld>/
053 *         announced.list       // list of the users announced key fingerprints and modification dates
054 * }
055 * </pre>
056 */
057public class FileBasedOpenPgpMetadataStore extends AbstractOpenPgpMetadataStore {
058
059    public static final String ANNOUNCED = "announced.list";
060
061    private static final Logger LOGGER = Logger.getLogger(FileBasedOpenPgpMetadataStore.class.getName());
062
063    private final File basePath;
064
065    public FileBasedOpenPgpMetadataStore(File basePath) {
066        this.basePath = basePath;
067    }
068
069    @Override
070    public Map<OpenPgpV4Fingerprint, Date> readAnnouncedFingerprintsOf(BareJid contact) throws IOException {
071        return readFingerprintsAndDates(getAnnouncedFingerprintsPath(contact));
072    }
073
074    @Override
075    public void writeAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> metadata)
076            throws IOException {
077        File destination = getAnnouncedFingerprintsPath(contact);
078        writeFingerprintsAndDates(metadata, destination);
079    }
080
081    static Map<OpenPgpV4Fingerprint, Date> readFingerprintsAndDates(File source) throws IOException {
082        // TODO: Why do we not throw a FileNotFoundException here?
083        if (!source.exists() || source.isDirectory()) {
084            return new HashMap<>();
085        }
086
087        BufferedReader reader = null;
088        try {
089            InputStream inputStream = FileUtils.prepareFileInputStream(source);
090            InputStreamReader isr = new InputStreamReader(inputStream, Util.UTF8);
091            reader = new BufferedReader(isr);
092            Map<OpenPgpV4Fingerprint, Date> fingerprintDateMap = new HashMap<>();
093
094            String line; int lineNr = 0;
095            while ((line = reader.readLine()) != null) {
096                lineNr++;
097
098                line = line.trim();
099                String[] split = line.split(" ");
100                if (split.length != 2) {
101                    LOGGER.log(Level.FINE, "Skipping invalid line " + lineNr + " in file " + source.getAbsolutePath());
102                    continue;
103                }
104
105                try {
106                    OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(split[0]);
107                    Date date = XmppDateTime.parseXEP0082Date(split[1]);
108                    fingerprintDateMap.put(fingerprint, date);
109                } catch (IllegalArgumentException | ParseException e) {
110                    LOGGER.log(Level.WARNING, "Error parsing fingerprint/date touple in line " + lineNr +
111                            " of file " + source.getAbsolutePath(), e);
112                }
113            }
114
115            return fingerprintDateMap;
116        } finally {
117            CloseableUtil.maybeClose(reader, LOGGER);
118        }
119    }
120
121    static void writeFingerprintsAndDates(Map<OpenPgpV4Fingerprint, Date> data, File destination)
122            throws IOException {
123        if (data == null || data.isEmpty()) {
124            FileUtils.maybeDeleteFileOrThrow(destination);
125            return;
126        }
127
128        FileUtils.maybeCreateFileWithParentDirectories(destination);
129
130        BufferedWriter writer = null;
131        try {
132            OutputStream outputStream = FileUtils.prepareFileOutputStream(destination);
133            OutputStreamWriter osw = new OutputStreamWriter(outputStream, Util.UTF8);
134            writer = new BufferedWriter(osw);
135            for (OpenPgpV4Fingerprint fingerprint : data.keySet()) {
136                Date date = data.get(fingerprint);
137                String line = fingerprint.toString() + " " +
138                        (date != null ? XmppDateTime.formatXEP0082Date(date) : XmppDateTime.formatXEP0082Date(new Date()));
139                writer.write(line);
140                writer.newLine();
141            }
142        } finally {
143            CloseableUtil.maybeClose(writer, LOGGER);
144        }
145    }
146
147    private File getAnnouncedFingerprintsPath(BareJid contact) {
148        return new File(FileBasedOpenPgpStore.getContactsPath(basePath, contact), ANNOUNCED);
149    }
150}