Document: Add and use public constants for layout names.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / server / ConfiguredDocument.java
1 /*
2  * Copyright (C) 2011 Openismus GmbH
3  *
4  * This file is part of GWT-Glom.
5  *
6  * GWT-Glom is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package org.glom.web.server;
21
22 import java.beans.PropertyVetoException;
23 import java.sql.Connection;
24 import java.sql.DriverManager;
25 import java.sql.SQLException;
26 import java.util.ArrayList;
27 import java.util.Hashtable;
28 import java.util.List;
29 import java.util.Locale;
30
31 import org.apache.commons.lang3.StringUtils;
32 import org.glom.web.server.database.DetailsDBAccess;
33 import org.glom.web.server.database.ListViewDBAccess;
34 import org.glom.web.server.database.RelatedListDBAccess;
35 import org.glom.web.server.database.RelatedListNavigation;
36 import org.glom.web.server.libglom.Document;
37 import org.glom.web.shared.DataItem;
38 import org.glom.web.shared.DocumentInfo;
39 import org.glom.web.shared.NavigationRecord;
40 import org.glom.web.shared.Reports;
41 import org.glom.web.shared.TypedDataItem;
42 import org.glom.web.shared.libglom.CustomTitle;
43 import org.glom.web.shared.libglom.Field;
44 import org.glom.web.shared.libglom.Relationship;
45 import org.glom.web.shared.libglom.Report;
46 import org.glom.web.shared.libglom.Translatable;
47 import org.glom.web.shared.libglom.layout.LayoutGroup;
48 import org.glom.web.shared.libglom.layout.LayoutItem;
49 import org.glom.web.shared.libglom.layout.LayoutItemField;
50 import org.glom.web.shared.libglom.layout.LayoutItemPortal;
51 import org.glom.web.shared.libglom.layout.UsesRelationship;
52
53 import com.mchange.v2.c3p0.ComboPooledDataSource;
54
55 /**
56  * A class to hold configuration information for a given Glom document. This class retrieves layout information from
57  * libglom and data from the underlying PostgreSQL database.
58  */
59 final class ConfiguredDocument {
60
61         private Document document;
62         private ComboPooledDataSource cpds;
63         private boolean authenticated = false;
64         private String documentID = "";
65         private String defaultLocaleID = "";
66
67         private static class LayoutLocaleMap extends Hashtable<String, List<LayoutGroup>> {
68                 private static final long serialVersionUID = 6542501521673767267L;
69         };
70
71         private static class TableLayouts {
72                 public LayoutLocaleMap listLayouts;
73                 public LayoutLocaleMap detailsLayouts;
74         }
75
76         private static class TableLayoutsForLocale extends Hashtable<String, TableLayouts> {
77                 private static final long serialVersionUID = -1947929931925049013L;
78
79                 public LayoutGroup getListLayout(final String tableName, final String locale) {
80                         final List<LayoutGroup> groups = getLayout(tableName, locale, false);
81                         if (groups == null) {
82                                 return null;
83                         }
84
85                         if (groups.isEmpty()) {
86                                 return null;
87                         }
88
89                         return groups.get(0);
90                 }
91
92                 public List<LayoutGroup> getDetailsLayout(final String tableName, final String locale) {
93                         return getLayout(tableName, locale, true);
94                 }
95
96                 public void setListLayout(final String tableName, final String locale, final LayoutGroup layout) {
97                         final List<LayoutGroup> list = new ArrayList<LayoutGroup>();
98                         list.add(layout);
99                         setLayout(tableName, locale, list, false);
100                 }
101
102                 public void setDetailsLayout(final String tableName, final String locale, final List<LayoutGroup> layout) {
103                         setLayout(tableName, locale, layout, true);
104                 }
105
106                 private List<LayoutGroup> getLayout(final String tableName, final String locale, final boolean details) {
107                         final LayoutLocaleMap map = getMap(tableName, details);
108
109                         if (map == null) {
110                                 return null;
111                         }
112
113                         return map.get(locale);
114                 }
115
116                 private LayoutLocaleMap getMap(final String tableName, final boolean details) {
117                         final TableLayouts tableLayouts = get(tableName);
118                         if (tableLayouts == null) {
119                                 return null;
120                         }
121
122                         LayoutLocaleMap map = null;
123                         if (details) {
124                                 map = tableLayouts.detailsLayouts;
125                         } else {
126                                 map = tableLayouts.listLayouts;
127                         }
128
129                         return map;
130                 }
131
132                 private LayoutLocaleMap getMapWithAdd(final String tableName, final boolean details) {
133                         TableLayouts tableLayouts = get(tableName);
134                         if (tableLayouts == null) {
135                                 tableLayouts = new TableLayouts();
136                                 put(tableName, tableLayouts);
137                         }
138
139                         LayoutLocaleMap map = null;
140                         if (details) {
141                                 if (tableLayouts.detailsLayouts == null) {
142                                         tableLayouts.detailsLayouts = new LayoutLocaleMap();
143                                 }
144
145                                 map = tableLayouts.detailsLayouts;
146                         } else {
147                                 if (tableLayouts.listLayouts == null) {
148                                         tableLayouts.listLayouts = new LayoutLocaleMap();
149                                 }
150
151                                 map = tableLayouts.listLayouts;
152                         }
153
154                         return map;
155                 }
156
157                 private void setLayout(final String tableName, final String locale, final List<LayoutGroup> layout,
158                                 final boolean details) {
159                         final LayoutLocaleMap map = getMapWithAdd(tableName, details);
160                         if (map != null) {
161                                 map.put(locale, layout);
162                         }
163                 }
164         }
165
166         private final TableLayoutsForLocale mapTableLayouts = new TableLayoutsForLocale();
167
168         @SuppressWarnings("unused")
169         private ConfiguredDocument() {
170                 // disable default constructor
171         }
172
173         ConfiguredDocument(final Document document) throws PropertyVetoException {
174
175                 // load the jdbc driver
176                 cpds = createAndSetupDataSource(document);
177
178                 this.document = document;
179         }
180
181         /**
182          * @param document
183          * @return
184          */
185         private static ComboPooledDataSource createAndSetupDataSource(final Document document) {
186                 final ComboPooledDataSource cpds = new ComboPooledDataSource();
187
188                 // We don't support sqlite or self-hosting yet.
189                 if ((document.getHostingMode() != Document.HostingMode.HOSTING_MODE_POSTGRES_CENTRAL)
190                                 && (document.getHostingMode() != Document.HostingMode.HOSTING_MODE_POSTGRES_SELF)) {
191                         // TODO: We allow self-hosting here, for testing,
192                         // but maybe the startup of self-hosting should happen here.
193                         Log.fatal("Error configuring the database connection." + " Only PostgreSQL hosting is supported.");
194                         // FIXME: Throw exception?
195                 }
196
197                 try {
198                         cpds.setDriverClass("org.postgresql.Driver");
199                 } catch (final PropertyVetoException e) {
200                         Log.fatal("Error loading the PostgreSQL JDBC driver."
201                                         + " Is the PostgreSQL JDBC jar available to the servlet?", e);
202                         return null;
203                 }
204
205                 // setup the JDBC driver for the current glom document
206                 String jdbcURL = "jdbc:postgresql://" + document.getConnectionServer() + ":" + document.getConnectionPort();
207
208                 String db = document.getConnectionDatabase();
209                 if (StringUtils.isEmpty(db)) {
210                         // Use the default PostgreSQL database, because ComboPooledDataSource.connect() fails otherwise.
211                         db = "template1";
212                 }
213                 jdbcURL += "/" + db; // TODO: Quote the database name?
214
215                 cpds.setJdbcUrl(jdbcURL);
216
217                 return cpds;
218         }
219
220         /**
221          * Sets the username and password for the database associated with the Glom document.
222          * 
223          * @return true if the username and password works, false otherwise
224          */
225         boolean setUsernameAndPassword(final String username, final String password) throws SQLException {
226                 cpds.setUser(username);
227                 cpds.setPassword(password);
228
229                 final int acquireRetryAttempts = cpds.getAcquireRetryAttempts();
230                 cpds.setAcquireRetryAttempts(1);
231                 Connection conn = null;
232                 try {
233                         // FIXME find a better way to check authentication
234                         // it's possible that the connection could be failing for another reason
235                         
236                         //Change the timeout, because it otherwise takes ages to fail sometimes when the details are not setup.
237                         //This is more than enough.
238                         DriverManager.setLoginTimeout(5); 
239                         
240                         conn = cpds.getConnection();
241                         authenticated = true;
242                 } catch (final SQLException e) {
243                         Log.info(Utils.getFileName(document.getFileURI()), e.getMessage());
244                         Log.info(Utils.getFileName(document.getFileURI()),
245                                         "Connection Failed. Maybe the username or password is not correct.");
246                         authenticated = false;
247                 } finally {
248                         if (conn != null) {
249                                 conn.close();
250                         }
251                         cpds.setAcquireRetryAttempts(acquireRetryAttempts);
252                 }
253                 return authenticated;
254         }
255
256         Document getDocument() {
257                 return document;
258         }
259
260         ComboPooledDataSource getCpds() {
261                 return cpds;
262         }
263
264         boolean isAuthenticated() {
265                 return authenticated;
266         }
267
268         String getDocumentID() {
269                 return documentID;
270         }
271
272         void setDocumentID(final String documentID) {
273                 this.documentID = documentID;
274         }
275
276         String getDefaultLocaleID() {
277                 return defaultLocaleID;
278         }
279
280         void setDefaultLocaleID(final String localeID) {
281                 this.defaultLocaleID = localeID;
282         }
283
284         /**
285          * @return
286          */
287         DocumentInfo getDocumentInfo(final String localeID) {
288                 final DocumentInfo documentInfo = new DocumentInfo();
289
290                 // get arrays of table names and titles, and find the default table index
291                 final List<String> tablesVec = document.getTableNames();
292
293                 final int numTables = Utils.safeLongToInt(tablesVec.size());
294                 // we don't know how many tables will be hidden so we'll use half of the number of tables for the default size
295                 // of the ArrayList
296                 final ArrayList<String> tableNames = new ArrayList<String>(numTables / 2);
297                 final ArrayList<String> tableTitles = new ArrayList<String>(numTables / 2);
298                 boolean foundDefaultTable = false;
299                 int visibleIndex = 0;
300                 for (int i = 0; i < numTables; i++) {
301                         final String tableName = tablesVec.get(i);
302                         if (!document.getTableIsHidden(tableName)) {
303                                 tableNames.add(tableName);
304
305                                 //The comparison will only be called if we haven't already found the default table
306                                 if (!foundDefaultTable && tableName.equals(document.getDefaultTable())) {
307                                         documentInfo.setDefaultTableIndex(visibleIndex);
308                                         foundDefaultTable = true;
309                                 }
310                                 tableTitles.add(document.getTableTitle(tableName, localeID));
311                                 visibleIndex++;
312                         }
313                 }
314
315                 // set everything we need
316                 documentInfo.setTableNames(tableNames);
317                 documentInfo.setTableTitles(tableTitles);
318                 documentInfo.setTitle(document.getDatabaseTitle(localeID));
319
320                 // Fetch arrays of locale IDs and titles:
321                 final List<String> localesVec = document.getTranslationAvailableLocales();
322                 final int numLocales = Utils.safeLongToInt(localesVec.size());
323                 final ArrayList<String> localeIDs = new ArrayList<String>(numLocales);
324                 final ArrayList<String> localeTitles = new ArrayList<String>(numLocales);
325                 for (int i = 0; i < numLocales; i++) {
326                         final String this_localeID = localesVec.get(i);
327                         localeIDs.add(this_localeID);
328
329                         // Use java.util.Locale to get a title for the locale:
330                         final String[] locale_parts = this_localeID.split("_");
331                         String locale_lang = this_localeID;
332                         if (locale_parts.length > 0) {
333                                 locale_lang = locale_parts[0];
334                         }
335                         String locale_country = "";
336                         if (locale_parts.length > 1) {
337                                 locale_country = locale_parts[1];
338                         }
339
340                         final Locale locale = new Locale(locale_lang, locale_country);
341                         final String title = locale.getDisplayName(locale);
342                         localeTitles.add(title);
343                 }
344                 documentInfo.setLocaleIDs(localeIDs);
345                 documentInfo.setLocaleTitles(localeTitles);
346
347                 return documentInfo;
348         }
349
350         /*
351          * Gets the layout group for the list view using the defined layout list in the document or the table fields if
352          * there's no defined layout group for the list view.
353          */
354         private LayoutGroup getValidListViewLayoutGroup(final String tableName, final String localeID) {
355
356                 // Try to return a cached version:
357                 final LayoutGroup result = mapTableLayouts.getListLayout(tableName, localeID);
358                 if (result != null) {
359                         updateLayoutGroupExpectedResultSize(result, tableName);
360                         return result;
361                 }
362
363                 final List<LayoutGroup> layoutGroupVec = document.getDataLayoutGroups(Document.LAYOUT_NAME_LIST, tableName);
364
365                 final int listViewLayoutGroupSize = Utils.safeLongToInt(layoutGroupVec.size());
366                 LayoutGroup libglomLayoutGroup = null;
367                 if (listViewLayoutGroupSize > 0) {
368                         // A list layout group is defined.
369                         // We use the first group as the list.
370                         if (listViewLayoutGroupSize > 1) {
371                                 Log.warn(documentID, tableName, "The size of the list layout group is greater than 1. "
372                                                 + "Attempting to use the first item for the layout list view.");
373                         }
374
375                         libglomLayoutGroup = layoutGroupVec.get(0);
376                 } else {
377                         // A list layout group is *not* defined; we are going make a LayoutGroup from the list of fields.
378                         // This is unusual.
379                         Log.info(documentID, tableName,
380                                         "A list layout is not defined for this table. Displaying a list layout based on the field list.");
381
382                         final List<Field> fieldsVec = document.getTableFields(tableName);
383                         libglomLayoutGroup = new LayoutGroup();
384                         for (int i = 0; i < fieldsVec.size(); i++) {
385                                 final Field field = fieldsVec.get(i);
386                                 final LayoutItemField layoutItemField = new LayoutItemField();
387                                 layoutItemField.setFullFieldDetails(field);
388                                 libglomLayoutGroup.addItem(layoutItemField);
389                         }
390                 }
391
392                 // TODO: Clone the group and change the clone, to discard unwanted information (such as translations)
393                 // store some information that we do not want to calculate on the client side.
394
395                 // Note that we don't use clone() here, because that would need clone() implementations
396                 // in classes which are also used in the client code (though the clone() methods would
397                 // not be used) and that makes the GWT java->javascript compilation fail.
398                 final LayoutGroup cloned = (LayoutGroup) Utils.deepCopy(libglomLayoutGroup);
399                 if (cloned != null) {
400                         updateTopLevelListLayoutGroup(cloned, tableName, localeID);
401
402                         // Discard unwanted translations so that getTitle(void) returns what we want.
403                         updateTitlesForLocale(cloned, localeID);
404                 }
405
406                 // Store it in the cache for next time.
407                 mapTableLayouts.setListLayout(tableName, localeID, cloned);
408
409                 return cloned;
410         }
411
412         /**
413          * @param libglomLayoutGroup
414          */
415         private void updateTopLevelListLayoutGroup(final LayoutGroup layoutGroup, final String tableName,
416                         final String localeID) {
417                 final List<LayoutItem> layoutItemsVec = layoutGroup.getItems();
418
419                 int primaryKeyIndex = -1;
420
421                 final int numItems = Utils.safeLongToInt(layoutItemsVec.size());
422                 for (int i = 0; i < numItems; i++) {
423                         final LayoutItem layoutItem = layoutItemsVec.get(i);
424
425                         if (layoutItem instanceof LayoutItemField) {
426                                 final LayoutItemField layoutItemField = (LayoutItemField) layoutItem;
427                                 final Field field = layoutItemField.getFullFieldDetails();
428                                 if ((field != null) && field.getPrimaryKey()) {
429                                         primaryKeyIndex = i;
430                                 }
431                         }
432                 }
433
434                 // Set the primary key index for the table
435                 if (primaryKeyIndex < 0) {
436                         // Add a LayoutItemField for the primary key to the end of the item list in the LayoutGroup because it
437                         // doesn't already contain a primary key.
438                         Field primaryKey = null;
439                         final List<Field> fieldsVec = document.getTableFields(tableName);
440                         for (int i = 0; i < Utils.safeLongToInt(fieldsVec.size()); i++) {
441                                 final Field field = fieldsVec.get(i);
442                                 if (field.getPrimaryKey()) {
443                                         primaryKey = field;
444                                         break;
445                                 }
446                         }
447
448                         if (primaryKey != null) {
449                                 final LayoutItemField layoutItemField = new LayoutItemField();
450                                 layoutItemField.setName(primaryKey.getName());
451                                 layoutItemField.setFullFieldDetails(primaryKey);
452                                 layoutGroup.addItem(layoutItemField);
453                                 layoutGroup.setPrimaryKeyIndex(layoutGroup.getItems().size() - 1);
454                                 layoutGroup.setHiddenPrimaryKey(true);
455                         } else {
456                                 Log.error(document.getDatabaseTitleOriginal(), tableName,
457                                                 "A primary key was not found in the FieldVector for this table. Navigation buttons will not work.");
458                         }
459                 } else {
460                         layoutGroup.setPrimaryKeyIndex(primaryKeyIndex);
461                 }
462         }
463
464         private void updateLayoutGroupExpectedResultSize(final LayoutGroup layoutGroup, final String tableName) {
465                 final ListViewDBAccess listViewDBAccess = new ListViewDBAccess(document, documentID, cpds, tableName,
466                                 layoutGroup);
467                 layoutGroup.setExpectedResultSize(listViewDBAccess.getExpectedResultSize());
468         }
469
470         /**
471          * 
472          * @param tableName
473          * @param quickFind
474          * @param start
475          * @param length
476          * @param useSortClause
477          * @param sortColumnIndex
478          *            The index of the column to sort by, or -1 for none.
479          * @param isAscending
480          * @return
481          */
482         ArrayList<DataItem[]> getListViewData(String tableName, final String quickFind, final int start, final int length,
483                         final boolean useSortClause, final int sortColumnIndex, final boolean isAscending) {
484                 // Validate the table name.
485                 tableName = getTableNameToUse(tableName);
486
487                 // Get the LayoutGroup that represents the list view.
488                 // TODO: Performance: Avoid calling this again:
489                 final LayoutGroup libglomLayoutGroup = getValidListViewLayoutGroup(tableName, "" /* irrelevant locale */);
490
491                 // Create a database access object for the list view.
492                 final ListViewDBAccess listViewDBAccess = new ListViewDBAccess(document, documentID, cpds, tableName,
493                                 libglomLayoutGroup);
494
495                 // Return the data.
496                 return listViewDBAccess.getData(quickFind, start, length, sortColumnIndex, isAscending);
497         }
498
499         DataItem[] getDetailsData(String tableName, final TypedDataItem primaryKeyValue) {
500                 // Validate the table name.
501                 tableName = getTableNameToUse(tableName);
502
503                 final DetailsDBAccess detailsDBAccess = new DetailsDBAccess(document, documentID, cpds, tableName);
504
505                 return detailsDBAccess.getData(primaryKeyValue);
506         }
507
508         /**
509          * 
510          * @param tableName
511          * @param portal
512          * @param foreignKeyValue
513          * @param start
514          * @param length
515          * @param sortColumnIndex
516          *            The index of the column to sort by, or -1 for none.
517          * @param isAscending
518          * @return
519          */
520         ArrayList<DataItem[]> getRelatedListData(String tableName, final LayoutItemPortal portal,
521                         final TypedDataItem foreignKeyValue, final int start, final int length, final int sortColumnIndex,
522                         final boolean isAscending) {
523                 if (portal == null) {
524                         Log.error("getRelatedListData(): portal is null");
525                         return null;
526                 }
527
528                 // Validate the table name.
529                 tableName = getTableNameToUse(tableName);
530
531                 // Create a database access object for the related list
532                 final RelatedListDBAccess relatedListDBAccess = new RelatedListDBAccess(document, documentID, cpds, tableName,
533                                 portal);
534
535                 // Return the data
536                 return relatedListDBAccess.getData(start, length, foreignKeyValue, sortColumnIndex, isAscending);
537         }
538
539         List<LayoutGroup> getDetailsLayoutGroup(String tableName, final String localeID) {
540                 // Validate the table name.
541                 tableName = getTableNameToUse(tableName);
542
543                 // Try to return a cached version:
544                 final List<LayoutGroup> result = mapTableLayouts.getDetailsLayout(tableName, localeID);
545                 if (result != null) {
546                         updatePortalsExtras(result, tableName); // Update expected results sizes.
547                         return result;
548                 }
549
550                 final List<LayoutGroup> listGroups = document.getDataLayoutGroups(Document.LAYOUT_NAME_DETAILS, tableName);
551
552                 // Clone the group and change the clone, to discard unwanted information (such as translations)
553                 // and to store some information that we do not want to calculate on the client side.
554
555                 // Note that we don't use clone() here, because that would need clone() implementations
556                 // in classes which are also used in the client code (though the clone() methods would
557                 // not be used) and that makes the GWT java->javascript compilation fail.
558                 final List<LayoutGroup> listCloned = new ArrayList<LayoutGroup>();
559                 for (final LayoutGroup group : listGroups) {
560                         final LayoutGroup cloned = (LayoutGroup) Utils.deepCopy(group);
561                         if (cloned != null) {
562                                 listCloned.add(cloned);
563                         }
564                 }
565
566                 updatePortalsExtras(listCloned, tableName);
567                 updateFieldsExtras(listCloned, tableName);
568
569                 // Discard unwanted translations so that getTitle(void) returns what we want.
570                 updateTitlesForLocale(listCloned, localeID);
571
572                 // Store it in the cache for next time.
573                 mapTableLayouts.setDetailsLayout(tableName, localeID, listCloned);
574
575                 return listCloned;
576         }
577
578         /**
579          * @param result
580          * @param tableName
581          */
582         private void updatePortalsExtras(final List<LayoutGroup> listGroups, final String tableName) {
583                 for (final LayoutGroup group : listGroups) {
584                         updatePortalsExtras(group, tableName);
585                 }
586
587         }
588
589         /**
590          * @param result
591          * @param tableName
592          */
593         private void updateFieldsExtras(final List<LayoutGroup> listGroups, final String tableName) {
594                 for (final LayoutGroup group : listGroups) {
595                         updateFieldsExtras(group, tableName);
596                 }
597         }
598
599         /**
600          * @param result
601          * @param tableName
602          */
603         private void updateTitlesForLocale(final List<LayoutGroup> listGroups, final String localeID) {
604                 for (final LayoutGroup group : listGroups) {
605                         updateTitlesForLocale(group, localeID);
606                 }
607         }
608
609         private void updatePortalsExtras(final LayoutGroup group, final String tableName) {
610                 if (group instanceof LayoutItemPortal) {
611                         final LayoutItemPortal portal = (LayoutItemPortal) group;
612                         final String tableNameUsed = portal.getTableUsed(tableName);
613                         updateLayoutGroupExpectedResultSize(portal, tableNameUsed);
614
615                         //Do not add a primary key field if there is already one:
616                         if(portal.getPrimaryKeyIndex() != -1 )
617                                 return;
618
619                         final Relationship relationship = portal.getRelationship();
620                         if (relationship != null) {
621
622                                 // Cache the navigation information:
623                                 // layoutItemPortal.set_name(libglomLayoutItemPortal.get_relationship_name_used());
624                                 // layoutItemPortal.setTableName(relationship.get_from_table());
625                                 // layoutItemPortal.setFromField(relationship.get_from_field());
626
627                                 // get the primary key for the related list table
628                                 final String toTableName = relationship.getToTable();
629                                 if (!StringUtils.isEmpty(toTableName)) {
630
631                                         // get the LayoutItemField with details from its Field in the document
632                                         final List<Field> fields = document.getTableFields(toTableName); // TODO_Performance: Cache this.
633                                         for (final Field field : fields) {
634                                                 // check the names to see if they're the same
635                                                 if (field.getPrimaryKey()) {
636                                                         final LayoutItemField layoutItemField = new LayoutItemField();
637                                                         layoutItemField.setName(field.getName());
638                                                         layoutItemField.setFullFieldDetails(field);
639                                                         portal.addItem(layoutItemField);
640                                                         portal.setPrimaryKeyIndex(portal.getItems().size() - 1);
641                                                         portal.setHiddenPrimaryKey(true); // always hidden in portals
642                                                         break;
643                                                 }
644                                         }
645                                 }
646                         }
647
648                 }
649
650                 final List<LayoutItem> childItems = group.getItems();
651                 for (final LayoutItem item : childItems) {
652                         if (item instanceof LayoutGroup) {
653                                 final LayoutGroup childGroup = (LayoutGroup) item;
654                                 updatePortalsExtras(childGroup, tableName);
655                         }
656                 }
657
658         }
659
660         private void updateFieldsExtras(final LayoutGroup group, final String tableName) {
661
662                 final List<LayoutItem> childItems = group.getItems();
663                 for (final LayoutItem item : childItems) {
664                         if (item instanceof LayoutGroup) {
665                                 // Recurse:
666                                 final LayoutGroup childGroup = (LayoutGroup) item;
667                                 updateFieldsExtras(childGroup, tableName);
668                         } else if (item instanceof LayoutItemField) {
669                                 final LayoutItemField field = (LayoutItemField) item;
670
671                                 // Set whether the field should have a navigation button,
672                                 // because it identifies a related record.
673                                 final String navigationTableName = document.getLayoutItemFieldShouldHaveNavigation(tableName, field);
674                                 if (navigationTableName != null) {
675                                         field.setNavigationTableName(navigationTableName);
676                                 }
677                         }
678                 }
679         }
680
681         private void updateTitlesForLocale(final LayoutGroup group, final String localeID) {
682
683                 updateItemTitlesForLocale(group, localeID);
684
685                 final List<LayoutItem> childItems = group.getItems();
686                 for (final LayoutItem item : childItems) {
687
688                         // Call makeTitleOriginal on all Translatable items and all special
689                         // Translatable items that they use:
690                         if (item instanceof LayoutItemField) {
691                                 final LayoutItemField layoutItemField = (LayoutItemField) item;
692
693                                 final Field field = layoutItemField.getFullFieldDetails();
694                                 if (field != null) {
695                                         field.makeTitleOriginal(localeID);
696                                 }
697
698                                 final CustomTitle customTitle = layoutItemField.getCustomTitle();
699                                 if (customTitle != null) {
700                                         customTitle.makeTitleOriginal(localeID);
701                                 }
702                         }
703
704                         updateItemTitlesForLocale(item, localeID);
705
706                         if (item instanceof LayoutGroup) {
707                                 // Recurse:
708                                 final LayoutGroup childGroup = (LayoutGroup) item;
709                                 updateTitlesForLocale(childGroup, localeID);
710                         }
711                 }
712         }
713
714         private void updateItemTitlesForLocale(final LayoutItem item, final String localeID) {
715                 if (item instanceof UsesRelationship) {
716                         final UsesRelationship usesRelationship = (UsesRelationship) item;
717                         final Relationship rel = usesRelationship.getRelationship();
718
719                         if (rel != null) {
720                                 rel.makeTitleOriginal(localeID);
721                         }
722
723                         final Relationship relatedRel = usesRelationship.getRelatedRelationship();
724                         if (relatedRel != null) {
725                                 relatedRel.makeTitleOriginal(localeID);
726                         }
727                 }
728
729                 if (item instanceof Translatable) {
730                         final Translatable translatable = item;
731                         translatable.makeTitleOriginal(localeID);
732                 }
733         }
734
735         /*
736          * Gets the expected row count for a related list.
737          */
738         int getRelatedListRowCount(String tableName, final LayoutItemPortal portal, final TypedDataItem foreignKeyValue) {
739                 if (portal == null) {
740                         Log.error("getRelatedListData(): portal is null");
741                         return 0;
742                 }
743
744                 // Validate the table name.
745                 tableName = getTableNameToUse(tableName);
746
747                 // Create a database access object for the related list
748                 final RelatedListDBAccess relatedListDBAccess = new RelatedListDBAccess(document, documentID, cpds, tableName,
749                                 portal);
750
751                 // Return the row count
752                 return relatedListDBAccess.getExpectedResultSize(foreignKeyValue);
753         }
754
755         NavigationRecord getSuitableRecordToViewDetails(String tableName, final LayoutItemPortal portal,
756                         final TypedDataItem primaryKeyValue) {
757                 // Validate the table name.
758                 tableName = getTableNameToUse(tableName);
759
760                 final RelatedListNavigation relatedListNavigation = new RelatedListNavigation(document, documentID, cpds,
761                                 tableName, portal);
762
763                 return relatedListNavigation.getNavigationRecord(primaryKeyValue);
764         }
765
766         LayoutGroup getListViewLayoutGroup(String tableName, final String localeID) {
767                 // Validate the table name.
768                 tableName = getTableNameToUse(tableName);
769                 return getValidListViewLayoutGroup(tableName, localeID);
770         }
771
772         /**
773          * Gets the table name to use when accessing the database and the document. This method guards against SQL injection
774          * attacks by returning the default table if the requested table is not in the database or if the table name has not
775          * been set.
776          * 
777          * @param tableName
778          *            The table name to validate.
779          * @return The table name to use.
780          */
781         private String getTableNameToUse(final String tableName) {
782                 if (StringUtils.isEmpty(tableName) || !document.getTableIsKnown(tableName)) {
783                         return document.getDefaultTable();
784                 }
785                 return tableName;
786         }
787
788         /**
789          * @param tableName
790          * @param localeID
791          * @return
792          */
793         public Reports getReports(final String tableName, final String localeID) {
794                 final Reports result = new Reports();
795
796                 final List<String> names = document.getReportNames(tableName);
797
798                 final int count = Utils.safeLongToInt(names.size());
799                 for (int i = 0; i < count; i++) {
800                         final String name = names.get(i);
801                         final Report report = document.getReport(tableName, name);
802                         if (report == null) {
803                                 continue;
804                         }
805
806                         final String title = report.getTitle(localeID);
807                         result.addReport(name, title);
808                 }
809
810                 return result;
811         }
812 }