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 }