001 /*
002 Copyright (c) 1996-2012, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.clApp.uploader;
031
032 import java.io.IOException;
033 import java.io.InputStream;
034 import java.util.Collections;
035 import java.util.Map;
036 import java.util.WeakHashMap;
037
038 import javax.jnlp.FileContents;
039
040 import org.hd.d.pg2k.svrCore.AccessionData;
041 import org.hd.d.pg2k.svrCore.AllExhibitProperties;
042 import org.hd.d.pg2k.svrCore.ExhibitName;
043 import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
044 import org.hd.d.pg2k.svrCore.Name;
045 import org.hd.d.pg2k.svrCore.ROByteArray;
046 import org.hd.d.pg2k.svrCore.TextUtils;
047 import org.hd.d.pg2k.svrCore.Tuple;
048
049 /**Immutable class to hold details of one file selected.
050 * This is used during the selection/naming and upload phase.
051 * <p>
052 * All package-visible for access from the GUI classes.
053 * <p>
054 * Thread-safe.
055 * <p>
056 * The get/set status methods are dynamic and do not operate on the instance;
057 * the class holds a weak-ref map from selected file details to an optional
058 * String status/error, which "shadows" any computed error.
059 */
060 final class SelectedFileDetails
061 {
062 /**Column for the (potentially mutable) proposed exhibit name. */
063 public static final int COLNUM_EXHIBIT_NAME = 0;
064
065 /**Column for the (immutable) local filename. */
066 public static final int COLNUM_LOCAL_FILE_NAME = 1;
067
068 /**Column for the (immutable) length in bytes. */
069 public static final int COLNUM_DESCRIPTION = 2;
070
071 /**Column for the (immutable) length in bytes. */
072 public static final int COLNUM_BYTESLEN = 3;
073
074 /**Column for the (dynamic) status string. */
075 public static final int COLNUM_STATUS = 4;
076
077
078 /**Column names for table, in order. */
079 private static final String[] fileLoadTableColNames = {
080 "Proposed Exhibit Name", "Filename", "Description", "Bytes", "Status/Error"
081 };
082
083 /**Return the number of columns; fixed for this model. */
084 static int getColumnCount() { return(fileLoadTableColNames.length); }
085
086 /**Return a column name, indexed from 0. */
087 static String getColumnName(final int col) { return(fileLoadTableColNames[col]); }
088
089 /**Get the value at the given column; null if none. */
090 Object getValueAt(final UploaderLogic l, final int col)
091 {
092 switch(col)
093 {
094 case COLNUM_LOCAL_FILE_NAME:
095 {
096 try { return(fc.getName()); }
097 catch(final IOException e) { throw new Error(e); } // Should not happen.
098 }
099
100 case COLNUM_EXHIBIT_NAME:
101 { return(esa.getCharSequence().toString()); }
102
103 case COLNUM_BYTESLEN:
104 { return(esa.length); }
105
106 case COLNUM_DESCRIPTION:
107 { return(getDescription()); }
108
109 case COLNUM_STATUS:
110 { return(getStatus(l)); }
111
112 default: return(null); // Unexpected/unsupported column.
113 }
114 }
115
116 /**Construct a new instance.
117 * @param userFile underlying (validated) user file; never null
118 * @param putativeExhibitName syntatically-valid proposed exhibit name;
119 * never null
120 * @param description description text; may be "" but not null
121 * @param hashMD5 MD5 hash of exhibit content;
122 * if null then it is computed here
123 */
124 SelectedFileDetails(final FileContents userFile,
125 final String putativeExhibitName,
126 final String description,
127 ROByteArray hashMD5)
128 throws IOException
129 {
130 if((userFile == null) ||
131 !ExhibitName.validNameSyntax(putativeExhibitName) ||
132 (description == null))
133 { throw new IllegalArgumentException(); }
134 // TODO: more checking of args and file data?
135 fc = userFile;
136 localFilename = fc.getName();
137 if((localFilename == null) || (localFilename.length() == 0))
138 { throw new IllegalArgumentException(); }
139 esa = new ExhibitStaticAttr(putativeExhibitName, userFile.getLength(), System.currentTimeMillis());
140 this.description = description;
141
142 // If need be, compute the MD5 hash here.
143 if(hashMD5 == null)
144 {
145 final InputStream is = fc.getInputStream();
146 try
147 {
148 final Tuple.Pair<Integer,ROByteArray> hashes = AccessionData.computeFullFileHashes(is);
149 hashMD5 = hashes.second;
150 }
151 finally { is.close(); }
152 }
153
154 this.hashMD5 = hashMD5;
155 }
156
157 /**Get the FileContents; never null. */
158 FileContents getFc() { return(fc); }
159
160 /**Get the local filename; never null (nor ""). */
161 String getLocalFilename() { return(localFilename); }
162
163 /**Get the static exhibit details; never null. */
164 ExhibitStaticAttr getEsa() { return(esa); }
165
166 /**Get the description; never null but may be "". */
167 String getDescription() { return(description); }
168
169 /**Get the MD5 hash; never null. */
170 ROByteArray getHashMD5() { return(hashMD5); }
171
172
173 /**Set a status note for this item; use "" or null to clear.
174 * Does not change the instance itself,
175 * but stores any note in a private global map.
176 */
177 void setStatus(final String n)
178 {
179 if((n == null) || (n.length() == 0))
180 { notes.remove(this); }
181 else
182 { notes.put(this, n); }
183 }
184
185 /**Get status/error: if this returns null all is OK, else the error is described.
186 * The routine must be quick to run, eg it cannot recompute a file hash.
187 * <p>
188 * If a note has been set it takes precedence over any computed value.
189 *
190 * @return null if all seems OK, non-null error message otherwise
191 */
192 String getStatus(final UploaderLogic l)
193 {
194 if(l == null) { throw new IllegalArgumentException(); }
195
196 final String note = notes.get(this);
197 if(note != null) { return(note); }
198
199 if(esa.length < 1)
200 { return("file is zero-length"); }
201
202 final int websvr_max_ex_bytes = l.getGenProps().getWEBSVR_MAX_EX_BYTES();
203 if(esa.length > websvr_max_ex_bytes)
204 { return("file is too long: limit (bytes): " + websvr_max_ex_bytes); }
205
206 final AllExhibitProperties aep = l.getAep();
207
208 // Check for hash clash with uploaded exhibit.
209 if(l.checkIfAlreadyUploadedByHashMD5(hashMD5)) { return("identical to recently-uploaded exhibit"); }
210
211 // Check for name clash with extant Gallery exhibit.
212 final Name.ExhibitFull clashingFullName = aep.aeid.getFullName(esa.getExhibitFullName().getShortName());
213 // assert((clashingFullName == null) || ExhibitName.validNameSyntax(clashingFullName));
214 if(clashingFullName != null) { return("name clashes with extant exhibit: `" + clashingFullName + "'"); }
215
216 // Check for hash clash with extant Gallery exhibit.
217 final Name.ExhibitFull clashingHashName = aep.getHashMD5ToName().get(hashMD5);
218 // assert((clashingHashName == null) || ExhibitName.validNameSyntax(clashingHashName));
219 if(clashingHashName != null) { return("identical content to extant exhibit: `" + clashingHashName + "'"); }
220
221 try
222 {
223 if(!fc.canRead()) { return("cannot read file"); }
224 if(esa.length != fc.getLength()) { return("file length changed"); }
225 }
226 catch(final Exception e)
227 {
228 e.printStackTrace();
229 return("Problem inspecting file: " + e.getMessage());
230 }
231
232 return(null); // All OK.
233 }
234
235 /**Local filename; never null (nor ""). */
236 private final String localFilename;
237
238 /**File on user side (should already be checked for validity); never null. */
239 private final FileContents fc;
240
241 /**Proposed upload name/details (valid unique new exhibit name); never null.
242 * Volatile for thread-safety without locks.
243 */
244 private final ExhibitStaticAttr esa;
245
246 /**Description; never null though may be "". */
247 private final String description;
248
249 /**MD5 hash of exhibit content; never null. */
250 private final ROByteArray hashMD5;
251
252
253 /**Equality is based on the (monocased) local filename and exhibit name.
254 * Both names are used to help avoid surprising ambiguities.
255 * <p>
256 * The MD5 hash is considered a feature of the content
257 * rather than of this object,
258 * and is not used in the object hash or equality.
259 */
260 @Override
261 public boolean equals(final Object obj)
262 {
263 if(this == obj) { return(true); }
264 if(!(obj instanceof SelectedFileDetails)) { return(false); }
265 final SelectedFileDetails other = (SelectedFileDetails) obj;
266 return(localFilename.equalsIgnoreCase(other.localFilename) &&
267 TextUtils.contentEquals(esa.getCharSequence(), other.esa.getCharSequence()));
268 }
269
270 /**The cached instance hash value; the hash may be expensive to compute. */
271 private transient int hash;
272
273 /**The hash code is based on the (monocased) local filename and exhibit name.
274 * Both names are used to help avoid surprising ambiguities.
275 * <p>
276 * The MD5 hash is considered a feature of the content
277 * rather than of this object,
278 * and is not used in the object hash or equality.
279 */
280 @Override
281 public int hashCode()
282 {
283 int h = hash;
284 if(h == 0)
285 {
286 // Compute and cache.
287 hash = h = localFilename.toLowerCase().hashCode() ^ (37 * esa.getExhibitFullName().hashCode());
288 }
289 return(h);
290 }
291
292 /**Global "error/status note" on instances of this class.
293 * Uses a WeakHashMap so as not to retain a note unnecessarily
294 * once the associated file has been dealt with and instance GCed.
295 * <p>
296 * Thread-safe.
297 */
298 private static final Map<SelectedFileDetails,String> notes =
299 Collections.synchronizedMap(new WeakHashMap<SelectedFileDetails, String>());
300 }