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