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.checksum.distribute;
024
025import java.io.File;
026
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import dk.netarkivet.archive.ArchiveSettings;
031import dk.netarkivet.archive.bitarchive.distribute.UploadMessage;
032import dk.netarkivet.archive.checksum.ChecksumArchive;
033import dk.netarkivet.common.CommonSettings;
034import dk.netarkivet.common.distribute.Channels;
035import dk.netarkivet.common.distribute.JMSConnectionFactory;
036import dk.netarkivet.common.distribute.RemoteFile;
037import dk.netarkivet.common.distribute.RemoteFileFactory;
038import dk.netarkivet.common.exceptions.ArgumentNotValid;
039import dk.netarkivet.common.exceptions.IllegalState;
040import dk.netarkivet.common.exceptions.UnknownID;
041import dk.netarkivet.common.utils.FileUtils;
042import dk.netarkivet.common.utils.NotificationType;
043import dk.netarkivet.common.utils.NotificationsFactory;
044import dk.netarkivet.common.utils.Settings;
045import dk.netarkivet.common.utils.SystemUtils;
046
047/**
048 * The server for the ChecksumFileApplication. Used for communication between the ArcRepository and the checksum
049 * archive.
050 */
051public class ChecksumFileServer extends ChecksumArchiveServer {
052
053    /** The logger used by this class. */
054    private static final Logger log = LoggerFactory.getLogger(ChecksumFileServer.class);
055
056    /** The instance of this server. */
057    protected static ChecksumFileServer instance;
058
059    /** The archive which contain the actual data. */
060    protected ChecksumArchive cs;
061
062    /** The character to separate the applicationInstanceId and the IP address. */
063    public static final String APPLICATION_ID_SEPARATOR = "_";
064
065    /**
066     * Returns the unique instance of this class.
067     * <p>
068     * The server creates an instance of the checksum it creates access to and starts to listen to a JMS messages on the
069     * incoming JMS queue.
070     * <p>
071     * <p>
072     * Should this do the heart beats to a monitor? This would be quite odd, since Checksum does not use a monitor.
073     *
074     * @return This instance.
075     */
076    public static ChecksumFileServer getInstance() {
077        if (instance == null) {
078            instance = new ChecksumFileServer();
079        }
080        return instance;
081    }
082
083    /**
084     * Constructor.
085     */
086    private ChecksumFileServer() {
087        // log that this instance is been invoked.
088        log.info("Initialising the ChecksumFileServer.");
089
090        // get the instance of the checksum archive
091        cs = ChecksumArchiveFactory.getInstance();
092
093        // initialise the JMSConnection.
094        jmsCon = JMSConnectionFactory.getInstance();
095
096        // initialise the channel.
097        theCR = Channels.getTheCR();
098
099        // Start listening to the channel.
100        jmsCon.setListener(theCR, this);
101
102        // create the application identifier
103        checksumAppId = createAppId();
104
105        // log that this instance has successfully been invoked.
106        log.info("ChecksumFileServer '{}' initialised.", checksumAppId);
107    }
108
109    /**
110     * Method for closing the instance.
111     */
112    public void close() {
113        log.info("ChecksumFileServer '{}' closing down.", checksumAppId);
114        cleanup();
115        if (jmsCon != null) {
116            jmsCon.removeListener(theCR, this);
117            jmsCon = null;
118        }
119        log.info("ChecksumFileServer '{}' closed down.", checksumAppId);
120    }
121
122    /**
123     * Method for cleaning up, when closing this instance down.
124     */
125    public void cleanup() {
126        instance = null;
127        cs.cleanup();
128    }
129
130    /**
131     * Method for retrieving the identification of this application.
132     *
133     * @return The id of this application.
134     */
135    public String getAppId() {
136        return checksumAppId;
137    }
138
139    /**
140     * Method for creating the identification for this application.
141     *
142     * @return The id of this application.
143     */
144    protected String createAppId() {
145        String id;
146        // Create an id with the IP address of this current host
147        id = SystemUtils.getLocalIP();
148
149        // Append an underscore and APPLICATION_INSTANCE_ID from settings
150        // to the id, if specified in settings.
151        // If no APPLICATION_INSTANCE_ID is found do nothing.
152        try {
153            String applicationInstanceId = Settings.get(CommonSettings.APPLICATION_INSTANCE_ID);
154            if (!applicationInstanceId.isEmpty()) {
155                id += APPLICATION_ID_SEPARATOR + applicationInstanceId;
156            }
157        } catch (UnknownID e) {
158            // Ignore the fact, that there is no APPLICATION_INSTANCE_ID in
159            // settings
160            log.warn("No setting APPLICATION_INSTANCE_ID found in settings: ", e);
161        }
162        return id;
163    }
164
165    /**
166     * The method for uploading arc files. Note that cleanup of the upload file embedded in the message is delegated the
167     * method {@link ChecksumArchive#upload(RemoteFile, String)}
168     *
169     * @param msg The upload message, containing the file to upload.
170     * @throws ArgumentNotValid If the UploadMessage is null.
171     */
172    public void visit(UploadMessage msg) throws ArgumentNotValid {
173        ArgumentNotValid.checkNotNull(msg, "UploadMessage msg");
174        log.debug("Receiving UploadMessage: " + msg.toString());
175        try {
176            try {
177                cs.upload(msg.getRemoteFile(), msg.getArcfileName());
178            } catch (Throwable e) {
179                log.warn("Cannot process upload message '{}'", msg, e);
180                msg.setNotOk(e);
181            } finally { // check if enough space
182                if (!cs.hasEnoughSpace()) {
183                    log.warn("Not enough space any more.");
184                    jmsCon.removeListener(theCR, this);
185                }
186            }
187        } catch (Throwable e) {
188            log.warn("Cannnot remove listener after upload message '{}'", msg, e);
189        } finally {
190            log.debug("Replying to UploadMessage: {}", msg.toString());
191            jmsCon.reply(msg);
192        }
193    }
194
195    /**
196     * Method for correcting an entry in the archive. It start by ensuring that the file exists, then it checks the
197     * credentials. Then it is checked whether the "bad entry" does have the "bad checksum". If no problems occurred,
198     * then the bad entry will be corrected by the archive (the bad entry is removed from the archive file and put into
199     * the "wrong entry" file. Then the new entry is placed in the archive file.
200     * <p>
201     * If it fails in any of the above, then the method fails (throws an exception which is caught and use for replying
202     * NotOk to the message).
203     *
204     * @param msg The message containing the correct instance of the file to correct.
205     * @throws ArgumentNotValid If the correct message is null.
206     */
207    public void visit(CorrectMessage msg) throws ArgumentNotValid {
208        ArgumentNotValid.checkNotNull(msg, "CorrectMessage msg");
209        log.debug("Receiving correct message: {}", msg.toString());
210        // the file for containing the received file from the message.
211        File correctFile = null;
212        try {
213            String filename = msg.getArcfileName();
214            String currentCs = cs.getChecksum(filename);
215            String incorrectCs = msg.getIncorrectChecksum();
216
217            // ensure that the entry actually exists.
218            if (currentCs == null) {
219                // This exception is logged later.
220                throw new IllegalState("Cannot correct an entry for the file '" + filename
221                        + "', since it is not within the archive.");
222            }
223
224            // Check credentials
225            String credentialsReceived = msg.getCredentials();
226            if (credentialsReceived == null || credentialsReceived.isEmpty()
227                    || !credentialsReceived.equals(Settings.get(ArchiveSettings.ENVIRONMENT_THIS_CREDENTIALS))) {
228                throw new IllegalState("The received credentials '" + credentialsReceived
229                        + "' were invalid. The entry of " + "file '" + filename + "' will not be corrected.");
230            }
231
232            // check that the current checksum is incorrect as supposed.
233            if (!currentCs.equals(incorrectCs)) {
234                throw new IllegalState("Wrong checksum for the entry for file '" + filename + "' has the checksum '"
235                        + currentCs + "', " + "though it was supposed to have the checksum '" + incorrectCs + "'.");
236            }
237
238            // retrieve the data as a file.
239            correctFile = File.createTempFile("correct", filename, FileUtils.getTempDir());
240            msg.getData(correctFile);
241
242            // Log and notify
243            String warning = "The record for file '" + filename + "' is being corrected at '"
244                    + Settings.get(CommonSettings.USE_REPLICA_ID) + "'";
245            log.warn(warning);
246            NotificationsFactory.getInstance().notify(warning, NotificationType.WARNING);
247
248            // put the file into the archive.
249            File badFile = cs.correct(filename, correctFile);
250
251            // Send the file containing the removed entry back.
252            msg.setRemovedFile(RemoteFileFactory.getMovefileInstance(badFile));
253        } catch (Throwable t) {
254            // Handle errors.
255            log.warn("Cannot handle CorrectMessage: '{}'", msg, t);
256            msg.setNotOk(t);
257        } finally {
258            // log and reply at the end.
259            log.info("Replying CorrectMessage: {}", msg.toString());
260            jmsCon.reply(msg);
261
262            // cleanup the data file
263            if (correctFile != null) {
264                FileUtils.remove(correctFile);
265            }
266        }
267    }
268
269    /**
270     * Method for retrieving the checksum of a record.
271     *
272     * @param msg The GetChecksumMessage which contains the name of the record to have its checksum retrieved.
273     * @throws ArgumentNotValid If the message is null.
274     */
275    public void visit(GetChecksumMessage msg) throws ArgumentNotValid {
276        ArgumentNotValid.checkNotNull(msg, "GetChecksumMessage msg");
277
278        log.debug("Receiving GetChecksumMessage: {}", msg.toString());
279        try {
280            // get the name of the arc file
281            String filename = msg.getArcfileName();
282            // get the checksum of the arc file
283            String checksum = cs.getChecksum(filename);
284
285            // Check if the checksum was found. If not throw exception.
286            if (checksum == null || checksum.isEmpty()) {
287                // The error is logged, when the exception is caught.
288                throw new IllegalState("Cannot fetch checksum of an entry, " + filename
289                        + ", which is not within the archive.");
290            }
291
292            // send the checksum of the arc file.
293            msg.setChecksum(checksum);
294        } catch (Throwable e) {
295            // Handle errors (if the file cannot be found).
296            log.warn("Cannot handle '{}' containing the message: {}", msg.getClass().getName(), msg, e);
297            msg.setNotOk(e);
298        } finally {
299            // TODO this should be set elsewhere.
300            msg.setIsReply();
301            // log the message and reply.
302            log.info("Replying GetChecksumMessage: {}", msg.toString());
303            jmsCon.reply(msg);
304        }
305    }
306
307    /**
308     * Method for retrieving all the filenames within the archive.
309     *
310     * @param msg The GetAllFilenamesMessage.
311     * @throws ArgumentNotValid If the GetAllFilenamesMessages is null.
312     */
313    public void visit(GetAllFilenamesMessage msg) throws ArgumentNotValid {
314        ArgumentNotValid.checkNotNull(msg, "GetAllFilenamesMessage msg");
315        log.debug("Receiving GetAllFilenamesMessage: {}", msg.toString());
316
317        try {
318            // get all the file names
319            msg.setFile(cs.getAllFilenames());
320        } catch (Throwable e) {
321            log.warn("Cannot retrieve the filenames to reply on the {} : {}", msg.getClass().getName(), msg, e);
322            msg.setNotOk(e);
323        } finally {
324            // log the message and reply.
325            log.info("Replying GetAllFilenamesMessage: {}", msg.toString());
326            jmsCon.reply(msg);
327        }
328    }
329
330    /**
331     * Method for retrieving a map containing all the checksums and their corresponding filenames within the archive.
332     *
333     * @param msg The GetAllChecksumMessage.
334     * @throws ArgumentNotValid If the GetAllChecksumMessage is null.
335     */
336    public void visit(GetAllChecksumsMessage msg) throws ArgumentNotValid {
337        ArgumentNotValid.checkNotNull(msg, "GetAllChecksumsMessage msg");
338        log.debug("Receiving GetAllChecksumsMessage: {}", msg.toString());
339
340        try {
341            msg.setFile(cs.getArchiveAsFile());
342        } catch (Throwable e) {
343            log.warn("Cannot retrieve all the checksums.", e);
344            msg.setNotOk(e);
345        } finally {
346            // log the message and reply
347            log.info("Replying GetAllChecksumsMessage: {}", msg.toString());
348            jmsCon.reply(msg);
349        }
350    }
351
352}