001/*
002 * #%L
003 * Netarchivesuite - archive
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.archive.arcrepositoryadmin;
024
025import java.io.File;
026import java.io.FileWriter;
027import java.io.IOException;
028import java.io.PrintWriter;
029import java.util.Map;
030
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import dk.netarkivet.archive.arcrepository.distribute.StoreMessage;
035import dk.netarkivet.common.distribute.arcrepository.ReplicaStoreState;
036import dk.netarkivet.common.exceptions.ArgumentNotValid;
037import dk.netarkivet.common.exceptions.IOFailure;
038import dk.netarkivet.common.exceptions.PermissionDenied;
039import dk.netarkivet.common.exceptions.UnknownID;
040
041/**
042 * Class for accessing and manipulating the administrative data for the ArcRepository. In the current implementation, it
043 * consists of a file with a number of lines of the form: filename checksum state timestamp-for-last-state-change [,
044 * bitarchive> storestatus timestamp-for-last-state-change]*
045 * <p>
046 * If a line in the admin data file is corrupt, the entry is removed from admindata.
047 * <p>
048 * Notes: If the admindata file does not exist on start-up, the file is created in the constructor. If the admindata
049 * file on start-up is the oldversion, the admindata file is migrated to the new version.
050 *
051 * @deprecated Use the database instance instead, DatabaseAdmin.
052 */
053@Deprecated
054public class UpdateableAdminData extends AdminData implements Admin {
055
056    /** Logger for this class. */
057    private static final Logger log = LoggerFactory.getLogger(UpdateableAdminData.class);
058
059    /** the singleton for the UpdateableAdminData class. */
060    private static UpdateableAdminData instance;
061
062    /**
063     * Constructor for the UpdateableAdminData class. Reads the admindata file if it exists, creates it otherwise
064     *
065     * @throws PermissionDenied if admin data directory is not accessible
066     * @throws IOFailure if there is trouble reading or creating the admin data file
067     */
068    private UpdateableAdminData() throws IOFailure, PermissionDenied {
069        super();
070        if (!adminDataFile.exists()) {
071            log.info("Creating new admin data file {}", adminDataFile.getAbsolutePath());
072        }
073        // Always rewrite after read, as we're cutting out old entries
074        // to shorten the file.
075        write();
076        log.debug("AdminData created");
077    }
078
079    /**
080     * Get the singleton instance.
081     *
082     * @return The singleton
083     */
084    public static UpdateableAdminData getInstance() {
085        if (UpdateableAdminData.instance == null) {
086            UpdateableAdminData.instance = new UpdateableAdminData();
087        }
088        return UpdateableAdminData.instance;
089    }
090
091    /**
092     * Add new entry to the admin data, and persist it to disk.
093     *
094     * @param filename A filename
095     * @param replyInfo A replyInfo for this entry (may be null)
096     * @param checksum The Checksum for this file
097     */
098    public void addEntry(String filename, StoreMessage replyInfo, String checksum) {
099        addEntry(filename, replyInfo, checksum, true);
100    }
101
102    /**
103     * Add new entry to the admin data, and persist it to disk, if persistNow set to true.
104     *
105     * @param filename A filename
106     * @param replyInfo A replyInfo for this entry (may be null)
107     * @param checksum The Checksum for this file
108     * @param persistNow Shall we persist this entry now?
109     */
110    public void addEntry(String filename, StoreMessage replyInfo, String checksum, boolean persistNow) {
111        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
112        ArgumentNotValid.checkNotNullOrEmpty(checksum, "String checksum");
113        storeEntries.put(filename, new ArcRepositoryEntry(filename, checksum, replyInfo));
114        if (persistNow) {
115            // Persist the new entry
116            // Note: This appends the new entry to the end of the admindata file
117            write(filename);
118        }
119    }
120
121    /**
122     * Records the replyInfo (StoreMessage object) so that it can be retrieved using the given file name.
123     *
124     * @param fileName An arc file that someone is trying to store.
125     * @param replyInfo A StoreMessage object related to this filename.
126     * @throws UnknownID if no info has been registered for the filename.
127     */
128    public void setReplyInfo(String fileName, StoreMessage replyInfo) throws UnknownID {
129        ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName");
130        ArgumentNotValid.checkNotNull(replyInfo, "replyInfo");
131        if (!hasEntry(fileName)) {
132            throw new UnknownID("Cannot set replyinfo '" + replyInfo + "' for unregistered file '" + fileName + "'");
133        }
134        ArcRepositoryEntry entry = storeEntries.get(fileName);
135        entry.setReplyInfo(replyInfo); // TODO Should this be persisted
136    }
137
138    /**
139     * Removes the replyInfo associated with arcfileName.
140     *
141     * @param fileName A file that we are trying to store.
142     * @return the replyInfo associated with arcfileName.
143     * @throws UnknownID If the filename is not known. or no replyInfo is associated with arcfileName.
144     */
145    public StoreMessage removeReplyInfo(String fileName) throws UnknownID {
146        ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName");
147        if (!hasEntry(fileName)) {
148            throw new UnknownID("Cannot get reply info for unregistered file '" + fileName + "'");
149        }
150        if (!hasReplyInfo(fileName)) {
151            throw new UnknownID("replyInfo not set for " + fileName);
152        }
153        ArcRepositoryEntry entry = storeEntries.get(fileName);
154        return entry.getAndRemoveReplyInfo();
155    }
156
157    /**
158     * Sets the store state for the given file on the given bitarchive.
159     *
160     * @param fileName A file that is being stored.
161     * @param replicaID A bitarchive.
162     * @param state The state of upload of arcfileName on bitarchiveID.
163     * @throws UnknownID If the file does not have a store entry.
164     * @throws ArgumentNotValid If the arguments are null or empty
165     */
166    public void setState(String fileName, String replicaID, ReplicaStoreState state) throws UnknownID, ArgumentNotValid {
167        ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName");
168        ArgumentNotValid.checkNotNullOrEmpty(replicaID, "String replicaID");
169        ArgumentNotValid.checkNotNull(state, "ReplicaStoreState state");
170        if (!hasEntry(fileName)) {
171            final String message = "Unregistered file '" + fileName + "' cannot be set to state " + state + " in '"
172                    + replicaID + "'";
173            log.warn(message);
174            throw new UnknownID(message);
175        }
176
177        // TODO What is this good for?
178        // Only used by the toString() method.
179        if (!knownBitArchives.contains(replicaID)) {
180            knownBitArchives.add(replicaID);
181        }
182        storeEntries.get(fileName).setStoreState(replicaID, state);
183        write(fileName); // Add entry for arcfileName in persistent storage.
184    }
185
186    /**
187     * Set/update the checksum for a given arcfileName in the admindata.
188     *
189     * @param fileName Unique name of file for which to store checksum
190     * @param checkSum The generated (MD5) checksum to be stored in reference table
191     * @throws UnknownID if the file is not already registered.
192     * @throws ArgumentNotValid If the arcfileName or the checksum is either null or the empty string.
193     */
194    public void setCheckSum(String fileName, String checkSum) throws ArgumentNotValid, UnknownID {
195        ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName");
196        ArgumentNotValid.checkNotNullOrEmpty(checkSum, "String checkSum");
197        if (!hasEntry(fileName)) {
198            throw new UnknownID("Cannot change checksum for unregistered file '" + fileName + "'");
199        }
200        log.trace("Changing checksum for {} from {} to {}", fileName, getCheckSum(fileName), checkSum);
201        storeEntries.get(fileName).setChecksum(checkSum);
202        write(); // Write everything to persistent storage
203    }
204
205    /**
206     * Write all the admin data to file. This overwrites the previous file and writes data for all entries. This
207     * operation can be rather time-consuming if there is a lot of data. We expect to only do this a) when creating a
208     * new admindata (in order to flush out repeated entries created during uploads) and b) when performing a correct
209     * (to ensure that an arcfile only has one checksum). The write is done atomically, i.e. either the old file is kept
210     * or the entire new file is written.
211     *
212     * @throws IOFailure on trouble writing to file
213     */
214    private void write() throws IOFailure {
215        // First write admindata to a temporary file.
216        final File adminDataStore = adminDataFile;
217        final File tmpDataStore = new File(adminDir, AdminData.ADMIN_FILE_NAME + ".tmp");
218        final File backupDataStore = new File(adminDir, AdminData.ADMIN_FILE_NAME + ".backup");
219        PrintWriter writer = null;
220        try {
221            final FileWriter out = new FileWriter(tmpDataStore);
222            writer = new PrintWriter(out);
223            writer.println(VERSION_NUMBER);
224            for (Map.Entry<String, ArcRepositoryEntry> entry : storeEntries.entrySet()) {
225                final String arcfilename = entry.getKey();
226                final ArcRepositoryEntry arcrepentry = entry.getValue();
227                write(writer, arcfilename, arcrepentry);
228            }
229            writer.flush();
230            writer.close();
231            writer = null;
232            adminDataStore.renameTo(backupDataStore);
233            tmpDataStore.renameTo(adminDataStore);
234        } catch (IOException e) {
235            throw new IOFailure("Failed to write admin data to '" + adminDataFile.getPath() + "'", e);
236        } finally {
237            if (writer != null) {
238                writer.flush();
239                writer.close();
240            }
241            // Delete the temporary file if write failed.
242            tmpDataStore.delete();
243            if (!adminDataStore.exists()) {
244                backupDataStore.renameTo(adminDataStore);
245            } else {
246                backupDataStore.delete();
247            }
248        }
249
250    }
251
252    /**
253     * Write a single entry to the admin data file. This uses the ArcRepositoryEntry.output() method.
254     *
255     * @param writer The output stream
256     * @param arcfilename the filename which entry is to be written
257     * @param arcrepentry The data kept for this arcfile
258     * @throws ArgumentNotValid if arcrepentry.getFilename() != arcfilename
259     */
260    private void write(PrintWriter writer, String arcfilename, final ArcRepositoryEntry arcrepentry)
261            throws ArgumentNotValid {
262        ArgumentNotValid.checkTrue(arcrepentry.getFilename().equals(arcfilename),
263                "arcrepentry.getFilename() is not equal to arcfilename (!!)");
264
265        arcrepentry.output(writer);
266        writer.println();
267    }
268
269    /**
270     * Write a particular entry to the admin data file. This will append the data to the end of the file.
271     *
272     * @param filename the name of the file which entry is to written to admin data file
273     * @throws IOFailure If an exception occurs when accessing the file.
274     */
275    private void write(String filename) throws IOFailure {
276        ArcRepositoryEntry entry = storeEntries.get(filename);
277        File adminDataStore = adminDataFile;
278        PrintWriter writer = null;
279        try {
280            final FileWriter out = new FileWriter(adminDataStore, true);
281            writer = new PrintWriter(out);
282            write(writer, filename, entry);
283        } catch (IOException e) {
284            throw new IOFailure("Failed to write admin data for '" + filename + "' to '" + adminDataFile.getName()
285                    + "'", e);
286        } finally {
287            if (writer != null) {
288                writer.flush();
289                writer.close();
290            }
291        }
292        log.debug("appending entry for filename '{}' to admin.data", filename);
293    }
294
295    /** Makes sure all data is written to disk. */
296    public void close() {
297        if (instance != null) {
298            write(); // This rewrites all admindata onto disk
299        }
300        instance = null;
301    }
302
303}