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        }