001    /*
002     * Created by IntelliJ IDEA.
003     * User: d@hd.org
004     * Date: 24-May-02
005     * Time: 18:24:08
006    
007    Copyright (c) 1996-2012, Damon Hart-Davis
008    All rights reserved.
009    
010    Redistribution and use in source and binary forms, with or without
011    modification, are permitted provided that the following conditions are
012    met:
013    
014      * Redistributions of source code must retain the above copyright
015        notice, this list of conditions and the following disclaimer.
016    
017      * Redistributions in binary form must reproduce the above copyright
018        notice, this list of conditions and the following disclaimer in the
019        documentation and/or other materials provided with the
020        distribution.
021    
022    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
023    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
024    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
025    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
026    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
027    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
028    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
030    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
032    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033    
034     */
035    package org.hd.d.pg2k.svrCore.uploader;
036    
037    import java.io.Serializable;
038    import java.util.ArrayList;
039    import java.util.Arrays;
040    import java.util.Collections;
041    import java.util.Iterator;
042    import java.util.List;
043    import java.util.Map;
044    import java.util.Set;
045    import java.util.StringTokenizer;
046    import java.util.TreeSet;
047    
048    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
049    import org.hd.d.pg2k.svrCore.CoreConsts;
050    import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
051    import org.hd.d.pg2k.svrCore.ExhibitName;
052    import org.hd.d.pg2k.svrCore.GenUtils;
053    import org.hd.d.pg2k.svrCore.LocaleBeanBase;
054    import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
055    
056    /**Base for a JavaBean to handle exhibit data for search or upload.
057     * Unset items are empty strings rather than null for simplest handling
058     * in JSPs.
059     * <p>
060     * This is thread-safe and Serializable so as to be able to be stored in
061     * a servlet session; nothing especially long-lived or sensitive.
062     */
063    public class ExhibitHandlerBeanBase implements Serializable
064        {
065        /**If true, limit selections to existing values in the database.
066         * If false, a wider range of legal values is allowed.
067         * <p>
068         * If true, file types and authors can only be selected from
069         * ones already represented in the production database.
070         */
071        protected final boolean onlyAllowExtant;
072    
073        /**Choose whether new values of some fields are allowed. */
074        protected ExhibitHandlerBeanBase(final boolean _onlyAllowExtant)
075            { onlyAllowExtant = _onlyAllowExtant; }
076    
077        /**Only allow selections from extant values. */
078        protected ExhibitHandlerBeanBase()
079            { this(true); }
080    
081        /**Used in a selector value  or setter to mean all values or no item. */
082        public static final String SETTER_ALL = "-";
083    
084    
085        /**For some unset values, set the most common value.
086         * Can be overridden for values unique to derived classes.
087         * Any overriding method should hold the instance lock to avoid races.
088         * <p>
089         * Should only be called in selected environments where an empty value
090         * is not useful anyway (eg not valid as a wildcard).
091         */
092        public synchronized void setCommonValuesForUnsetFields()
093            {
094            // Nothing to be done if no AEP info...
095            if(aep.aeid.length == 0) { return; }
096    
097            // Try filling in with the most-popular category.
098            if("".equals(getCategory()))
099                {
100                int bestCount = 0;
101                String bestCat = "";
102                final Map<String,Integer> categoryExhibitCounts = aep.getCategoryExhibitCounts();
103                for(final String cat : categoryExhibitCounts.keySet())
104                    {
105                    final int count = categoryExhibitCounts.get(cat);
106                    if(count > bestCount)
107                        {
108                        bestCount = count;
109                        bestCat = cat;
110                        }
111                    }
112                setCategory(bestCat);
113                }
114    
115            // Try filling in with the most-popular suffix/type.
116            if("".equals(getSuffix()))
117                {
118                int bestCount = 0;
119                String bestSuf = "";
120                final Map<String,Integer> suffixExhibitCounts = aep.getDottedExtensionExhibitCounts();
121                for(final String suf : suffixExhibitCounts.keySet())
122                    {
123                    final int count = suffixExhibitCounts.get(suf);
124                    if(count > bestCount)
125                        {
126                        bestCount = count;
127                        bestSuf = suf;
128                        }
129                    }
130                setSuffix(bestSuf);
131                }
132            }
133    
134    
135        /**The AllExhibitProperties data; never null.
136         * Must be set before other property values are set.
137         */
138        private AllExhibitProperties aep = new AllExhibitProperties();
139    
140        /**Suffix selected (starting with ``.''), or "" if none (ie all types OK). */
141        private String suffix = "";
142    
143        /**Author (initials) selected, or "" if none. */
144        private String author = "";
145    
146        /**Category selected, or "" if none. */
147        private String category = "";
148    
149        /**List of attribute words (String), or empty if none; immutable.
150         * We preserve order and repetition unless the dedupAttrs() call is made.
151         * <p>
152         * This item may be replaced but is not mutated in situ
153         * and is immutable
154         * and so can be safely handed out to callers.
155         */
156        private List<String> attributes = Collections.emptyList();
157    
158        /**Set the AllExhibitProperties; ignored if null and must happen before other properties are set. */
159        public synchronized void setAep(final AllExhibitProperties _aep)
160            { if(_aep != null) { aep = _aep; } }
161    
162        /**Get the AllExhibitProperties; never null. */
163        public synchronized AllExhibitProperties getAep() { return(aep); }
164    
165        /**Get all file suffixes user is allowed to select, sorted.
166         * If onlyAllowExtant is true,
167         * only file types in the database can be selected,
168         * else all acceptable types can be selected.
169         */
170        public synchronized String[] getAllSuffixes()
171            {
172            if(!onlyAllowExtant)
173                { return(AllExhibitProperties.getLegalSuffixes()); }
174    
175            final Set<String> s = getAep().getDottedExtensionExhibitCounts().keySet();
176            final Iterator<String> it = s.iterator();
177            final String result[] = new String[s.size()];
178            for(int i = result.length; --i >= 0; )
179            { result[i] = it.next(); }
180            Arrays.sort(result);
181            return(result);
182            }
183    
184        /**Get suffix selected (starting with ``.''), or "" if none (ie all types OK). */
185        public synchronized String getSuffix() { return(suffix); }
186    
187        /**Set suffix selected (starting with ``.''), or "" or SETTER_ALL if none (ie all types OK).
188         * If the suffix is invalid it is ignored.
189         */
190        public synchronized void setSuffix(final String suf)
191            {
192            // Accept request to clear the value.
193            if("".equals(suf) || SETTER_ALL.equals(suf)) { suffix = ""; return; }
194    
195            if((suf == null) || !suf.startsWith(".") || (suf.length() < 2))
196                { return; } // Clearly invalid; ignore.
197    
198            // Quickly check if this is a legal, recognised suffix...
199            final ExhibitMIME.ExhibitTypeParameters type =
200                ExhibitMIME.isValidInputExhibitNameExtension(suf.substring(1));
201            if(type == null)
202                { return; } // Clearly invalid; ignore.
203    
204            // Accept any suffix we know how to handle.
205            suffix = suf;
206    
207    //        // Get valid/legal suffixes,
208    //        // and ignore input unless it matches one of them.
209    //        final String valid[] = getAllSuffixes();
210    //        for(int i = valid.length; --i >= 0; )
211    //            {
212    //            if(valid[i].equals(suf))
213    //                { suffix = suf; return; } // Yes, got it!
214    //            }
215    //        // No, invalid; so ignored.
216            }
217    
218        /**Generates body of suffix HTML select statement (dependent on old value, if any).
219         * Assumed not to require localisation or internationalisation.
220         */
221        public synchronized String makeSuffixSelectBody()
222            {
223            final String legit[] = getAllSuffixes();
224            final String fullList[] = new String[legit.length + 1];
225            fullList[0] = SETTER_ALL; // Meaning ``all''.
226            System.arraycopy(legit, 0, fullList, 1, legit.length);
227    
228            // Now make the labels...
229            final String labels[] = new String[fullList.length];
230            labels[0] = ".*"; // Meaning all.
231    
232            // Make a meaningful set of labels.
233            final Map<String,Integer> dottedExtensionCount = getAep().getDottedExtensionExhibitCounts();
234            for(int i = labels.length; --i > 0; )
235                {
236                labels[i] = fullList[i];
237                final ExhibitMIME.ExhibitTypeParameters type =
238                    ExhibitMIME.isValidInputExhibitNameExtension(fullList[i].substring(1));
239                if(type != null)
240                    {
241                    labels[i] = labels[i] +
242                        " [" + type.mimeType + ": " + type.description + "]";
243                    }
244                final Integer count = (dottedExtensionCount.get(fullList[i]));
245                if(count != null) { labels[i] = labels[i] + " (" + count + ")"; }
246                }
247    
248            return(UploaderUtils.makeSelectBody(fullList, labels, getSuffix()));
249            }
250    
251        /**Get all authors user is allowed to accept, sorted. */
252        public synchronized String[] getAllAuthors()
253            {
254            final Set<String> s = getAep().getAuthorExhibitCounts().keySet();
255            final Iterator<String> it = s.iterator();
256            final String result[] = new String[s.size()];
257            for(int i = result.length; --i >= 0; )
258                { result[i] = it.next(); }
259            Arrays.sort(result);
260            return(result);
261            }
262    
263        /**Returns author (initials) selected, or "" if none. */
264        public synchronized String getAuthor() { return(author); }
265    
266        /**Set author (initials) selected, or "" or SETTER_ALL if none (ie all types OK).
267         * If the author is invalid it is ignored.
268         */
269        public synchronized void setAuthor(final String auth)
270            {
271            if((auth == null) || (auth.length() == 0))
272                { return; } // Clearly invalid; ignore.
273    
274            if("".equals(auth) || SETTER_ALL.equals(auth)) { author = ""; return; }
275    
276            // Get valid/legal authors,
277            // and ignore input unless it matches one of them.
278            final String valid[] = getAllAuthors();
279            for(int i = valid.length; --i >= 0; )
280                {
281                if(valid[i].equals(auth))
282                    { author = auth; return; } // Yes, got it!
283                }
284            // No, invalid; so ignored.
285            }
286    
287        /**Generates body of author HTML select statement (dependent on old value, if any).
288         * Assumed not to require localisation or internationalisation.
289         */
290        public synchronized String makeAuthorSelectBody()
291            {
292            final String legit[] = getAllAuthors();
293            final String fullList[] = new String[legit.length + 1];
294            fullList[0] = SETTER_ALL; // Meaning ``all''.
295            System.arraycopy(legit, 0, fullList, 1, legit.length);
296    
297            // Now make the labels...
298            final String labels[] = new String[fullList.length];
299            labels[0] = "*"; // Meaning all.
300    
301            // Make a meaningful set of labels.
302            final Map<String,Integer> authorCount = getAep().getAuthorExhibitCounts();
303            for(int i = labels.length; --i > 0; )
304                {
305                labels[i] = fullList[i];
306                final Integer count = (authorCount.get(fullList[i]));
307                if(count != null) { labels[i] = labels[i] + " (" + count + ")"; }
308                }
309    
310            return(UploaderUtils.makeSelectBody(fullList, labels, getAuthor()));
311            }
312    
313        /**Get all categories user is allowed to accept, sorted. */
314        public synchronized String[] getAllCategories()
315            {
316            final Set<String> s = getAep().getCategoryExhibitCounts().keySet();
317            final Iterator<String> it = s.iterator();
318            final String result[] = new String[s.size()];
319            for(int i = result.length; --i >= 0; )
320                { result[i] = it.next(); }
321            Arrays.sort(result, String.CASE_INSENSITIVE_ORDER);
322            return(result);
323            }
324    
325        /**Returns category selected, or "" if none. */
326        public synchronized String getCategory() { return(category); }
327    
328        /**Set category selected, or "" or SETTER_ALL if none (ie all types OK).
329         * If the category is invalid it is ignored.
330         */
331        public synchronized void setCategory(final String cat)
332            {
333            // Accept request to clear the value.
334            if("".equals(cat) || SETTER_ALL.equals(cat)) { category = ""; return; }
335    
336            if((cat == null) || (cat.length() == 0))
337                { return; } // Clearly invalid; ignore.
338    
339            // Ignore putative category name with invalid syntax.
340            if(!ExhibitName.validNameInitialComponentSyntax(cat)) { return; }
341    
342            // Get valid/legal categories,
343            // and ignore input unless it matches one of them.
344            if(getAep().getCategoryExhibitCounts().keySet().contains(cat))
345                { category = cat; return; } // Yes, got it!
346    
347            // No, invalid; so ignored.
348            return;
349            }
350    
351        /**Generates body of category HTML select statement (dependent on old value, if any).
352         * This version does not use a LocaleBean.
353         * <p>
354         * Assumed not to require localisation or internationalisation.
355         * <p>
356         * We munge the categories for display by stripping any non-alphanumerics
357         * (actually just ``-'' and ``_'')
358         * and converting everything to lower-case.
359         */
360        public String makeCategorySelectBody()
361            { return(makeCategorySelectBody()); }
362    
363        /**Generates body of category HTML select statement (dependent on old value, if any).
364         * Assumed not to require localisation or internationalisation.
365         * <p>
366         * We munge the categories for display by stripping any non-alphanumerics
367         * (actually just ``-'' and ``_'')
368         * and converting everything to lower-case.
369         *
370         * @param localeBean  if non-null, used to lookup and localise
371         *     attribute descriptions
372         */
373        public synchronized String makeCategorySelectBody(final LocaleBeanBase localeBean)
374            {
375            final String legit[] = getAllCategories();
376            final String fullList[] = new String[legit.length + 1];
377            fullList[0] = SETTER_ALL; // Meaning ``all''.
378            System.arraycopy(legit, 0, fullList, 1, legit.length);
379    
380            // Now make the labels...
381            final String labels[] = new String[fullList.length];
382            labels[0] = "*"; // Meaning all.
383    
384            // Make a meaningful set of labels.
385            final Map<String,Integer> categoryCount = getAep().getCategoryExhibitCounts();
386            for(int i = labels.length; --i > 0; )
387                {
388                final String sectionDir = fullList[i];
389                // Munge into more acceptable form.
390                // Might look this up (convert it) in some more
391                // i18n way later.
392                final String normalisedDirname = sectionDir.toLowerCase().replace('-', ' ').replace('_', ' ').trim();
393                labels[i] = normalisedDirname;
394    
395                // Insert a localised description if possible.
396                if(localeBean != null)
397                    {
398                    // We'll see if there is a hand-crafted title for this section.
399                    final String msg = GenUtils.computeSectionTitle(aep, sectionDir, localeBean);
400                    // We show the localised version if it exists and isn't just in a different case.
401                    if(!msg.equals(sectionDir) &&
402                        !msg.equalsIgnoreCase(normalisedDirname))
403                        {
404                        labels[i] = labels[i] + " [" + msg + "]";
405                        }
406                    }
407    
408    
409                final Integer count = (categoryCount.get(sectionDir));
410                if(count != null) { labels[i] = labels[i] + " (" + count + ")"; }
411                }
412    
413            return(UploaderUtils.makeSelectBody(fullList, labels, getCategory()));
414            }
415    
416        /**Generates body of attribute HTML select statement (dependent on old value, if any).
417         * This version does not take a localeBean.
418         * <p>
419         * Assumed not to require localisation or internationalisation.
420         * <p>
421         * We assume that all attribute words are safe for use as values.
422         * <p>
423         * We explicitly pass in the attribute word we want selected,
424         * or SETTER_ALL (or null) to select the default.
425         *
426         * @param inUse  if true, only attributes currently in use in the Gallery
427         *     will be includes, else all legal attributes will be included
428         * @param selectedAttr  if non-null and not SETTER_ALL and
429         *     and attribute in the final select list,
430         *     causes that item to be selected by default
431         */
432        public String makeAttributeSelectBody(final boolean inUse,
433                                              final String selectedAttr)
434            { return(makeAttributeSelectBody(inUse, selectedAttr, null)); }
435    
436        /**Generates body of attribute HTML select statement (dependent on old value, if any).
437         * Assumed not to require localisation or internationalisation.
438         * <p>
439         * We assume that all attribute words are safe for use as values.
440         * <p>
441         * We explicitly pass in the attribute word we want selected,
442         * or SETTER_ALL (or null) to select the default.
443         *
444         * @param inUse  if true, only attributes currently in use in the Gallery
445         *     will be includes, else all legal attributes will be included
446         * @param selectedAttr  if non-null and not SETTER_ALL and
447         *     and attribute in the final select list,
448         *     causes that item to be selected by default
449         * @param localeBean  if non-null, used to lookup and localise
450         *     attribute descriptions
451         */
452        public synchronized String makeAttributeSelectBody(final boolean inUse,
453                                                           final String selectedAttr,
454                                                           final LocaleBeanBase localeBean)
455            {
456            final Map<String,Integer> eBA = getAep().getExhibitCountsByAttribute();
457            final List<String> attrWords = new ArrayList<String>();
458            // Iterate through the attribute words in sorted order.
459            for(final Iterator<String> it = (new TreeSet<String>(eBA.keySet())).iterator(); it.hasNext(); )
460                {
461                final String attrWord = it.next();
462                final Integer count = eBA.get(attrWord);
463    
464                // Skip attributes that are not in use if we care about that
465                if(inUse && (count == 0)) { continue; }
466    
467                attrWords.add(attrWord);
468                }
469            final String legit[] = new String[attrWords.size()];
470            attrWords.toArray(legit);
471    
472            final String fullList[] = new String[legit.length + 1];
473            fullList[0] = SETTER_ALL; // Meaning ``all''.
474            System.arraycopy(legit, 0, fullList, 1, legit.length);
475    
476            // Now make the labels...
477            final String labels[] = new String[fullList.length];
478            labels[0] = "*"; // Meaning all.
479    
480            // Make a meaningful set of labels.
481            for(int i = labels.length; --i > 0; )
482                {
483                // Munge into more acceptable form.
484                // Might look this up (convert it) in some more
485                // i18n way later.
486                labels[i] = fullList[i];
487    
488                // Insert a localised description if possible.
489                if(localeBean != null)
490                    {
491                    // We'll see if there is descriptive text for this attribute.
492                    final String i18nName = CoreConsts.ATTR_I18N_DESC_PREFIX + fullList[i];
493                    final String msg = localeBean.getLocalisedMessage(i18nName);
494                    if(!msg.equals(i18nName))
495                        {
496                        labels[i] = labels[i] + " [" + msg + "]";
497                        }
498                    }
499    
500                final Map<String,Integer> attrCount = getAep().getExhibitCountsByAttribute();
501                final int count = attrCount.get(fullList[i]);
502                labels[i] = labels[i] + " (" + count + ")";
503                }
504    
505            return(UploaderUtils.makeSelectBody(fullList, labels, selectedAttr));
506            }
507    
508        /**Get all acceptable attribute words that are in use as a single String for display to the user.
509         * This returns the list of attribute words in use in the Gallery exhibits if inUse is true,
510         * else it returns the list of attribute words not currently in use though legal.
511         * <p>
512         * The result is a space-separated sorted list.
513         * <p>
514         * This returns the empty string if no list is available.
515         */
516        public synchronized String getInUseAttrWordListAsString(final boolean inUse)
517            {
518            final Map<String,Integer> eBA = getAep().getExhibitCountsByAttribute();
519            final StringBuilder result = new StringBuilder();
520            // Iterate through the attribute words in sorted order.
521            for(final Iterator<String> it = (new TreeSet<String>(eBA.keySet())).iterator(); it.hasNext(); )
522                {
523                final String attrWord = it.next();
524                final Integer count = eBA.get(attrWord);
525    
526                // Skip attributes that don't match our filter.
527                if(inUse == (count == 0)) { continue; }
528    
529                if(result.length() != 0) { result.append(' '); }
530                result.append(attrWord);
531                }
532            return(result.toString());
533            }
534    
535        /**Get list of all acceptable attribute words as single String for display to user.
536         * This displays the legal attribute word set in sorted order,
537         * space-separated, for display to a user (eg in an HTML page).
538         * <p>
539         * This returns the empty string if no list is available.
540         */
541        public synchronized String getLegalAttrWordListAsString()
542            {
543            final Set<String> words = ExhibitAttrUtils.getAttrWords().getAttrWordsSortedSet();
544            final StringBuilder result = new StringBuilder(words.size() * 8);
545            for(final Iterator<String> it = words.iterator(); it.hasNext(); )
546                {
547                if(result.length() != 0) { result.append(' '); }
548                result.append(it.next());
549                }
550            return(result.toString());
551            }
552    
553        /**Eliminates duplicate attribute words and sorts into lexical order.
554         * Must only be called once the attributes have been set.
555         * <p>
556         * Assumes all entries are valid ie are non-null, legal attribute words.
557         * <p>
558         * Idempotent.
559         */
560        public synchronized void dedupAttrs()
561           {
562           attributes = Collections.unmodifiableList(new ArrayList<String>(new TreeSet<String>(attributes)));
563           }
564    
565        /**Get attribute words as immutable List; can be zero-length but never null.
566         * All words are valid attribute words (at least when they were set)
567         * and the trailing one will not be a number.
568         */
569        public synchronized List<String> getAttributeWordsAsList()
570            {
571            return(attributes); // Must be immutable.
572            }
573    
574        /**Get attribute words as a valid string of hyphen-separated words; can be "" but never null.
575         * All words are valid attribute words (at least when they were set)
576         * and the trailing one will not be a number.
577         */
578        public synchronized String getAttributeWords()
579            {
580            final StringBuilder sb = new StringBuilder();
581            for(final Iterator<String> it = attributes.iterator(); it.hasNext(); )
582                {
583                final String s = it.next();
584                if(sb.length() != 0) { sb.append(ExhibitName.WORD_SEP); }
585                sb.append(s);
586                }
587            return(sb.toString());
588            }
589    
590        /**Set attribute words; any invalid items will be silently discarded.
591         * Any existing attribute words held by this bean are removed first.
592         * <p>
593         * The argument may not be null nor contain nulls.
594         */
595        public synchronized void setAttributeWords(final String words[])
596            {
597            if(words == null) { throw new IllegalArgumentException(); }
598            setAttributeWords(Arrays.asList(words));
599            }
600    
601        /**Set attribute words; any invalid items will be silently discarded.
602         * Any existing attribute words held by this bean are removed first.
603         * <p>
604         * The argument may not be null nor contain nulls.
605         */
606        public synchronized void setAttributeWords(final List<String> words)
607            {
608            if(words == null) { throw new IllegalArgumentException(); }
609    
610            // Deal with common case of no attributes.
611            if(words.size() == 0)
612                {
613                attributes = Collections.emptyList();
614                return;
615                }
616    
617            final ArrayList<String> newAttrs = new ArrayList<String>(words.size());
618    
619            // Get set of valid attribute words.
620            final Set<String> validAttrWords = ExhibitAttrUtils.getAttrWords().getAttrWordsSortedSet();
621    
622            for(final String s : words)
623                {
624                if(s == null) { continue; } // Definitely invalid!
625                if(validAttrWords.contains(s)) { newAttrs.add(s); }
626                }
627            newAttrs.trimToSize(); // Conserve memory...
628    
629            attributes = Collections.unmodifiableList(newAttrs);
630            }
631    
632        /**Set attribute words; valid string of hyphen-separated words; can be "" or null.
633         * Use null or "" or SETTER_ALL to clear this field.
634         * <p>
635         * All words must be attribute words and the trailing one may not be
636         * a number.
637         * <p>
638         * Any items that are not valid attribute words will just be silently
639         * deleted.
640         * <p>
641         * Any non-valid word characters (eg including spaces) will be converted
642         * to hyphens.
643         * <p>
644         * This tries very hard to keep as much of its input as possible.
645         */
646        public void setAttributeWords(final String attributeWords)
647            {
648            if((attributeWords == null) || (attributeWords.length() == 0))
649                {
650                attributes = Collections.emptyList();
651                return;
652                }
653    
654            // Work through the input treating space and dash as separators.
655            final StringTokenizer st = new StringTokenizer(attributeWords,
656                                                    ExhibitName.WORD_SEPS + ' ');
657            final String[] sa = new String[st.countTokens()];
658            for(int i = 0; i < sa.length; ++i)
659                { sa[i] = st.nextToken(); }
660            setAttributeWords(sa);
661            }
662    
663        /**Unique Serialisation class ID generated by http://random.hd.org/. */
664        private static final long serialVersionUID = 8025048065146981171L;
665        }