View Javadoc

1   package org.paneris.bibliomania;
2   
3   
4   import java.io.BufferedOutputStream;
5   import java.io.BufferedReader;
6   import java.io.File;
7   import java.io.FileOutputStream;
8   import java.io.FileReader;
9   import java.io.IOException;
10  import java.io.OutputStream;
11  import java.io.Reader;
12  import java.io.StringReader;
13  import java.net.InetAddress;
14  import java.sql.PreparedStatement;
15  import java.sql.ResultSet;
16  import java.sql.Timestamp;
17  import java.util.Enumeration;
18  import java.util.Hashtable;
19  import java.util.List;
20  import java.util.Locale;
21  import java.util.Properties;
22  import java.util.Vector;
23  
24  import org.melati.Melati;
25  import org.melati.MelatiConfig;
26  import org.melati.poem.AccessToken;
27  import org.melati.poem.Capability;
28  import org.melati.poem.Group;
29  import org.melati.poem.NoSuchRowPoemException;
30  import org.melati.poem.PoemTask;
31  import org.melati.poem.PoemThread;
32  import org.melati.poem.PreparedStatementFactory;
33  import org.melati.poem.Setting;
34  import org.melati.poem.TableInfo;
35  import org.melati.template.webmacro.MelatiFastWriter;
36  import org.melati.util.Email;
37  import org.melati.util.FileUtils;
38  import org.melati.util.IoUtils;
39  import org.melati.util.MelatiRuntimeException;
40  import org.melati.util.UnexpectedExceptionException;
41  import org.paneris.bibliomania.fti.FTIException;
42  import org.paneris.bibliomania.fti.IndexOther;
43  import org.paneris.bibliomania.fti.Library;
44  import org.paneris.bibliomania.fti.Text;
45  import org.paneris.bibliomania.generated.BibliomaniaDatabaseBase;
46  import org.paneris.bibliomania.metasearch.BookshopBackend;
47  import org.paneris.bibliomania.metasearch.BookshopBackendFactory;
48  import org.paneris.bibliomania.pagination.Pagination;
49  import org.webmacro.Context;
50  import org.webmacro.InitException;
51  import org.webmacro.WM;
52  import org.webmacro.WebMacro;
53  import org.webmacro.WebMacroException;
54  import org.webmacro.engine.FileTemplate;
55  import org.webmacro.engine.StreamTemplate;
56  
57  /**
58   * Melati POEM generated, programmer modifiable stub.
59   */
60  public class BibliomaniaDatabase extends BibliomaniaDatabaseBase
61                              implements BibliomaniaDatabaseTables {
62    // programmer's domain-specific code here
63     private IndexOther fti = null, infoFTI = null;
64     private Properties ftiConfig;
65     private Pagination pagination = null;
66  
67     public static final int defaultDefaultSearchHitsPerText = 5,
68       defaultSearchHitsPerPage = 20,
69       defaultBookStockingsCheckIntervalDays = 7,
70       defaultBibBookBookshopSearchTimeoutSeconds = 120,
71       defaultBookStockingsCheckConcurrentMax = 2,
72       defaultBookStockingsOutputStartOffset = 25,
73       defaultBookStockingsCacheSizeMax = 10000;
74  
75     public static final String defaultPasswordReminderMessage =
76       "defaultPasswordReminderMessage.wm",
77       defaultPasswordReminderFrom = "help",
78       defaultOrderEmailFrom = "orders@bibliomania.com",
79       defaultConfirmationEmailFrom = "customercare@bibliomania.com",
80       defaultOrderEmailTo = "purchases@bibliomania.com",
81       defaultStratusEmailTo = "info@houseofstratus.com",
82       defaultContentEncoding = "ISO-8859-1",
83       defaultUploadDir = "/usr/local/bibliomania/books/",
84       defaultUploadURL = "/",
85       defaultDomainUrl = "www.bibliomania.com";
86     //  defaultContentEncoding = "UTF8";
87  
88     private Setting workspaceDir,
89       contentRootDir,
90       staticRootURL,
91       contentStaticRootURL,
92       cacheRootURL,
93       homepageURL,
94       orderEmailFrom,
95       confirmationEmailFrom,
96       orderEmailTo,
97       stratusEmailTo,
98       defaultSearchHitsPerText,
99       searchHitsPerPage,
100      paginationTexHeader,
101      bookStockingsCheckIntervalDays,
102      bookStockingsCheckConcurrentMax,
103      bookStockingsInBackground,
104      bibBookBookshopSearchTimeoutSeconds,
105      smtpServer,
106      passwordReminderMessage,
107      passwordReminderFrom,
108      infoFTIDir,
109      bannerURLPath,
110      bookStockingsOutputStartOffset,
111      bookStockingsCacheDir,
112      bookStockingsCacheSizeMax,
113      contentEncoding,
114      uploadDir,
115      uploadURL;
116 
117    private Currency UKCurrency;
118    //private Currency USCurrency;
119 
120    private Group registeredUserGroup;
121 
122    private Capability registeredUserCapability,
123      contentModificationCapability,
124      contentAdditionCapability;
125 
126    private User templateRegisterUser;
127 
128    private Bookshop[] bookshops = null;
129 
130    private SectionGroup readSectionGroup,
131      studySectionGroup,
132      researchSectionGroup,
133      shopSectionGroup,
134      searchSectionGroup;
135 
136    private boolean bookStockingsInBackgroundAppropriate, openBerkeleyDBs;
137 
138    private Section drama;
139    private Author shakespeare;
140    private Book macbeth;
141    
142    public BibliomaniaDatabase() {
143      this(false);
144    }
145 
146    public BibliomaniaDatabase(boolean bookStockingsInBackgroundAppropriate) {
147      this(bookStockingsInBackgroundAppropriate, true);  // Turn on BerkeleyDB
148    }
149 
150    public BibliomaniaDatabase(
151        boolean bookStockingsInBackgroundAppropriate,
152        boolean openAuxDBs) {
153      this(bookStockingsInBackgroundAppropriate, openAuxDBs, null);
154    }
155 
156    public BibliomaniaDatabase(
157        boolean bookStockingsInBackgroundAppropriate,
158        boolean openAuxDBs,
159        Properties ftiConfig) {
160      this.bookStockingsInBackgroundAppropriate =
161        bookStockingsInBackgroundAppropriate;
162      this.openBerkeleyDBs = openAuxDBs;
163      this.ftiConfig = ftiConfig;
164    }
165 
166    public boolean debug() {
167      return false;
168    }
169 
170    public Capability getCanAdminister() {
171      return administerCapability();
172    }
173 
174    public void connect(
175      String name,
176      String dbmsclass,
177      String url,
178      String username,
179      String password,
180      int maxConnections) {
181      super.connect(name, dbmsclass, url, username, password, maxConnections);
182 
183      inSession(AccessToken.root, new PoemTask() {
184        public void run() {
185          String root = "/usr/local/bibliomania";
186          ((UserTable)getUserTable()).setBoardManager(
187              ((UserTable)getUserTable()).ensure(
188                  "_manager_","FIXME",
189                  "board_manager@paneris.co.uk",
190                  "Bibliomania Board Manager"));
191 
192 
193          readSectionGroup =
194            getSectionGroupTable().ensure(
195              "#ff9900",
196              "read",
197              "read.gif",
198              "readg.gif",
199              "1",
200              "read",
201              "",
202              false,
203              null);
204 
205          studySectionGroup =
206            getSectionGroupTable().ensure(
207              "#9999ff",
208              "study",
209              "study.gif",
210              "studyg.gif",
211              "4",
212              "study",
213              "",
214              false,
215              null);
216 
217          researchSectionGroup =
218            getSectionGroupTable().ensure(
219              "#cc9966",
220              "research",
221              "researchb.gif",
222              "researcha.gif",
223              "6",
224              "research",
225              "",
226              false,
227              null);
228          /*
229          shopSectionGroup = 
230            getSectionGroupTable().ensure(
231              "#00cc99", 
232              "shop", 
233              "shop.gif", 
234              "shopg.gif", 
235              "3",
236              "shop", 
237              "<a href=/b/org.paneris.melati.shopping.Trolley/bibliomania/View target=main>Your Basket</a>", 
238              false, 
239              null);
240          */
241          searchSectionGroup =
242            getSectionGroupTable().ensure(
243              "#ff0000",
244              "search",
245              "search.gif",
246              "searchg.gif",
247              "2",
248              "search",
249              "",
250              true,
251              "/b/Search");
252 
253          
254          drama = getSectionTable().ensure("Drama", readSectionGroup);
255          shakespeare = getAuthorTable().ensure("William Shakespeare", drama);
256          macbeth = getBookTable().ensure("Macbeth", shakespeare, drama);
257          
258          registeredUserGroup = getGroupTable().ensure("Registered users");
259          registeredUserCapability =
260            getCapabilityTable().ensure("Access study material");
261          contentModificationCapability =
262            getCapabilityTable().ensure("Edit content database");
263          contentAdditionCapability =
264            getCapabilityTable().ensure("Add to content database");
265          
266          getGroupCapabilityTable().ensure(
267              getGroupTable().administratorsGroup(),
268            contentModificationCapability);
269              
270          getGroupCapabilityTable().ensure(
271              getGroupTable().administratorsGroup(),
272              contentAdditionCapability);
273 
274          
275          templateRegisterUser =
276            (User)getUserTable().getLoginColumn().firstWhereEq(
277              "_registerTemplate_");
278 
279          if (templateRegisterUser == null) {
280            templateRegisterUser = (User)getUserTable().newPersistent();
281            templateRegisterUser.setLogin("_registerTemplate_");
282            templateRegisterUser.setPassword("0.804461358580738");
283            templateRegisterUser.setEmail("none@bogus");
284            templateRegisterUser.setFulltimeeducation(false);
285            templateRegisterUser.setWantspam(false);
286            templateRegisterUser.setWantemailalerts(false);
287            templateRegisterUser.setDodgeyemail(false);
288            templateRegisterUser.setName(
289              "<Template for newly registering users>");
290            getUserTable().create(templateRegisterUser);
291          }
292 
293          workspaceDir =
294            getSettingTable().ensure(
295              "WorkspaceDir",
296              root + "/workspace",
297              "Workspace directory",
298              "Where the system should store its working files");
299 
300          infoFTIDir =
301            getSettingTable().ensure(
302              "InfoFTIDir",
303              "/infoFTI",
304              "Info-FTI directory",
305              "Where the system should store the info-page index, relative "
306                + "to WorkspaceDir");
307 
308          contentRootDir =
309            getSettingTable().ensure(
310              "ContentRootDir",
311              root + "/books",
312              "Content root directory",
313              "Where (in the local filesystem) the system should look for "
314                + "the book content files");
315 
316          staticRootURL =
317            getSettingTable().ensure(
318              "StaticRootURL",
319              "/bibliomania-static",
320              "Static root URL",
321              "Where the browser can find the Bibliomania chrome, "
322                + "relative to the server root URL");
323 
324          contentStaticRootURL =
325            getSettingTable().ensure(
326              "ContentStaticRootURL",
327              "/bibliomania-content-static",
328              "Content static root URL",
329              "Where the browser can find the Bibliomania static content, "
330                + "relative to the server root URL");
331 
332          bannerURLPath =
333            getSettingTable().ensure(
334              "BannerURLPath",
335              "/bibliomania-content-static",
336              "Banner URL Path",
337              "Where the browser can find the Bibliomania static content, "
338                + "relative to the server root URL");
339 
340          cacheRootURL =
341            getSettingTable().ensure(
342              "CacheRootURL",
343              "",
344              "Cache root URL",
345              "Where the web server should look for the banner ad, "
346                + "relative to the server root URL");
347 
348          homepageURL =
349            getSettingTable().ensure(
350              "HomepageURL",
351              "http://www.bibliomania.com",
352              "Home page URL",
353              "A full URL for the front page of bibliomania");
354 
355          defaultSearchHitsPerText =
356            getSettingTable().ensure(
357              "DefaultSearchHitsPerText",
358              defaultDefaultSearchHitsPerText,
359              "Default search hits per text",
360              "The default number of search hits to return from each text");
361 
362          searchHitsPerPage =
363            getSettingTable().ensure(
364              "SearchHitsPerPage",
365              defaultSearchHitsPerPage,
366              "Search hits per page",
367              "The number of search hits to return on each results page");
368 
369          bookStockingsCheckIntervalDays =
370            getSettingTable().ensure(
371              "StockingsCheckIntervalDays",
372              defaultBookStockingsCheckIntervalDays,
373              "Bookshop search refresh days",
374              "The number of days for which a cached bookshop search "
375                + "remains valid");
376 
377          bibBookBookshopSearchTimeoutSeconds =
378            getSettingTable().ensure(
379              "BibBookSearchTimeoutSecs",
380              defaultBibBookBookshopSearchTimeoutSeconds,
381              "Timeout for shop search for Bib book",
382              "The number of seconds which a bookshop search for a "
383                + "Bibliomania book is allowed to continue before being "
384                + "cut short");
385 
386          bookStockingsInBackground =
387            getSettingTable().ensure(
388              "StockingsCheckInBackground",
389              false,
390              "Background bookshop search",
391              "Whether to run continual bookshop searches in the background");
392 
393          bookStockingsOutputStartOffset =
394            getSettingTable().ensure(
395              "StockingsOutputStartOffset",
396              defaultBookStockingsOutputStartOffset,
397              "Start Offset for Shop DHTML",
398              "How far down the page to start DHTML output");
399 
400          bookStockingsCacheDir =
401            getSettingTable().ensure(
402              "StockingsCacheDir",
403              root + "/stockings",
404              "Book stockings cache directory",
405              "Where the system should store cached results of bookshop "
406                + "searches");
407 
408          bookStockingsCacheSizeMax =
409            getSettingTable().ensure(
410              "StockingsCacheMax",
411              defaultBookStockingsCacheSizeMax,
412              "Max cached book stockings",
413              "How many bookshop search results the system should cache");
414 
415          bookStockingsCheckConcurrentMax =
416            getSettingTable().ensure(
417              "StockingsCheckConcurrentMax",
418              defaultBookStockingsCheckConcurrentMax,
419              "Max concurrent bookshop searches",
420              "The maximum number of bookshop searches to allow "
421                + "at once (including the background search)");
422 
423          UKCurrency =
424            getCurrencyTable().ensure("UK Pound Sterling", 1d, Locale.UK);
425          //USCurrency = getCurrencyTable().ensure("US Dollar", 1.43, Locale.US);
426 
427          if (openBerkeleyDBs) {
428            try {
429              fti = new IndexOther(new File(getWorkspaceDir()), ftiConfig);
430              infoFTI = new IndexOther(new File(getInfoFTIDir()));
431            } catch (Exception e) {
432              throw new FTIException(e);
433            }
434          }
435          try{ 
436            pagination = new Pagination(new File(getWorkspaceDir()));
437          } catch (Exception e) { 
438            throw new RuntimeException(e);
439          }
440           
441          paginationTexHeader =
442            (Setting)getSettingTable().getNameColumn().firstWhereEq(
443              "PaginationTexHeader");
444          if (paginationTexHeader == null) {
445            paginationTexHeader =
446              getSettingTable().ensure(
447                "PaginationTexHeader",
448                pagination.defaultTexHeader(),
449                "TeX header for pagination",
450                "The header to be prepended to the TeX version of a "
451                  + "chapter when TeX is run to determine page breaks");
452            paginationTexHeader.setWidth(60);
453            paginationTexHeader.setHeight(5);
454          }
455          
456          Vector<Bookshop> bookshopsList = new Vector<Bookshop>();
457 
458          Bookshop bol = getBookshopTable().ensure("BOLUK", "BOL UK", "uk.gif");
459          if (!Boolean.TRUE.equals(bol.getDisabled())) {
460            bol.backend = bookshopBackendNamed("bol");
461            bookshopsList.addElement(bol);
462          }
463 
464          Bookshop bob =
465            getBookshopTable().ensure(
466              "BOB",
467              "Blackwells Online Bookshop",
468              "uk.gif");
469          if (!Boolean.TRUE.equals(bob.getDisabled())) {
470            bob.backend = bookshopBackendNamed("bob");
471            bookshopsList.addElement(bob);
472          }
473 
474          Bookshop amazon =
475            getBookshopTable().ensure("AMAZON", "amazon.com", "us.gif");
476          if (!Boolean.TRUE.equals(amazon.getDisabled())) {
477            amazon.backend = bookshopBackendNamed("amazon");
478            bookshopsList.addElement(amazon);
479          }
480 
481          bookshops = new Bookshop[bookshopsList.size()];
482          bookshopsList.copyInto(bookshops);
483 
484          String defaultSmtpServer;
485          try {
486            defaultSmtpServer = InetAddress.getLocalHost().toString();
487          } catch (Exception e) {
488            defaultSmtpServer = "www.bibliomania.com";
489          }
490 
491            smtpServer =
492              getSettingTable().ensure(Email.SMTPSERVER,
493    defaultSmtpServer,
494      "SMTP server",
495      "The SMTP server through which auto-generated mail " + "should be sent");
496 
497          passwordReminderMessage =
498            (Setting)getSettingTable().getNameColumn().firstWhereEq(
499              "PasswordReminderMessage");
500          if (passwordReminderMessage == null) {
501            String it;
502            try {
503              it =
504                new String(
505                  IoUtils.slurp(
506                    this.getClass().getResource(defaultPasswordReminderMessage),
507                    4096));
508            } catch (Exception e) {
509              throw new UnexpectedExceptionException(
510                "Didn't find resource " + defaultPasswordReminderMessage,
511                e);
512            }
513 
514            passwordReminderMessage =
515              getSettingTable().ensure(
516                "PasswordReminderMessage",
517                it,
518                "Password reminder message",
519                "The text of the message sent to users to remind them of "
520                  + "their password (actually a WebMacro template)");
521            passwordReminderMessage.setWidth(80);
522            passwordReminderMessage.setHeight(30);
523          }
524 
525          passwordReminderFrom =
526            getSettingTable().ensure(
527              "PasswordReminderFrom",
528              defaultPasswordReminderFrom,
529              "Password reminder `From:'",
530              "The address from whom the emailed password reminders "
531                + "should originate");
532 
533          confirmationEmailFrom =
534            getSettingTable().ensure(
535              "ConfirmationEmailFrom",
536              defaultConfirmationEmailFrom,
537              "Confirmation Email `From:'",
538              "The address from whom the confirmation email that is sent on "
539                + "registration should originate");
540 
541          orderEmailFrom =
542            getSettingTable().ensure(
543              "OrderEmailFrom",
544              defaultOrderEmailFrom,
545              "Order Email `From:'",
546              "The address from whom the emailed order acknowledgements "
547                + "should originate");
548 
549          orderEmailTo =
550            getSettingTable().ensure(
551              "OrderEmailTo",
552              defaultOrderEmailTo,
553              "Order Email `To:'",
554              "The address to send email acknowledgements to (at bibliomania)");
555 
556          stratusEmailTo =
557            getSettingTable().ensure(
558              "StratusEmailTo",
559              defaultStratusEmailTo,
560              "Stratus Email `To:'",
561              "The address to send email orders to (at Stratus)");
562 
563          contentEncoding =
564            getSettingTable().ensure(
565              "Content encoding",
566              defaultContentEncoding,
567              "Encoding (character set) to use for content",
568              "The encoding, or character set, to use for the cached "
569                + "content; if you don't know why you need to set this, "
570                + "leave it as UTF8.");
571 
572          uploadDir =
573            getSettingTable().ensure(
574              "UploadDir",
575              defaultUploadDir,
576              "Upload Directory for Product Images",
577              "Upload Directory for product images.  This is prepended to the "
578                + "path used for the book to which this product relates.");
579 
580          uploadURL =
581            getSettingTable().ensure(
582              "UploadURL",
583              defaultUploadURL,
584              "URL used when finding uploaded images",
585              "The URL used when finding uploaded images.  This is prepended to the "
586                + "URL used for the book to which this product relates.");
587 
588          /* this is all a bit verbose, but it is nice to ensure these are present
589           */
590 
591          // Advert table - normal admin permissions
592          setDefaultAccessPermissions(
593            getAdvertTable().getTableInfo(),
594            contentAdditionCapability,
595            administerCapability(),
596            contentModificationCapability,
597            null);
598 
599          // Attachment table - only sys admins can add / change
600          // individual users can also change things
601          setDefaultAccessPermissions(
602            getAttachmentTable().getTableInfo(),
603            null,
604            administerCapability(),
605            administerCapability(),
606            null);
607 
608          // Attachment type table - only sys admins can add / change
609          setDefaultAccessPermissions(
610            getAttachmentTypeTable().getTableInfo(),
611            administerCapability(),
612            administerCapability(),
613            administerCapability(),
614            null);
615 
616          // Author table - normal admin permissions
617          setDefaultAccessPermissions(
618            getAuthorTable().getTableInfo(),
619            contentAdditionCapability,
620            administerCapability(),
621            contentModificationCapability,
622            null);
623 
624          // Author website table - normal admin permissions
625          setDefaultAccessPermissions(
626            getAuthorWebSiteTable().getTableInfo(),
627            contentAdditionCapability,
628            administerCapability(),
629            contentModificationCapability,
630            null);
631 
632          // messageboard table - normal admin permissions
633          setDefaultAccessPermissions(
634            getBoardTable().getTableInfo(),
635            contentAdditionCapability,
636            administerCapability(),
637            contentModificationCapability,
638            null);
639 
640          // messageboard type table - only administrators
641          setDefaultAccessPermissions(
642            getBoardTypeTable().getTableInfo(),
643            administerCapability(),
644            administerCapability(),
645            contentModificationCapability,
646            null);
647 
648          // book table - normal admin permissions
649          setDefaultAccessPermissions(
650            getBookTable().getTableInfo(),
651            contentAdditionCapability,
652            administerCapability(),
653            contentModificationCapability,
654            null);
655 
656          // book format table - only administrators
657          setDefaultAccessPermissions(
658            getBookFormatTable().getTableInfo(),
659            administerCapability(),
660            administerCapability(),
661            administerCapability(),
662            null);
663 
664          // bookshop table - only administrators - but others can edit
665          setDefaultAccessPermissions(
666            getBookshopTable().getTableInfo(),
667            administerCapability(),
668            administerCapability(),
669            contentModificationCapability,
670            null);
671 
672          // book stocking table - only administrators 
673          setDefaultAccessPermissions(
674            getBookStockingTable().getTableInfo(),
675            administerCapability(),
676            administerCapability(),
677            administerCapability(),
678            null);
679 
680          // chapter table - normal admin permissions
681          setDefaultAccessPermissions(
682            getChapterTable().getTableInfo(),
683            contentAdditionCapability,
684            administerCapability(),
685            contentModificationCapability,
686            null);
687 
688          // country table - normal admin permissions
689          setDefaultAccessPermissions(
690            getCountryTable().getTableInfo(),
691            contentAdditionCapability,
692            administerCapability(),
693            contentModificationCapability,
694            null);
695 
696          // currency table - normal admin permissions
697          setDefaultAccessPermissions(
698            getCurrencyTable().getTableInfo(),
699            contentAdditionCapability,
700            administerCapability(),
701            contentModificationCapability,
702            null);
703 
704          // delivery charge table - normal admin permissions
705          setDefaultAccessPermissions(
706            getDeliveryChargeTable().getTableInfo(),
707            contentAdditionCapability,
708            administerCapability(),
709            contentModificationCapability,
710            null);
711 
712          // delivery charge band table - normal admin permissions
713          setDefaultAccessPermissions(
714            getDeliveryChargeBandTable().getTableInfo(),
715            contentAdditionCapability,
716            administerCapability(),
717            contentModificationCapability,
718            null);
719 
720          // download table - normal admin permissions
721          setDefaultAccessPermissions(
722            getDownloadTable().getTableInfo(),
723            contentAdditionCapability,
724            administerCapability(),
725            contentModificationCapability,
726            null);
727 
728          // download event table - only administrator and the relevant user
729          setDefaultAccessPermissions(
730            getDownloadEventTable().getTableInfo(),
731            null,
732            administerCapability(),
733            administerCapability(),
734            administerCapability());
735 
736          // layout table - normal admin permissions
737          setDefaultAccessPermissions(
738            getLayoutTable().getTableInfo(),
739            contentAdditionCapability,
740            administerCapability(),
741            contentModificationCapability,
742            null);
743 
744          // membership status table - normal admin permissions
745          setDefaultAccessPermissions(
746            getMembershipStatusTable().getTableInfo(),
747            contentAdditionCapability,
748            administerCapability(),
749            contentModificationCapability,
750            null);
751 
752          // order table - owning user, and admins can modify
753          setDefaultAccessPermissions(
754            getShopOrderTable().getTableInfo(),
755            null,
756            administerCapability(),
757            null,
758            null);
759 
760          // order item table - owning user, and admins can modify
761          setDefaultAccessPermissions(
762            getShopOrderItemTable().getTableInfo(),
763            null,
764            administerCapability(),
765            null,
766            null);
767 
768          // order status table - anly admins can chnage 
769          setDefaultAccessPermissions(
770            getOrderStatusTable().getTableInfo(),
771            administerCapability(),
772            administerCapability(),
773            administerCapability(),
774            null);
775 
776          // product table - normal permissions
777          setDefaultAccessPermissions(
778            getProductTable().getTableInfo(),
779            contentAdditionCapability,
780            administerCapability(),
781            contentModificationCapability,
782            null);
783 
784          // product association table - normal permissions
785          setDefaultAccessPermissions(
786            getProductAssociationTable().getTableInfo(),
787            contentAdditionCapability,
788            administerCapability(),
789            contentModificationCapability,
790            null);
791 
792          // publisher table - normal permissions
793          setDefaultAccessPermissions(
794            getPublisherTable().getTableInfo(),
795            contentAdditionCapability,
796            administerCapability(),
797            contentModificationCapability,
798            null);
799 
800          // publisher table - normal permissions
801          setDefaultAccessPermissions(
802            getPublisherTable().getTableInfo(),
803            contentAdditionCapability,
804            administerCapability(),
805            contentModificationCapability,
806            null);
807 
808          // section table - normal permissions
809          setDefaultAccessPermissions(
810            getSectionTable().getTableInfo(),
811            contentAdditionCapability,
812            administerCapability(),
813            contentModificationCapability,
814            null);
815 
816          // section group table - admin only
817          setDefaultAccessPermissions(
818            getSectionTable().getTableInfo(),
819            administerCapability(),
820            administerCapability(),
821            administerCapability(),
822            null);
823 
824          // Sex table - admin only
825          setDefaultAccessPermissions(
826            getSexTable().getTableInfo(),
827            administerCapability(),
828            administerCapability(),
829            administerCapability(),
830            null);
831 
832          // Stockingssearch table - admin only
833          setDefaultAccessPermissions(
834            getStockingsSearchTable().getTableInfo(),
835            null,
836            administerCapability(),
837            administerCapability(),
838            null);
839 
840          // Supplier table - normal permission
841          setDefaultAccessPermissions(
842            getSupplierTable().getTableInfo(),
843            contentAdditionCapability,
844            administerCapability(),
845            contentModificationCapability,
846            null);
847 
848          // Supplier products table - normal permission
849          setDefaultAccessPermissions(
850            getSupplierProductTable().getTableInfo(),
851            contentAdditionCapability,
852            administerCapability(),
853            contentModificationCapability,
854            null);
855 
856          // User table - owner and admin only
857          setDefaultAccessPermissions(
858            getUserTable().getTableInfo(),
859            administerCapability(),
860            administerCapability(),
861            administerCapability(),
862            administerCapability());
863        }
864      });
865 
866      if (bookStockingsInBackgroundAppropriate)
867        new BackgroundStockingsChecker().start();
868 
869      /*
870       * Here is a good place to put one off hacks.
871       */
872       
873    }
874 
875    private void setDefaultAccessPermissions(
876      TableInfo info,
877      Capability add,
878      Capability delete,
879      Capability write,
880      Capability read) {
881      if (info.getCancreate() == null)
882        info.setCancreate(add);
883      if (info.getDefaultcandelete() == null)
884        info.setDefaultcandelete(delete);
885      if (info.getDefaultcanwrite() == null)
886        info.setDefaultcanwrite(write);
887      if (info.getDefaultcanread() == null)
888        info.setDefaultcanread(read);
889    }
890 
891    public IndexOther fti() {
892      return fti;
893    }
894 
895    public IndexOther infoFTI() {
896      return infoFTI;
897    }
898 
899    Library infoLibrary = new Library() {
900      public Text text(long textID) {
901        int a =
902          (int) ((textID & ((1l << Chapter.ftiTextID_section_shift) - 1))
903            >> Chapter.ftiTextID_author_shift);
904 
905        long aMask = (1l << Chapter.ftiTextID_author_shift) - 1;
906 
907        if ((textID & aMask) == aMask) {
908          // it's an author
909          // but be defensive about finding it
910          try {
911            return getAuthorTable().getAuthorObject(new Integer(a));
912          } catch (NoSuchRowPoemException e) {
913            // log it
914            e.printStackTrace(System.err);
915            return null;
916          }
917        } else {
918          // it's a book
919          int b = (int) ((textID & aMask) >> Chapter.ftiTextID_book_shift);
920          return (Book)getBookTable().firstSelection(
921            "author = " + a + " AND " + "authorsequence = " + b);
922        }
923      }
924    };
925 
926    public Library infoLibrary() {
927      return infoLibrary;
928    }
929 
930    public boolean openBerkelyDBs() { 
931      return openBerkeleyDBs;
932    }
933    
934    public Pagination pagination() {
935      return pagination;
936    }
937 
938    public String getWorkspaceDir() {
939      return workspaceDir.getStringCooked();
940    }
941 
942    public String getUploadDir() {
943      return uploadDir.getStringCooked();
944    }
945 
946    public String getUploadURL() {
947      return uploadURL.getStringCooked();
948    }
949 
950    public String getInfoFTIDir() {
951      return getWorkspaceDir() + infoFTIDir.getStringCooked();
952    }
953 
954    public String getContentRootDir() {
955      return contentRootDir.getStringCooked();
956    }
957 
958    private WebMacro _wm = null;
959 
960    public WebMacro getWebMacro() {
961      if (_wm == null) {
962        synchronized (this) {
963          if (_wm == null) {
964            try {
965              _wm = new WM();
966            } catch (InitException e) {
967              throw new UnexpectedExceptionException(
968                "Initialising WebMacro for the pagination subsystem",
969                e);
970            }
971          }
972        }
973      }
974 
975      return _wm;
976    }
977 
978    public void 
979      setupContext(Melati melati, Context context, Unit it, Hashtable<String, Object> extras) {
980 
981      SectionGroup sectiongroup = it == null ? null : it.getReadArea();
982      context.put("sectiongroup", sectiongroup);
983      if (sectiongroup != null) {
984        context.put("areaColour", sectiongroup.getThemecolour());
985      } else {
986        context.put("areaColour", "#FF0000");
987      }
988 
989      Section section = null;
990      Author author = null;
991      Book book = null;
992      Chapter chapter = null;
993 
994      if (it != null) {
995        context.put("meta_description", it.getMetatag_description());
996        context.put("meta_keywords", it.getMetatag_keywords());
997 
998        if (it instanceof Chapter) {
999          chapter = (Chapter)it;
1000          book = chapter.getBook();
1001          author = book.getAuthor();
1002          section = book.getSection();
1003        } else if (it instanceof Book) {
1004          book = (Book)it;
1005          author = book.getAuthor();
1006          section = book.getSection();
1007        } else if (it instanceof Author) {
1008          author = (Author)it;
1009        } else if (it instanceof Section) {
1010          section = (Section)it;
1011        }
1012      }
1013 
1014      context.put("object", it);
1015      context.put("section", section);
1016      context.put("author", author);
1017      context.put("book", book);
1018      context.put("chapter", chapter);
1019      context.put("bib", getBib());
1020      context.put("db", this);
1021      context.put("melati", melati);
1022      if (extras != null)
1023        for (Enumeration<String> keys = extras.keys(); keys.hasMoreElements();) {
1024          String key = keys.nextElement();
1025          context.put(key, extras.get(key));
1026        }
1027    }
1028 
1029    public final void setupContext(Melati melati, Context context, Unit it) {
1030      setupContext(melati, context, it, null);
1031    }
1032 
1033    /**
1034     * Hook to be called when content files are created or changed.  At the
1035     * moment, this makes the file executable (<TT>chmod</TT> <TT>ugo+x</TT>),
1036     * because we use mod-include (server-side includes) to serve the ad banners,
1037     * and Apache will only send a <TT>Last-Modified</TT> in the header if the
1038     * <TT>XBitHack</TT> option is enabled in <TT>httpd.conf</TT> and the
1039     * <TT>x</TT> bit is set.  For a while we feared that if Apache doesn't send
1040     * <TT>Last-Modified</TT>, Google doesn't index us; that seems not to be
1041     * true, but nevertheless sending <TT>Last-Modified</TT> is a good thing.
1042     */
1043 
1044    public static void notifyNewContentFile(File file) throws IOException {
1045      FileUtils.makeExecutable(file);
1046    }
1047 
1048    public String getWMTemplet(String templetName) {
1049      return "org/paneris/bibliomania/template/webmacro/templets/html/" + templetName + ".wm";
1050    }
1051 
1052    public final void interpolateTemplateToStream(
1053        org.webmacro.Template template,
1054        OutputStream o,
1055        Unit object) throws WebMacroException, IOException {
1056      interpolateTemplateToStream(template, o, object, null);
1057    }
1058    public void interpolateTemplateToStream(org.webmacro.Template template,
1059                               OutputStream o,
1060                               Unit object,
1061                               Hashtable extras)
1062        throws WebMacroException, IOException {
1063      
1064      WebMacro wm = getWebMacro();
1065 
1066      MelatiFastWriter fmw =
1067        new MelatiFastWriter(wm.getBroker(), o, getContentEncoding());
1068      Melati m = new Melati(new MelatiConfig(), fmw);
1069      Context context = wm.getContext();
1070      setupContext(m, context, object, extras);
1071      template.write(fmw.getFastWriter(), context);
1072      fmw.getFastWriter().flush();
1073      /*
1074          Melati m = new Melati(new MelatiConfig(),
1075                                new SimpleMelatiWriter(new OutputStreamWriter(o)));
1076          m.setBufferingOff(false);
1077          Context context = wm.getContext();
1078          setupContext(m, context, object, extras);
1079          template.write(new FastWriter(o, getContentEncoding()), context);
1080      */
1081    }
1082 
1083   public final void interpolateTemplateToFile(
1084       String templateName, 
1085       File to,
1086       Unit object) throws WebMacroException, IOException {
1087     interpolateTemplateToFile(templateName, to, object, null);
1088   }
1089 
1090    public void interpolateTemplateToFile(
1091        String templateName,
1092        File to,
1093        Unit object,
1094        Hashtable<String, Object> extras) throws WebMacroException, IOException {
1095      org.webmacro.Template template = getWebMacro().getTemplate(templateName);
1096      OutputStream out = new BufferedOutputStream(new FileOutputStream(to));
1097      try {
1098        interpolateTemplateToStream(template, out, object, extras);
1099      } finally {
1100        try {
1101          out.close();
1102        } catch (Exception e) {
1103          throw new RuntimeException(e);
1104        }
1105      }
1106 
1107      notifyNewContentFile(to);
1108    }
1109 
1110 
1111    public void interpolateAsTemplate(Reader in, OutputStream out, Unit object)
1112        throws WebMacroException, IOException {
1113      interpolateTemplateToStream(
1114          new StreamTemplate(getWebMacro().getBroker(), in), out, object);
1115    }
1116 
1117    public void interpolateAsTemplate(File from, OutputStream out, Unit object)
1118        throws WebMacroException, IOException {
1119      Reader in = new BufferedReader(new FileReader(from));
1120      try {
1121        interpolateAsTemplate(in, out, object);
1122      } finally {
1123        try {
1124          in.close();
1125        } catch (Exception e) {
1126          throw new RuntimeException(e);
1127        }
1128      }
1129    }
1130 
1131    public void interpolateAsTemplate(File from, File to, Unit object)
1132        throws WebMacroException, IOException {
1133      OutputStream out = new BufferedOutputStream(new FileOutputStream(to));
1134      try {
1135        interpolateAsTemplate(from, out, object);
1136      } finally {
1137        try {
1138          out.close();
1139        } catch (Exception e) {
1140          throw new RuntimeException(e);
1141        }
1142      }
1143    }
1144 
1145    public static class TemplateException extends MelatiRuntimeException {
1146      private static final long serialVersionUID = 1L;
1147      public File file;
1148 
1149      public TemplateException(File file, Exception trouble) {
1150        super(trouble);
1151        this.file = file;
1152      }
1153 
1154      public String getMessage() {
1155        return "Couldn't load templet from " + file + "\n" + subException;
1156      }
1157    }
1158 
1159    private org.webmacro.Template template(String source) {
1160      File file = new File(getContentRootDir(), source);
1161 
1162      org.webmacro.Template it =
1163        new FileTemplate(getWebMacro().getBroker(), file, getContentEncoding());
1164      try {
1165        it.parse();
1166      } catch (Exception e) {
1167        throw new TemplateException(file, e);
1168      }
1169 
1170      return it;
1171    }
1172 
1173    public void writeContentHeader(OutputStream w, Unit object, String template)
1174      throws WebMacroException, IOException {
1175      interpolateTemplateToStream(
1176        template(template == null ? "header.wm" : template),
1177        w,
1178        object);
1179    }
1180 
1181    public void writeContentFooter(OutputStream w, Unit object, String template)
1182      throws WebMacroException, IOException {
1183      interpolateTemplateToStream(
1184        template(template == null ? "footer.wm" : template),
1185        w,
1186        object);
1187    }
1188 
1189    private org.webmacro.Template footnoteTemplate = null;
1190 
1191    public org.webmacro.Template getFootnoteTemplate() {
1192      if (footnoteTemplate == null)
1193        footnoteTemplate = template("footnote.wm");
1194 
1195      return footnoteTemplate;
1196    }
1197 
1198    public String getPasswordReminderMessage() {
1199      return passwordReminderMessage.getStringCooked();
1200    }
1201 
1202    public org.webmacro.Template getPasswordReminderTemplate() {
1203      return new StreamTemplate(
1204        getWebMacro().getBroker(),
1205        new StringReader(getPasswordReminderMessage().trim()));
1206      //        new StringReader(getPasswordReminderMessage().trim()),getContentEncoding());
1207    }
1208 
1209    public String getPasswordReminderFrom() {
1210      return passwordReminderFrom.getStringCooked();
1211    }
1212 
1213    public String getOrderEmailFrom() {
1214      return orderEmailFrom.getStringCooked();
1215    }
1216 
1217    public String getConfirmationEmailFrom() {
1218      return confirmationEmailFrom.getStringCooked();
1219    }
1220 
1221    public String getOrderEmailTo() {
1222      return orderEmailTo.getStringCooked();
1223    }
1224 
1225    public String getStratusEmailTo() {
1226      return stratusEmailTo.getStringCooked();
1227    }
1228 
1229    public String getCachedContentRootDir() {
1230      return getWorkspaceDir() + "/cached";
1231    }
1232 
1233    public String getStaticRootURL() {
1234      return staticRootURL.getStringCooked();
1235    }
1236 
1237    public String getContentStaticRootURL() {
1238      return contentStaticRootURL.getStringCooked();
1239    }
1240 
1241    public String getBannerURLPath() {
1242      return bannerURLPath.getStringCooked();
1243    }
1244 
1245    public String getCacheRootURL() {
1246      String it = cacheRootURL.getStringCooked();
1247      return it == null ? "" : it;
1248    }
1249 
1250    public String getHomepageURL() {
1251      return homepageURL.getStringCooked();
1252    }
1253 
1254    public String getSmtpServer() {
1255      return smtpServer.getStringCooked();
1256    }
1257 
1258    public int getDefaultSearchHitsPerText() {
1259      Integer it = defaultSearchHitsPerText.getIntegerCooked();
1260      return it == null ? defaultDefaultSearchHitsPerText : it.intValue();
1261    }
1262 
1263    public int getSearchHitsPerPage() {
1264      Integer it = searchHitsPerPage.getIntegerCooked();
1265      return it == null ? defaultSearchHitsPerPage : it.intValue();
1266    }
1267 
1268    public String getPaginationTexHeader() {
1269      return paginationTexHeader.getStringCooked();
1270    }
1271 
1272    public int getBookStockingsCheckIntervalDays() {
1273      Integer it = bookStockingsCheckIntervalDays.getIntegerCooked();
1274      return it == null ? defaultBookStockingsCheckIntervalDays : it.intValue();
1275    }
1276 
1277    public long getBookStockingsCheckIntervalMillis() {
1278      return (long)getBookStockingsCheckIntervalDays() * 1000L * 60L * 60L * 24L;
1279    }
1280 
1281    public int getBibBookBookshopSearchTimeoutSeconds() {
1282      Integer it = bibBookBookshopSearchTimeoutSeconds.getIntegerCooked();
1283      return it == null
1284        ? defaultBibBookBookshopSearchTimeoutSeconds
1285        : it.intValue();
1286    }
1287 
1288    public long getBibBookBookshopSearchTimeoutMillis() {
1289      return (long)getBibBookBookshopSearchTimeoutSeconds() * 1000L;
1290    }
1291 
1292    public int getBookStockingsOutputStartOffset() {
1293      Integer it = bookStockingsOutputStartOffset.getIntegerCooked();
1294      return it == null ? defaultBookStockingsOutputStartOffset : it.intValue();
1295    }
1296 
1297    public String getBookStockingsCacheDir() {
1298      return bookStockingsCacheDir.getStringCooked();
1299    }
1300 
1301    public int getBookStockingsCacheSizeMax() {
1302      Integer it = bookStockingsCacheSizeMax.getIntegerCooked();
1303      return it == null ? defaultBookStockingsCacheSizeMax : it.intValue();
1304    }
1305 
1306    public Integer pageFromAnchor(String anchor) {
1307      if (anchor.startsWith(Pagination.pageAnchorPrefix))
1308        try {
1309          return new Integer(
1310            Integer.parseInt(
1311              anchor.substring(Pagination.pageAnchorPrefix.length()))
1312              + 1);
1313        } catch (NumberFormatException e) {
1314          return null;
1315        } else
1316        return null;
1317    }
1318 
1319    public static class BookshopException extends MelatiRuntimeException {
1320      /**
1321       * 
1322       */
1323      private static final long serialVersionUID = 1L;
1324      public String which;
1325 
1326      public BookshopException(String which, Exception e) {
1327        super(e);
1328        this.which = which;
1329      }
1330 
1331      public String getMessage() {
1332        return "Something went wrong starting the "
1333          + which
1334          + " subsystem\n"
1335          + subException.getMessage();
1336      }
1337    }
1338 
1339    private BookshopBackend bookshopBackendNamed(String packageComp) {
1340      try {
1341        return (
1342          (BookshopBackendFactory)Class
1343            .forName(
1344              "org.paneris.bibliomania.metasearch." + packageComp + ".Factory")
1345            .newInstance())
1346            .instance(
1347          new File(getWorkspaceDir()),
1348          0);
1349      } catch (Exception e) {
1350        throw new BookshopException(packageComp, e);
1351      }
1352    }
1353 
1354    public Bookshop[] bookshops() {
1355      return bookshops;
1356    }
1357 
1358    public SectionGroup getReadSectionGroup() {
1359      return readSectionGroup;
1360    }
1361 
1362    public Currency getUKCurrency() {
1363      return UKCurrency;
1364    }
1365 
1366    /** Not used, just for show */
1367    public List<ShopOrder> getUKCurrentcyShopOrders() { 
1368      return getUKCurrency().getCurrencyShopOrderList();
1369    }
1370 
1371    public SectionGroup getStudySectionGroup() {
1372      return studySectionGroup;
1373    }
1374 
1375    public SectionGroup getResearchSectionGroup() {
1376      return researchSectionGroup;
1377    }
1378 
1379    public SectionGroup getShopSectionGroup() {
1380      return shopSectionGroup;
1381    }
1382 
1383    public SectionGroup getSearchSectionGroup() {
1384      return searchSectionGroup;
1385    }
1386 
1387    public Group getRegisteredUserGroup() {
1388      return registeredUserGroup;
1389    }
1390 
1391    public Capability getRegisteredUserCapability() {
1392      return registeredUserCapability;
1393    }
1394    public Capability getContentModificationCapability() {
1395      return contentModificationCapability;
1396    }
1397    public Capability getContentAdditionCapability() {
1398      return contentAdditionCapability;
1399    }
1400 
1401    public User getTemplateRegisterUser() {
1402      return templateRegisterUser;
1403    }
1404 
1405    private Bib bib = null;
1406 
1407    public Bib getBib() {
1408      if (bib == null)
1409        bib = new Bib(this);
1410      return bib;
1411    }
1412 
1413    private int bookshopSearchesRunning = 0;
1414 
1415    public void notifyBookshopSearchStart() {
1416      ++bookshopSearchesRunning;
1417    }
1418 
1419    public void notifyBookshopSearchStop() {
1420      --bookshopSearchesRunning;
1421    }
1422 
1423    public boolean okToRunABookshopSearch() {
1424      Integer maxO = bookStockingsCheckConcurrentMax.getIntegerCooked();
1425      int max =
1426        maxO == null ? defaultBookStockingsCheckConcurrentMax : maxO.intValue();
1427 
1428      return bookshopSearchesRunning < max;
1429    }
1430 
1431    public boolean getBookStockingsInBackground() {
1432      return bookStockingsInBackgroundAppropriate
1433        && Boolean.TRUE.equals(bookStockingsInBackground.getBooleanCooked());
1434    }
1435 
1436    public String getContentEncoding() {
1437      return contentEncoding.getStringCooked();
1438    }
1439 
1440    private class BackgroundStockingsChecker extends Thread {
1441 
1442      private PreparedStatementFactory booksPS, booksPSNull;
1443 
1444      BackgroundStockingsChecker() {
1445        booksPSNull =
1446          new PreparedStatementFactory(
1447            BibliomaniaDatabase.this,
1448            "SELECT book.id FROM book WHERE lastbookshopsearch IS NULL AND "
1449              + "(hasnofrontpage IS NULL OR NOT hasnofrontpage) AND "
1450              + "(author.id = book.author AND "
1451              + "(author.nonstandard IS NULL OR NOT author.nonstandard)) "
1452              + "LIMIT 1");
1453 
1454        booksPS =
1455          new PreparedStatementFactory(
1456            BibliomaniaDatabase.this,
1457            "SELECT id FROM book WHERE lastbookshopsearch < ? AND "
1458              + "(hasnofrontpage IS NULL OR NOT hasnofrontpage) AND "
1459              + "(author.id = book.author AND "
1460              + "(author.nonstandard IS NULL OR NOT author.nonstandard)) "
1461              + "ORDER BY lastbookshopsearch "
1462              + "LIMIT 1");
1463      }
1464 
1465      private void doNext() throws Exception {
1466        inSession(AccessToken.root, new PoemTask() {
1467          public void run() {
1468            Book nextToDo;
1469            PreparedStatement ps = null;
1470 
1471            try {
1472              if (!getBookStockingsInBackground())
1473                nextToDo = null;
1474              else {
1475                Integer bookTroid = null;
1476 
1477                ps = booksPSNull.preparedStatement(PoemThread.transaction());
1478                ResultSet rs = ps.executeQuery();
1479                try {
1480                  if (rs.next())
1481                    bookTroid = new Integer(rs.getInt(1));
1482                } finally {
1483                  rs.close();
1484                }
1485 
1486                if (bookTroid == null) {
1487                  ps = booksPS.preparedStatement(PoemThread.transaction());
1488                  ps.setTimestamp(
1489                    1,
1490                    new Timestamp(
1491                      System.currentTimeMillis()
1492                        - getBookStockingsCheckIntervalMillis()));
1493 
1494                  rs = ps.executeQuery();
1495                  try {
1496                    if (rs.next())
1497                      bookTroid = new Integer(rs.getInt(1));
1498                  } finally {
1499                    rs.close();
1500                  }
1501                }
1502 
1503                nextToDo =
1504                  bookTroid == null
1505                    ? null
1506                    : (Book)getBookTable().getObject(bookTroid);
1507              }
1508            } catch (Exception e) {
1509              System.err.println("BackgroundStockingsChecker: " + e);
1510              nextToDo = null;
1511            }
1512 
1513            if (nextToDo != null)
1514              getStockingsSearchTable().searchFor(nextToDo).runNewSearch();
1515          }
1516        });
1517      }
1518 
1519      public void run() {
1520        for (;;) {
1521          try {
1522            Thread.sleep(120000);
1523            doNext();
1524          } catch (Exception e) {
1525            System.err.println("Background stockings checker: " + e);
1526          }
1527        }
1528      }
1529    }
1530 
1531    public Author getShakespeare() {
1532      return shakespeare;
1533    }
1534 
1535    public Section getDrama() {
1536      return drama;
1537    }
1538 
1539    public Book getMacbeth() {
1540      return macbeth;
1541    }
1542 
1543  public org.paneris.melati.boards.model.UserTable<org.paneris.melati.boards.model.User> getBoardsUserTable() {  
1544   return (org.paneris.melati.boards.model.UserTable<org.paneris.melati.boards.model.User>)getBoardsUserTable();
1545  }
1546  
1547  /** 
1548   * Convert 'Head in the Sky' to 'headInTheSky'.
1549   * Also 'NHS management' to 'nhsManagement'.
1550   */
1551  public static String toCamelCase(String value) {
1552    return toCamelCase(value, true);   
1553  }
1554  public static String toCamelCase(String value, boolean lowercaseFirstLetter) {
1555    StringBuilder sb = new StringBuilder();
1556 
1557    final char delimiter = ' ';
1558    boolean lower = lowercaseFirstLetter;
1559    for (int i = 0; i < value.length(); ++i) {
1560      final char valueChar = value.charAt(i);
1561      if (valueChar == delimiter) {
1562        lower = false;
1563      } else if (lower) {
1564        sb.append(Character.toLowerCase(valueChar));
1565      } else {
1566        sb.append(Character.toUpperCase(valueChar));
1567        lower = true;
1568      }
1569    }
1570 
1571    return sb.toString();
1572  }
1573 }
1574 
1575