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 */
023
024package dk.netarkivet.archive.arcrepositoryadmin;
025
026import java.io.PrintWriter;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.Set;
031
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import dk.netarkivet.archive.arcrepository.distribute.StoreMessage;
036import dk.netarkivet.common.distribute.arcrepository.ReplicaStoreState;
037import dk.netarkivet.common.exceptions.ArgumentNotValid;
038
039/**
040 * This class contains the information that we keep about each file in the arcrepository: Checksum and the store states
041 * for all bitarchives.
042 * <p>
043 * TODO Maybe don't have the store state info for fully completed stores, or else use a slimmer map for it.
044 */
045public class ArcRepositoryEntry {
046
047    /** The log. */
048    private static final Logger log = LoggerFactory.getLogger(ArcRepositoryEntry.class);
049
050    /** The filename for this entry (only set in the constructor). */
051    private String filename;
052
053    /**
054     * The checksum of this file. This field is persistent in the admin data file. This field can never be null. Note:
055     * AdminData.setChecksum() can change this value. Note: ArcRepositoryEntry.setChecksum() can change this value.
056     */
057    private String md5sum;
058
059    /**
060     * How the upload of this file into the bitarchives went. This field is persistent in the admin data file. After
061     * constructor initialization, this field should only be set in case of a correct operation (now or earlier). Note:
062     * the value 2 below is a hint to the number of bitarchives in our system.
063     */
064    private Map<String, ArchiveStoreState> storeStates = new HashMap<String, ArchiveStoreState>(2);
065
066    /**
067     * The information used to reply about this entry being done. Once a reply has been sent, this entry is set to null.
068     * This field is not persistent.
069     */
070    private StoreMessage replyInfo;
071
072    /**
073     * String used to separate the different parts of the arcRepositoryEntry, when we write the entry to persistent
074     * storage. Make package private, so accessable from AdminData
075     */
076    static final String ENTRY_COMPONENT_SEPARATOR_STRING = " , ";
077
078    /**
079     * General delimiter used several places. Used to delimite the different storestates for the entry in the output()
080     * method.
081     */
082    private static final String GENERAL_DELIMITER = " ";
083
084    /**
085     * Create a new entry with given checksum and replyinfo.
086     *
087     * @param filename The filename for this entry
088     * @param md5sum The checksum for this entry
089     * @param replyInfo The one-use-only reply info chunk
090     */
091    public ArcRepositoryEntry(String filename, String md5sum, StoreMessage replyInfo) {
092        this.filename = filename;
093        this.md5sum = md5sum;
094        this.replyInfo = replyInfo;
095    }
096
097    /**
098     * Get the ArchiveStoreState for the entry in general. This is computed from the ArchiveStoreStates for the
099     * bitarchives. <br>
100     * 1. If no information about the bitarchives are available, the state UPLOAD_FAILED with timestamp=NOW is returned <br>
101     * 2. If there are information about one bitarchive, the state of this bitarchive is returned. <br>
102     * 3. If there are information from more than one bitarchive, A. if the state of one of the bitarchives equals
103     * UPLOAD_FAILED, the state UPLOAD_FAILED with the latest timestamp is returned B. else, find the lowest state of
104     * the N bitarchives: return this state together with the the latest timestamp
105     * <p>
106     * Note that the storestate and the timestamp might not come from the same bitarchive.
107     *
108     * @return the current ArchiveStoreState for the entry in general
109     */
110    public ArchiveStoreState getGeneralStoreState() {
111        Set<String> bitarchives = storeStates.keySet();
112        // Check whether scenario 1.
113        if (bitarchives.size() == 0) {
114            return new ArchiveStoreState(ReplicaStoreState.UPLOAD_FAILED);
115        }
116
117        String[] bitarchiveNames = bitarchives.toArray(new String[bitarchives.size()]);
118        // Check whether scenario 2.
119        if (bitarchives.size() == 1) {
120            ArchiveStoreState ass = storeStates.get(bitarchiveNames[0]);
121            return new ArchiveStoreState(ass.getState(), ass.getLastChanged());
122        }
123
124        // Scenario 3: If there are information from more than one bitarchive
125        ArchiveStoreState ass = storeStates.get(bitarchiveNames[0]);
126        Date lastChanged = ass.getLastChanged();
127        boolean failState = false;
128        ReplicaStoreState lowestStoreState = ass.getState();
129        if (ass.getState().equals(ReplicaStoreState.UPLOAD_FAILED)) {
130            failState = true;
131        }
132        for (int i = 1; i < bitarchiveNames.length; i++) {
133            ArchiveStoreState tmpAss = storeStates.get(bitarchiveNames[i]);
134            if (tmpAss.getState().ordinal() < lowestStoreState.ordinal()) {
135                lowestStoreState = tmpAss.getState();
136            }
137            if (tmpAss.getState().equals(ReplicaStoreState.UPLOAD_FAILED)) {
138                failState = true;
139            }
140            if (tmpAss.getLastChanged().after(lastChanged)) {
141                lastChanged = tmpAss.getLastChanged();
142            }
143        }
144
145        // Scenario 3A: if the state of one of the bitarchives equals
146        // UPLOAD_FAILED.
147        if (failState) {
148            return new ArchiveStoreState(ReplicaStoreState.UPLOAD_FAILED, lastChanged);
149        }
150
151        // Scenario 3B:
152        // B. else, find the lowest state of the N bitarchives:
153        // return this state together with the the latest timestamp
154        return new ArchiveStoreState(lowestStoreState, lastChanged);
155    }
156
157    /**
158     * Set the StoreState for a specific bitarchive (set timestamp for last update to NOW).
159     *
160     * @param ba a bitarchive
161     * @param state the new StoreState for this bitarchive.
162     */
163    void setStoreState(String ba, ReplicaStoreState state) {
164        ArchiveStoreState ass = new ArchiveStoreState(state);
165        storeStates.put(ba, ass);
166    }
167
168    /**
169     * Set the StoreState for a specific bitarchive (set timestamp for last update to lastchanged).
170     *
171     * @param baId a bitarchive
172     * @param state the new StoreState for this bitarchive.
173     * @param lastchanged the time for when the state was changed
174     */
175    void setStoreState(String baId, ReplicaStoreState state, Date lastchanged) {
176        ArchiveStoreState ass = new ArchiveStoreState(state, lastchanged);
177        storeStates.put(baId, ass);
178    }
179
180    /**
181     * Get the StoreState for this entry for a given bitarchive or null if none.
182     *
183     * @param baId a bitarchive id
184     * @return the StoreState for a given bitarchive.
185     */
186    public ReplicaStoreState getStoreState(String baId) {
187        ArgumentNotValid.checkNotNullOrEmpty(baId, "String baId");
188        ArchiveStoreState ass = storeStates.get(baId);
189        if (ass == null) {
190            return null;
191        }
192        return ass.getState();
193    }
194
195    /**
196     * Get the filename for this entry.
197     *
198     * @return the filename for this entry
199     */
200    String getFilename() {
201        return filename;
202    }
203
204    /**
205     * Set the checksum for this entry.
206     *
207     * @param checksum the new checksum for this entry
208     */
209    void setChecksum(String checksum) {
210        ArgumentNotValid.checkNotNullOrEmpty(checksum, "String checksum");
211        md5sum = checksum;
212    }
213
214    /**
215     * Get the checksum for this entry.
216     *
217     * @return the stored checksum for this entry
218     */
219    public String getChecksum() {
220        return md5sum;
221    }
222
223    /**
224     * Get the reply info and remove it from the entry.
225     *
226     * @return A reply info object that nobody else has gotten or will get.
227     */
228    StoreMessage getAndRemoveReplyInfo() {
229        StoreMessage currentReplyInfo = this.replyInfo;
230        // Reset replyInfo of this entry.
231        this.replyInfo = null;
232        return currentReplyInfo; // return value of replyInfo before being reset
233    }
234
235    /**
236     * Returns information of whether a ReplyInfo object has been stored with this entry.
237     *
238     * @return true, if replyInfo is not null.
239     */
240    boolean hasReplyInfo() {
241        return replyInfo != null;
242    }
243
244    /**
245     * Write this object to persistent storage.
246     *
247     * @param o A stream to write to.
248     */
249    void output(PrintWriter o) {
250        o.print(filename + GENERAL_DELIMITER);
251        o.print(md5sum);
252        o.print(GENERAL_DELIMITER + getGeneralStoreState().toString());
253
254        for (Map.Entry<String, ArchiveStoreState> entry : storeStates.entrySet()) {
255            o.print(ENTRY_COMPONENT_SEPARATOR_STRING + entry.getKey() + GENERAL_DELIMITER + entry.getValue());
256        }
257    }
258
259    /**
260     * Check, if a given bitArchive has a StoreState connected to it.
261     *
262     * @param bitArchive a given bitarchive
263     * @return true, if the given bitArchive has a StoreState connected to it.
264     */
265    boolean hasStoreState(String bitArchive) {
266        return storeStates.containsKey(bitArchive);
267    }
268
269    /**
270     * Set the replyInfo instance variable.
271     *
272     * @param replyInfo The new value for the replyInfo variable.
273     */
274    void setReplyInfo(StoreMessage replyInfo) {
275        if (this.replyInfo != null) {
276            log.warn("Overwriting replyInfo '{}' with '{}'", this.replyInfo, replyInfo);
277        }
278        this.replyInfo = replyInfo;
279    }
280
281}