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.util.ArrayList;
034    import java.util.HashSet;
035    import java.util.Set;
036    
037    import org.hd.d.pg2k.svrCore.ExhibitName;
038    import org.hd.d.pg2k.svrCore.ROByteArray;
039    
040    /**Database of selected files.
041     * Package-visible for direct manipulation by GUI classes.
042     * <p>
043     * Thread-safe.
044     * <p>
045     * A lock can be held on an instance to make compound atomic operations.
046     */
047    final class SelectedFilesDB
048        {
049        /**Ordered random-access list of proposed exhibits to be uploaded; never null.
050         */
051        private final ArrayList<SelectedFileDetails> files = new ArrayList<SelectedFileDetails>();
052    
053        /**Set of all local file names; never null. */
054        private final Set<String> localFilenames = new HashSet<String>();
055    
056        /**Set of all putative short exhibit names; never null.
057         * FIXME: consider converting to Name.ExhibitFull (or ExhibitShort) for smaller memory footprint.
058         */
059        private final Set<String> shortNames = new HashSet<String>();
060    
061        /**Set of all MD5 hashes; never null. */
062        private final Set<ROByteArray> hashesMD5 = new HashSet<ROByteArray>();
063    
064    
065        /**Add item, silently dropping duplicate file entries if possible.
066         * Duplicate entries for a given local file or exhibit (short) name
067         * will be ignored.
068         *
069         * @param newFile  new file to add; must be null
070         */
071        synchronized void add(final SelectedFileDetails newFile)
072            {
073            if(newFile == null) { throw new IllegalArgumentException(); }
074    
075            final String shortNameAsString = newFile.getEsa().getExhibitFullName().getShortName().toString();
076    
077            if(localFilenames.contains(newFile.getLocalFilename()) ||
078               shortNames.contains(shortNameAsString) ||
079               hashesMD5.contains(newFile.getHashMD5()))
080                { return; /* Already present. */ }
081    
082            // Add to all internal tables...
083            localFilenames.add(newFile.getLocalFilename());
084            shortNames.add(shortNameAsString);
085            hashesMD5.add(newFile.getHashMD5());
086            files.add(newFile);
087    
088            changed = true;
089            }
090    
091        /**Remove the indicated item (shuffling any following elements down).
092         * Silently ignores out-of-range requests.
093         */
094        synchronized void remove(final int index)
095            {
096            if((index < 0) || (index >= files.size())) { return; }
097    
098            // Remove from all internal tables.
099            final SelectedFileDetails selectedFileDetails = files.get(index);
100            localFilenames.remove(selectedFileDetails.getLocalFilename());
101            shortNames.remove(selectedFileDetails.getEsa().getExhibitFullName().getShortName().toString());
102            hashesMD5.remove(selectedFileDetails.getHashMD5());
103            files.remove(index);
104    
105            changed = true;
106            }
107    
108        /**Remove the indicated item (shuffling any following elements down).
109         * Silently ignores requests where the items is not present.
110         */
111        synchronized boolean remove(final SelectedFileDetails selectedFileDetails)
112            {
113            if(selectedFileDetails == null) { throw new IllegalArgumentException(); }
114    
115            // Remove from all internal tables.
116            localFilenames.remove(selectedFileDetails.getLocalFilename());
117            shortNames.remove(selectedFileDetails.getEsa().getExhibitFullName().getShortName().toString());
118            hashesMD5.remove(selectedFileDetails.getHashMD5());
119            final boolean removed = files.remove(selectedFileDetails);
120    
121            if(removed) { changed = true; }
122            return(removed);
123            }
124    
125        /**Change the putative exhibit name to another new syntatically-valid one.
126         * Attempting to set it to an invalid or existing (in this DB) name
127         * (ignoring the directory component) will be rejected with an exception.
128         * @param index  entry to be replaced
129         * @param newFullName  new unique (in this DB) valid exhibit name
130         */
131        synchronized void setExhibitName(final int index, final String newFullName)
132            {
133            if(!ExhibitName.validNameSyntax(newFullName)) { throw new IllegalArgumentException(); }
134    
135            final String newShortName = ExhibitName.getFileComponent(newFullName).toString();
136    
137            // Atomically replace old details with new.
138            if(shortNames.contains(newShortName))
139                { throw new IllegalArgumentException("duplicate name"); }
140    
141            try
142                {
143                final SelectedFileDetails oldSFD = files.get(index);
144                final SelectedFileDetails newSFD =
145                    new SelectedFileDetails(oldSFD.getFc(), newFullName, oldSFD.getDescription(), oldSFD.getHashMD5());
146                files.set(index, newSFD);
147                final String oldShortName = oldSFD.getEsa().getExhibitFullName().getShortName().toString();
148                shortNames.remove(oldShortName);
149                shortNames.add(newShortName);
150    
151                changed = true;
152                }
153            catch(final IOException e) { throw new Error(e); } // Should not happen.
154            }
155    
156        /**Change the putative exhibit description.
157         * @param index  entry to be replaced
158         * @param newDescription  new description; never null
159         */
160        synchronized void setDescription(final int index, final String newDescription)
161            {
162            if(newDescription == null) { throw new IllegalArgumentException(); }
163    
164            try
165                {
166                final SelectedFileDetails oldSFD = files.get(index);
167                if(oldSFD.getDescription().equals(newDescription))
168                    { return; /* No change; leave untouched. */ }
169                final SelectedFileDetails newSFD =
170                    new SelectedFileDetails(oldSFD.getFc(), oldSFD.getEsa().getFilePath(), newDescription, oldSFD.getHashMD5());
171                files.set(index, newSFD);
172    
173                changed = true;
174                }
175            catch(final IOException e) { throw new Error(e); } // Should not happen.
176            }
177    
178        /**Clear the whole DB. */
179        synchronized void clear()
180            {
181            localFilenames.clear();
182            shortNames.clear();
183            hashesMD5.clear();
184            files.clear();
185    
186            changed = true;
187            }
188    
189        /**Get number of entries in DB; non-negative. */
190        synchronized int size()
191            {
192            return(files.size());
193            }
194    
195        /**Get given numbered entry by index; never null. */
196        synchronized SelectedFileDetails get(final int index)
197            {
198            return(files.get(index));
199            }
200    
201        /**Returns true if an exhibit in this DB already has this short name.
202         * @param shortName  never null
203         */
204        synchronized boolean shortNameInUse(final String shortName)
205            {
206            return(shortNames.contains(shortName));
207            }
208    
209        /**Returns true if this local name is already in this DB.
210         * @param localFilename  never null
211         */
212        synchronized boolean localFilenameInUse(final String localFilename)
213            {
214            return(localFilenames.contains(localFilename));
215            }
216    
217    
218        /**Flag set true when the table is changed. */
219        private boolean changed;
220    
221        /**Returns the "changed" flag value and clears it.
222         * Can be used to arrange redrawing of a view of this data after an update.
223         */
224        synchronized boolean testAndClearChangedFlag()
225            {
226            final boolean result = changed;
227            changed = false;
228            return(result);
229            }
230        }