Cleanup by Eclipse: Add final keywords.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / server / libglom / Document.java
1 /*
2  * Copyright (C) 2012 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.libglom;
21
22 import java.io.IOException;
23 import java.util.ArrayList;
24 import java.util.Hashtable;
25 import java.util.List;
26
27 import javax.xml.parsers.DocumentBuilder;
28 import javax.xml.parsers.DocumentBuilderFactory;
29 import javax.xml.parsers.ParserConfigurationException;
30
31 import org.apache.commons.lang3.StringUtils;
32 import org.glom.web.shared.libglom.CustomTitle;
33 import org.glom.web.shared.libglom.Field;
34 import org.glom.web.shared.libglom.NumericFormat;
35 import org.glom.web.shared.libglom.Relationship;
36 import org.glom.web.shared.libglom.Report;
37 import org.glom.web.shared.libglom.Translatable;
38 import org.glom.web.shared.libglom.layout.Formatting;
39 import org.glom.web.shared.libglom.layout.LayoutGroup;
40 import org.glom.web.shared.libglom.layout.LayoutItem;
41 import org.glom.web.shared.libglom.layout.LayoutItemField;
42 import org.glom.web.shared.libglom.layout.LayoutItemNotebook;
43 import org.glom.web.shared.libglom.layout.LayoutItemPortal;
44 import org.glom.web.shared.libglom.layout.LayoutItemPortal.NavigationType;
45 import org.glom.web.shared.libglom.layout.TableToViewDetails;
46 import org.glom.web.shared.libglom.layout.UsesRelationship;
47 import org.glom.web.shared.libglom.layout.UsesRelationshipImpl;
48 import org.glom.web.shared.libglom.layout.reportparts.LayoutItemGroupBy;
49 import org.jfree.util.Log;
50 import org.w3c.dom.Element;
51 import org.w3c.dom.Node;
52 import org.w3c.dom.NodeList;
53 import org.xml.sax.SAXException;
54
55 /**
56  * @author Murray Cumming <murrayc@openismus.com>
57  * 
58  */
59 public class Document {
60
61         @SuppressWarnings("serial")
62         private static class TableInfo extends Translatable {
63                 public boolean isDefault;
64                 public boolean isHidden;
65
66                 private final Hashtable<String, Field> fieldsMap = new Hashtable<String, Field>();
67                 private final Hashtable<String, Relationship> relationshipsMap = new Hashtable<String, Relationship>();
68                 private final Hashtable<String, Report> reportsMap = new Hashtable<String, Report>();
69
70                 private List<LayoutGroup> layoutGroupsList = new ArrayList<LayoutGroup>();
71                 private List<LayoutGroup> layoutGroupsDetails = new ArrayList<LayoutGroup>();
72         }
73
74         private String fileURI = "";
75         private org.w3c.dom.Document xmlDocument = null;
76
77         private final Translatable databaseTitle = new Translatable();
78         private String translationOriginalLocale = "";
79         private final List<String> translationAvailableLocales = new ArrayList<String>();
80         private String connectionServer = "";
81         private String connectionDatabase = "";
82         private int connectionPort = 0;
83         private final Hashtable<String, TableInfo> tablesMap = new Hashtable<String, TableInfo>();
84
85         private static final String ATTRIBUTE_TRANSLATION_ORIGINAL_LOCALE = "translation_original_locale";
86         private static final String NODE_CONNECTION = "connection";
87         private static final String ATTRIBUTE_CONNECTION_SERVER = "server";
88         private static final String ATTRIBUTE_CONNECTION_DATABASE = "database";
89         private static final String ATTRIBUTE_CONNECTION_PORT = "port";
90         private static final String NODE_TABLE = "table";
91         private static final String ATTRIBUTE_NAME = "name";
92         private static final String ATTRIBUTE_TITLE = "title";
93         private static final String ATTRIBUTE_DEFAULT = "default";
94         private static final String ATTRIBUTE_HIDDEN = "hidden";
95         private static final String NODE_TRANSLATIONS_SET = "trans_set";
96         private static final String NODE_TRANSLATIONS = "trans";
97         private static final String ATTRIBUTE_TRANSLATION_LOCALE = "loc";
98         private static final String ATTRIBUTE_TRANSLATION_TITLE = "val";
99         private static final String NODE_REPORTS = "reports";
100         private static final String NODE_REPORT = "report";
101         private static final String NODE_FIELDS = "fields";
102         private static final String NODE_FIELD = "field";
103         private static final String ATTRIBUTE_PRIMARY_KEY = "primary_key";
104         private static final String ATTRIBUTE_FIELD_TYPE = "type";
105         private static final String NODE_FORMATTING = "formatting";
106         // private static final String ATTRIBUTE_TEXT_FORMAT_MULTILINE = "format_text_multiline";
107         private static final String ATTRIBUTE_USE_THOUSANDS_SEPARATOR = "format_thousands_separator";
108         private static final String ATTRIBUTE_DECIMAL_PLACES = "format_decimal_places";
109         private static final String NODE_RELATIONSHIPS = "relationships";
110         private static final String NODE_RELATIONSHIP = "relationship";
111         private static final String ATTRIBUTE_RELATIONSHIP_FROM_FIELD = "key";
112         private static final String ATTRIBUTE_RELATIONSHIP_TO_TABLE = "other_table";
113         private static final String ATTRIBUTE_RELATIONSHIP_TO_FIELD = "other_key";
114         private static final String NODE_DATA_LAYOUTS = "data_layouts";
115         private static final String NODE_DATA_LAYOUT = "data_layout";
116         private static final String NODE_DATA_LAYOUT_GROUPS = "data_layout_groups";
117         private static final String NODE_DATA_LAYOUT_GROUP = "data_layout_group";
118         private static final String ATTRIBUTE_LAYOUT_GROUP_COLUMNS_COUNT = "columns_count";
119         private static final String NODE_DATA_LAYOUT_NOTEBOOK = "data_layout_notebook";
120         private static final String NODE_DATA_LAYOUT_PORTAL = "data_layout_portal";
121         private static final String NODE_DATA_LAYOUT_PORTAL_NAVIGATIONRELATIONSHIP = "portal_navigation_relationship";
122         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE = "navigation_type";
123         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE_AUTOMATIC = "automatic";
124         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE_SPECIFIC = "specific";
125         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE_NONE = "none";
126         private static final String ATTRIBUTE_RELATIONSHIP_NAME = "relationship";
127         private static final String ATTRIBUTE_RELATED_RELATIONSHIP_NAME = "related_relationship";
128         private static final String NODE_DATA_LAYOUT_ITEM = "data_layout_item";
129         private static final String NODE_CUSTOM_TITLE = "title_custom";
130         private static final String ATTRIBUTE_CUSTOM_TITLE_USE_CUSTOM = "use_custom";
131         private static final String NODE_DATA_LAYOUT_ITEM_GROUPBY = "data_layout_item_groupby";
132         private static final String NODE_GROUPBY = "groupby";
133         private static final String NODE_SECONDARY_FIELDS = "secondary_fields";
134         private static final String ATTRIBUTE_USE_DEFAULT_FORMATTING = "use_default_formatting";
135         private static final String LAYOUT_NAME_DETAILS = "details";
136         private static final String LAYOUT_NAME_LIST = "list";
137
138         public void setFileURI(final String fileURI) {
139                 this.fileURI = fileURI;
140         }
141
142         public String getFileURI() {
143                 return fileURI;
144         }
145
146         // TODO: Make sure these have the correct values.
147         public enum LoadFailureCodes {
148                 LOAD_FAILURE_CODE_NONE, LOAD_FAILURE_CODE_NOT_FOUND, LOAD_FAILURE_CODE_FILE_VERSION_TOO_NEW
149         };
150
151         public boolean load() {
152                 final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
153                 DocumentBuilder documentBuilder;
154                 try {
155                         documentBuilder = dbf.newDocumentBuilder();
156                 } catch (final ParserConfigurationException e) {
157                         // TODO Auto-generated catch block
158                         e.printStackTrace();
159                         return false;
160                 }
161
162                 try {
163                         xmlDocument = documentBuilder.parse(fileURI);
164                 } catch (final SAXException e) {
165                         // TODO Auto-generated catch block
166                         e.printStackTrace();
167                         return false;
168                 } catch (final IOException e) {
169                         // TODO Auto-generated catch block
170                         e.printStackTrace();
171                         return false;
172                 }
173
174                 final Element rootNode = xmlDocument.getDocumentElement();
175                 if (rootNode.getNodeName() != "glom_document") {
176                         Log.error("Unexpected XML root node name found: " + rootNode.getNodeName());
177                         return false;
178                 }
179
180                 databaseTitle.setTitleOriginal(rootNode.getAttribute(ATTRIBUTE_TITLE));
181
182                 translationOriginalLocale = rootNode.getAttribute(ATTRIBUTE_TRANSLATION_ORIGINAL_LOCALE);
183                 translationAvailableLocales.add(translationOriginalLocale); // Just a cache.
184
185                 final Element nodeConnection = getElementByName(rootNode, NODE_CONNECTION);
186                 if (nodeConnection != null) {
187                         connectionServer = nodeConnection.getAttribute(ATTRIBUTE_CONNECTION_SERVER);
188                         connectionDatabase = nodeConnection.getAttribute(ATTRIBUTE_CONNECTION_DATABASE);
189                         connectionPort = getAttributeAsDecimal(nodeConnection, ATTRIBUTE_CONNECTION_PORT);
190                 }
191
192                 // We first load the fields, relationships, etc,
193                 // for all tables:
194                 final List<Node> listTableNodes = getChildrenByTagName(rootNode, NODE_TABLE);
195                 for (final Node node : listTableNodes) {
196                         if (!(node instanceof Element)) {
197                                 continue;
198                         }
199
200                         final Element element = (Element) node;
201                         final TableInfo info = loadTableNodeBasic(element);
202                         tablesMap.put(info.getName(), info);
203                 }
204
205                 // We then load the layouts for all tables, because they
206                 // need the fields and relationships for all tables:
207                 for (final Node node : listTableNodes) {
208                         if (!(node instanceof Element)) {
209                                 continue;
210                         }
211
212                         final Element element = (Element) node;
213                         final String tableName = element.getAttribute(ATTRIBUTE_NAME);
214
215                         // We first load the fields, relationships, etc:
216                         final TableInfo info = getTableInfo(tableName);
217                         if (info == null) {
218                                 continue;
219                         }
220
221                         // We then load the layouts afterwards, because they
222                         // need the fields and relationships:
223                         loadTableLayouts(element, info);
224
225                         tablesMap.put(info.getName(), info);
226                 }
227
228                 return true;
229         };
230
231         private Element getElementByName(final Element parentElement, final String tagName) {
232                 final List<Node> listNodes = getChildrenByTagName(parentElement, tagName);
233                 if (listNodes == null) {
234                         return null;
235                 }
236
237                 if (listNodes.size() == 0) {
238                         return null;
239                 }
240
241                 return (Element) listNodes.get(0);
242         }
243
244         private boolean getAttributeAsBoolean(final Element node, final String attributeName) {
245                 final String str = node.getAttribute(attributeName);
246                 if (str == null) {
247                         return false;
248                 }
249
250                 return str.equals("true");
251         }
252
253         /**
254          * @param elementFormatting
255          * @param aTTRIBUTE_DECIMAL_PLACES2
256          * @return
257          */
258         private int getAttributeAsDecimal(final Element node, final String attributeName) {
259                 final String str = node.getAttribute(attributeName);
260                 if (StringUtils.isEmpty(str)) {
261                         return 0;
262                 }
263
264                 return Integer.valueOf(str);
265         }
266
267         /**
268          * Load a title and its translations.
269          * 
270          * @param node
271          *            The XML Element that may contain a title attribute and a trans_set of translations of the title.
272          * @param title
273          */
274         private void loadTitle(final Element node, final Translatable title) {
275                 title.setName(node.getAttribute(ATTRIBUTE_NAME));
276
277                 title.setTitleOriginal(node.getAttribute(ATTRIBUTE_TITLE));
278
279                 final Element nodeSet = getElementByName(node, NODE_TRANSLATIONS_SET);
280                 if (nodeSet == null) {
281                         return;
282                 }
283
284                 final List<Node> listNodes = getChildrenByTagName(nodeSet, NODE_TRANSLATIONS);
285                 if (listNodes == null) {
286                         return;
287                 }
288
289                 for (final Node transNode : listNodes) {
290                         if (!(transNode instanceof Element)) {
291                                 continue;
292                         }
293
294                         final Element element = (Element) transNode;
295
296                         final String locale = element.getAttribute(ATTRIBUTE_TRANSLATION_LOCALE);
297                         final String translatedTitle = element.getAttribute(ATTRIBUTE_TRANSLATION_TITLE);
298                         if (!StringUtils.isEmpty(locale) && !StringUtils.isEmpty(translatedTitle)) {
299                                 title.setTitle(translatedTitle, locale);
300
301                                 // Remember any new translation locales in our cached list:
302                                 if (!translationAvailableLocales.contains(locale)) {
303                                         translationAvailableLocales.add(locale);
304                                 }
305                         }
306                 }
307         }
308
309         /**
310          * @param tableNode
311          * @return
312          */
313         private TableInfo loadTableNodeBasic(final Element tableNode) {
314                 final TableInfo info = new TableInfo();
315                 loadTitle(tableNode, info);
316                 final String tableName = info.getName();
317
318                 info.isDefault = getAttributeAsBoolean(tableNode, ATTRIBUTE_DEFAULT);
319                 info.isHidden = getAttributeAsBoolean(tableNode, ATTRIBUTE_HIDDEN);
320
321                 // These should be loaded before the fields, because the fields use them.
322                 final Element relationshipsNode = getElementByName(tableNode, NODE_RELATIONSHIPS);
323                 if (relationshipsNode != null) {
324                         final List<Node> listNodes = getChildrenByTagName(relationshipsNode, NODE_RELATIONSHIP);
325                         for (final Node node : listNodes) {
326                                 if (!(node instanceof Element)) {
327                                         continue;
328                                 }
329
330                                 final Element element = (Element) node;
331                                 final Relationship relationship = new Relationship();
332                                 loadTitle(element, relationship);
333                                 relationship.setFromTable(tableName);
334                                 relationship.setFromField(element.getAttribute(ATTRIBUTE_RELATIONSHIP_FROM_FIELD));
335                                 relationship.setToTable(element.getAttribute(ATTRIBUTE_RELATIONSHIP_TO_TABLE));
336                                 relationship.setToField(element.getAttribute(ATTRIBUTE_RELATIONSHIP_TO_FIELD));
337
338                                 info.relationshipsMap.put(relationship.getName(), relationship);
339                         }
340                 }
341
342                 final Element fieldsNode = getElementByName(tableNode, NODE_FIELDS);
343                 if (fieldsNode != null) {
344                         final List<Node> listNodes = getChildrenByTagName(fieldsNode, NODE_FIELD);
345                         for (final Node node : listNodes) {
346                                 if (!(node instanceof Element)) {
347                                         continue;
348                                 }
349
350                                 final Element element = (Element) node;
351                                 final Field field = new Field();
352                                 loadField(element, field);
353
354                                 info.fieldsMap.put(field.getName(), field);
355                         }
356                 }
357
358                 return info;
359         }
360
361         /**
362          * @param tableNode
363          * @param info
364          */
365         private void loadTableLayouts(final Element tableNode, final TableInfo info) {
366                 final String tableName = info.getName();
367
368                 final Element layoutsNode = getElementByName(tableNode, NODE_DATA_LAYOUTS);
369                 if (layoutsNode != null) {
370                         final List<Node> listNodes = getChildrenByTagName(layoutsNode, NODE_DATA_LAYOUT);
371                         for (final Node node : listNodes) {
372                                 if (!(node instanceof Element)) {
373                                         continue;
374                                 }
375
376                                 final Element element = (Element) node;
377                                 final String name = element.getAttribute("name");
378                                 final List<LayoutGroup> listLayoutGroups = loadLayoutNode(element, tableName);
379                                 if (name.equals(LAYOUT_NAME_DETAILS)) {
380                                         info.layoutGroupsDetails = listLayoutGroups;
381                                 } else if (name.equals(LAYOUT_NAME_LIST)) {
382                                         info.layoutGroupsList = listLayoutGroups;
383                                 } else {
384                                         Log.error("loadTableNode(): unexpected layout name: " + name);
385                                 }
386                         }
387                 }
388
389                 final Element reportsNode = getElementByName(tableNode, NODE_REPORTS);
390                 if (reportsNode != null) {
391                         final List<Node> listNodes = getChildrenByTagName(reportsNode, NODE_REPORT);
392                         for (final Node node : listNodes) {
393                                 if (!(node instanceof Element)) {
394                                         continue;
395                                 }
396
397                                 final Element element = (Element) node;
398                                 final Report report = new Report();
399                                 loadReport(element, report, tableName);
400
401                                 info.reportsMap.put(report.getName(), report);
402                         }
403                 }
404         }
405
406         /**
407          * @param node
408          * @return
409          */
410         private List<LayoutGroup> loadLayoutNode(final Element node, final String tableName) {
411                 if (node == null) {
412                         return null;
413                 }
414
415                 final List<LayoutGroup> result = new ArrayList<LayoutGroup>();
416
417                 final List<Node> listNodes = getChildrenByTagName(node, NODE_DATA_LAYOUT_GROUPS);
418                 for (final Node nodeGroups : listNodes) {
419                         if (!(nodeGroups instanceof Element)) {
420                                 continue;
421                         }
422
423                         final Element elementGroups = (Element) nodeGroups;
424
425                         final NodeList list = elementGroups.getChildNodes();
426                         final int num = list.getLength();
427                         for (int i = 0; i < num; i++) {
428                                 final Node nodeLayoutGroup = list.item(i);
429                                 if (nodeLayoutGroup == null) {
430                                         continue;
431                                 }
432
433                                 if (!(nodeLayoutGroup instanceof Element)) {
434                                         continue;
435                                 }
436
437                                 final Element element = (Element) nodeLayoutGroup;
438                                 final String tagName = element.getTagName();
439                                 if (tagName == NODE_DATA_LAYOUT_GROUP) {
440                                         final LayoutGroup group = new LayoutGroup();
441                                         loadDataLayoutGroup(element, group, tableName);
442                                         result.add(group);
443                                 } else if (tagName == NODE_DATA_LAYOUT_NOTEBOOK) {
444                                         final LayoutItemNotebook group = new LayoutItemNotebook();
445                                         loadDataLayoutGroup(element, group, tableName);
446                                         result.add(group);
447                                 } else if (tagName == NODE_DATA_LAYOUT_PORTAL) {
448                                         final LayoutItemPortal portal = new LayoutItemPortal();
449                                         loadDataLayoutPortal(element, portal, tableName);
450                                         result.add(portal);
451                                 }
452                         }
453                 }
454
455                 return result;
456         }
457
458         /**
459          * @param element
460          * @param tableName
461          * @param portal
462          */
463         private void loadUsesRelationship(final Element element, final String tableName, final UsesRelationship item) {
464                 if (element == null) {
465                         return;
466                 }
467
468                 if (item == null) {
469                         return;
470                 }
471
472                 final String relationshipName = element.getAttribute(ATTRIBUTE_RELATIONSHIP_NAME);
473                 Relationship relationship = null;
474                 if (!StringUtils.isEmpty(relationshipName)) {
475                         // std::cout << "  debug in : tableName=" << tableName << ", relationshipName=" << relationship_name <<
476                         // std::endl;
477                         relationship = getRelationship(tableName, relationshipName);
478                         item.setRelationship(relationship);
479
480                         if (relationship == null) {
481                                 Log.error("relationship not found: " + relationshipName + ", in table: " + tableName);
482                         }
483                 }
484
485                 // TODO: Unit test loading of doubly-related fields.
486                 final String relatedRelationshipName = element.getAttribute(ATTRIBUTE_RELATED_RELATIONSHIP_NAME);
487                 if (!StringUtils.isEmpty(relatedRelationshipName) && (relationship != null)) {
488                         final Relationship relatedRelationship = getRelationship(relationship.getToTable(), relatedRelationshipName);
489                         item.setRelatedRelationship(relatedRelationship);
490
491                         if (relatedRelationship == null) {
492                                 Log.error("related relationship not found in table=" + relationship.getToTable() + ",  name="
493                                                 + relatedRelationshipName);
494                         }
495                 }
496         }
497
498         /**
499          * getElementsByTagName() is recursive, but we do not want that.
500          * 
501          * @param node
502          * @param
503          * @return
504          */
505         private List<Node> getChildrenByTagName(final Element parentNode, final String tagName) {
506                 final List<Node> result = new ArrayList<Node>();
507
508                 final NodeList list = parentNode.getElementsByTagName(tagName);
509                 final int num = list.getLength();
510                 for (int i = 0; i < num; i++) {
511                         final Node node = list.item(i);
512                         if (node == null) {
513                                 continue;
514                         }
515
516                         final Node itemParentNode = node.getParentNode();
517                         if (itemParentNode.equals(parentNode)) {
518                                 result.add(node);
519                         }
520                 }
521
522                 return result;
523         }
524
525         /**
526          * @param element
527          * @param group
528          */
529         private void loadDataLayoutGroup(final Element nodeGroup, final LayoutGroup group, final String tableName) {
530                 loadTitle(nodeGroup, group);
531
532                 // Read the column count:
533                 int columnCount = getAttributeAsDecimal(nodeGroup, ATTRIBUTE_LAYOUT_GROUP_COLUMNS_COUNT);
534                 if (columnCount < 1) {
535                         columnCount = 1; // 0 is a useless default.
536                 }
537                 group.setColumnCount(columnCount);
538
539                 // Get the child items:
540                 final NodeList listNodes = nodeGroup.getChildNodes();
541                 final int num = listNodes.getLength();
542                 for (int i = 0; i < num; i++) {
543                         final Node node = listNodes.item(i);
544                         if (!(node instanceof Element)) {
545                                 continue;
546                         }
547
548                         final Element element = (Element) node;
549                         final String tagName = element.getTagName();
550                         if (tagName == NODE_DATA_LAYOUT_GROUP) {
551                                 final LayoutGroup childGroup = new LayoutGroup();
552                                 loadDataLayoutGroup(element, childGroup, tableName);
553                                 group.addItem(childGroup);
554                         } else if (tagName == NODE_DATA_LAYOUT_NOTEBOOK) {
555                                 final LayoutItemNotebook childGroup = new LayoutItemNotebook();
556                                 loadDataLayoutGroup(element, childGroup, tableName);
557                                 group.addItem(childGroup);
558                         } else if (tagName == NODE_DATA_LAYOUT_PORTAL) {
559                                 final LayoutItemPortal childGroup = new LayoutItemPortal();
560                                 loadDataLayoutPortal(element, childGroup, tableName);
561                                 group.addItem(childGroup);
562                         } else if (element.getTagName() == NODE_DATA_LAYOUT_ITEM) {
563                                 final LayoutItemField item = new LayoutItemField();
564                                 loadDataLayoutItemField(element, item, tableName);
565                                 group.addItem(item);
566                         } else if (element.getTagName() == NODE_DATA_LAYOUT_ITEM_GROUPBY) {
567                                 final LayoutItemGroupBy item = new LayoutItemGroupBy();
568                                 loadDataLayoutItemGroupBy(element, item, tableName);
569                                 group.addItem(item);
570                         }
571                 }
572         }
573
574         /**
575          * @param element
576          * @param item
577          * @param tableName
578          */
579         private void loadDataLayoutItemGroupBy(final Element element, final LayoutItemGroupBy item, final String tableName) {
580                 loadDataLayoutGroup(element, item, tableName);
581
582                 final Element elementGroupBy = getElementByName(element, NODE_GROUPBY);
583                 if (elementGroupBy == null) {
584                         return;
585                 }
586
587                 final LayoutItemField fieldGroupBy = new LayoutItemField();
588                 loadDataLayoutItemField(elementGroupBy, fieldGroupBy, tableName);
589                 item.setFieldGroupBy(fieldGroupBy);
590
591                 final Element elementSecondaryFields = getElementByName(element, NODE_SECONDARY_FIELDS);
592                 if (elementSecondaryFields == null) {
593                         return;
594                 }
595
596                 final Element elementLayoutGroup = getElementByName(elementSecondaryFields, NODE_DATA_LAYOUT_GROUP);
597                 if (elementLayoutGroup != null) {
598                         final LayoutGroup secondaryLayoutGroup = new LayoutGroup();
599                         loadDataLayoutGroup(elementLayoutGroup, secondaryLayoutGroup, tableName);
600                         item.setSecondaryFields(secondaryLayoutGroup);
601                 }
602         }
603
604         /**
605          * @param element
606          * @param item
607          */
608         private void loadDataLayoutItemField(final Element element, final LayoutItemField item, final String tableName) {
609                 item.setName(element.getAttribute(ATTRIBUTE_NAME));
610                 loadUsesRelationship(element, tableName, item);
611
612                 final Element elementCustomTitle = getElementByName(element, NODE_CUSTOM_TITLE);
613                 if (elementCustomTitle != null) {
614                         final CustomTitle customTitle = item.getCustomTitle();
615                         customTitle.setUseCustomTitle(getAttributeAsBoolean(elementCustomTitle, ATTRIBUTE_CUSTOM_TITLE_USE_CUSTOM));
616                         loadTitle(elementCustomTitle, customTitle); // LayoutItemField doesn't use its own title member.
617                 }
618
619                 // Get the actual field:
620                 final String fieldName = item.getName();
621                 final String inTableName = item.getTableUsed(tableName);
622                 final Field field = getField(inTableName, fieldName);
623                 item.setFullFieldDetails(field);
624
625                 item.setUseDefaultFormatting(getAttributeAsBoolean(element, ATTRIBUTE_USE_DEFAULT_FORMATTING));
626
627                 final Element elementFormatting = getElementByName(element, NODE_FORMATTING);
628                 if (elementFormatting != null) {
629                         loadFormatting(elementFormatting, item.getFormatting());
630                 }
631         }
632
633         /**
634          * @param element
635          * @param childGroup
636          */
637         private void loadDataLayoutPortal(final Element element, final LayoutItemPortal portal, final String tableName) {
638                 loadUsesRelationship(element, tableName, portal);
639                 final String relatedTableName = portal.getTableUsed(tableName);
640                 loadDataLayoutGroup(element, portal, relatedTableName);
641
642                 final Element elementNavigation = getElementByName(element, NODE_DATA_LAYOUT_PORTAL_NAVIGATIONRELATIONSHIP);
643                 if (elementNavigation != null) {
644                         final String navigationTypeAsString = elementNavigation.getAttribute(ATTRIBUTE_PORTAL_NAVIGATION_TYPE);
645                         if (StringUtils.isEmpty(navigationTypeAsString)
646                                         || navigationTypeAsString == ATTRIBUTE_PORTAL_NAVIGATION_TYPE_AUTOMATIC) {
647                                 portal.setNavigationType(LayoutItemPortal.NavigationType.NAVIGATION_AUTOMATIC);
648                         } else if (navigationTypeAsString == ATTRIBUTE_PORTAL_NAVIGATION_TYPE_NONE) {
649                                 portal.setNavigationType(LayoutItemPortal.NavigationType.NAVIGATION_NONE);
650                         } else if (navigationTypeAsString == ATTRIBUTE_PORTAL_NAVIGATION_TYPE_SPECIFIC) {
651                                 // Read the specified relationship name:
652                                 final UsesRelationship relationshipNavigationSpecific = new UsesRelationshipImpl();
653                                 loadUsesRelationship(elementNavigation, relatedTableName, relationshipNavigationSpecific);
654                                 portal.setNavigationRelationshipSpecific(relationshipNavigationSpecific);
655                         }
656                 }
657
658         }
659
660         /**
661          * @param element
662          * @param field
663          */
664         private void loadField(final Element element, final Field field) {
665                 loadTitle(element, field);
666
667                 Field.GlomFieldType fieldType = Field.GlomFieldType.TYPE_INVALID;
668                 final String fieldTypeStr = element.getAttribute(ATTRIBUTE_FIELD_TYPE);
669                 if (!StringUtils.isEmpty(fieldTypeStr)) {
670                         if (fieldTypeStr.equals("Boolean")) {
671                                 fieldType = Field.GlomFieldType.TYPE_BOOLEAN;
672                         } else if (fieldTypeStr.equals("Date")) {
673                                 fieldType = Field.GlomFieldType.TYPE_DATE;
674                         } else if (fieldTypeStr.equals("Image")) {
675                                 fieldType = Field.GlomFieldType.TYPE_IMAGE;
676                         } else if (fieldTypeStr.equals("Number")) {
677                                 fieldType = Field.GlomFieldType.TYPE_NUMERIC;
678                         } else if (fieldTypeStr.equals("Text")) {
679                                 fieldType = Field.GlomFieldType.TYPE_TEXT;
680                         } else if (fieldTypeStr.equals("Time")) {
681                                 fieldType = Field.GlomFieldType.TYPE_TIME;
682                         }
683                 }
684
685                 field.setGlomFieldType(fieldType);
686
687                 field.setPrimaryKey(getAttributeAsBoolean(element, ATTRIBUTE_PRIMARY_KEY));
688                 loadTitle(element, field);
689
690                 final Element elementFormatting = getElementByName(element, NODE_FORMATTING);
691                 if (elementFormatting != null) {
692                         loadFormatting(elementFormatting, field.getFormatting());
693                 }
694         }
695
696         /**
697          * @param elementFormatting
698          * @param formatting
699          */
700         private void loadFormatting(final Element elementFormatting, final Formatting formatting) {
701                 if (elementFormatting == null) {
702                         return;
703                 }
704
705                 if (formatting == null) {
706                         return;
707                 }
708
709                 // formatting.setTextFormatMultiline(getAttributeAsBoolean(elementFormatting, ATTRIBUTE_TEXT_FORMAT_MULTILINE));
710
711                 final NumericFormat numericFormatting = formatting.getNumericFormat();
712                 if (numericFormatting != null) {
713                         numericFormatting.setUseThousandsSeparator(getAttributeAsBoolean(elementFormatting,
714                                         ATTRIBUTE_USE_THOUSANDS_SEPARATOR));
715                         numericFormatting.setDecimalPlaces(getAttributeAsDecimal(elementFormatting, ATTRIBUTE_DECIMAL_PLACES));
716                 }
717
718         }
719
720         /**
721          * @param element
722          * @param reportNode
723          */
724         private void loadReport(final Element element, final Report report, final String tableName) {
725                 report.setName(element.getAttribute(ATTRIBUTE_NAME));
726                 loadTitle(element, report);
727
728                 final List<LayoutGroup> listLayoutGroups = loadLayoutNode(element, tableName);
729
730                 // A report can actually only have one LayoutGroup,
731                 // though it uses the same XML structure as List and Details layouts,
732                 // which (wrongly) suggests that it can have more than one group.
733                 LayoutGroup layoutGroup = null;
734                 if (!listLayoutGroups.isEmpty()) {
735                         layoutGroup = listLayoutGroups.get(0);
736                 }
737
738                 report.setLayoutGroup(layoutGroup);
739         }
740
741         private TableInfo getTableInfo(final String tableName) {
742                 return tablesMap.get(tableName);
743         }
744
745         public enum HostingMode {
746                 HOSTING_MODE_POSTGRES_CENTRAL, HOSTING_MODE_POSTGRES_SELF, HOSTING_MODE_SQLITE
747         };
748
749         public String getDatabaseTitle(final String locale) {
750                 return databaseTitle.getTitle(locale);
751         }
752
753         public String getDatabaseTitleOriginal() {
754                 return databaseTitle.getTitleOriginal();
755         }
756
757         public List<String> getTranslationAvailableLocales() {
758                 return translationAvailableLocales;
759         }
760
761         public Document.HostingMode getHostingMode() {
762                 return HostingMode.HOSTING_MODE_POSTGRES_CENTRAL; // TODO
763         }
764
765         public String getConnectionServer() {
766                 return connectionServer;
767         }
768
769         public long getConnectionPort() {
770                 return connectionPort;
771         }
772
773         public String getConnectionDatabase() {
774                 return connectionDatabase;
775         }
776
777         public List<String> getTableNames() {
778                 // TODO: Return a Set?
779                 return new ArrayList<String>(tablesMap.keySet());
780         }
781
782         public boolean getTableIsHidden(final String tableName) {
783                 final TableInfo info = getTableInfo(tableName);
784                 if (info == null) {
785                         return false;
786                 }
787
788                 return info.isHidden;
789         }
790
791         public String getTableTitle(final String tableName, final String locale) {
792                 final TableInfo info = getTableInfo(tableName);
793                 if (info == null) {
794                         return "";
795                 }
796
797                 return info.getTitle(locale);
798         }
799
800         public String getDefaultTable() {
801                 for (final TableInfo info : tablesMap.values()) {
802                         if (info.isDefault) {
803                                 return info.getName();
804                         }
805                 }
806
807                 return "";
808         }
809
810         public boolean getTableIsKnown(final String tableName) {
811                 final TableInfo info = getTableInfo(tableName);
812                 if (info == null) {
813                         return false;
814                 }
815
816                 return true;
817         }
818
819         public List<Field> getTableFields(final String tableName) {
820                 final TableInfo info = getTableInfo(tableName);
821                 if (info == null) {
822                         return null;
823                 }
824
825                 return new ArrayList<Field>(info.fieldsMap.values());
826         }
827
828         public Field getField(final String tableName, final String strFieldName) {
829                 final TableInfo info = getTableInfo(tableName);
830                 if (info == null) {
831                         return null;
832                 }
833
834                 return info.fieldsMap.get(strFieldName);
835         }
836
837         public List<LayoutGroup> getDataLayoutGroups(final String layoutName, final String parentTableName) {
838                 final TableInfo info = getTableInfo(parentTableName);
839                 if (info == null) {
840                         return new ArrayList<LayoutGroup>();
841                 }
842
843                 if (layoutName == LAYOUT_NAME_DETAILS) {
844                         return info.layoutGroupsDetails;
845                 } else if (layoutName == LAYOUT_NAME_LIST) {
846                         return info.layoutGroupsList;
847                 } else {
848                         return new ArrayList<LayoutGroup>();
849                 }
850         }
851
852         public List<String> getReportNames(final String tableName) {
853                 final TableInfo info = getTableInfo(tableName);
854                 if (info == null) {
855                         return new ArrayList<String>();
856                 }
857
858                 return new ArrayList<String>(info.reportsMap.keySet());
859         }
860
861         public Report getReport(final String tableName, final String reportName) {
862                 final TableInfo info = getTableInfo(tableName);
863                 if (info == null) {
864                         return null;
865                 }
866
867                 return info.reportsMap.get(reportName);
868         }
869
870         /**
871          * @param tableName
872          * @param field
873          * @return
874          */
875         public Relationship getFieldUsedInRelationshipToOne(final String tableName, final LayoutItemField layoutField) {
876
877                 if (layoutField == null) {
878                         Log.error("layoutField was null");
879                         return null;
880                 }
881
882                 Relationship result = null;
883
884                 final String tableUsed = layoutField.getTableUsed(tableName);
885                 final TableInfo info = getTableInfo(tableUsed);
886                 if (info == null) {
887                         // This table is special. We would not create a relationship to it using a field:
888                         // if(tableUsed == GLOM_STANDARD_TABLE_PREFS_TABLE_NAME)
889                         // return result;
890
891                         Log.error("table not found: " + tableUsed);
892                         return null;
893                 }
894
895                 // Look at each relationship:
896                 final String fieldName = layoutField.getName();
897                 for (final Relationship relationship : info.relationshipsMap.values()) {
898                         if (relationship != null) {
899                                 // If the relationship uses the field
900                                 if (StringUtils.equals(relationship.getFromField(), fieldName)) {
901                                         // if the to_table is not hidden:
902                                         if (!getTableIsHidden(relationship.getToTable())) {
903                                                 // TODO_Performance: The use of this convenience method means we get the full relationship
904                                                 // information again:
905                                                 if (getRelationshipIsToOne(tableName, relationship.getName())) {
906                                                         result = relationship;
907                                                 }
908                                         }
909                                 }
910                         }
911                 }
912
913                 return result;
914         }
915
916         /**
917          * @param tableName
918          * @param relationshipName
919          * @return
920          */
921         private boolean getRelationshipIsToOne(final String tableName, final String relationshipName) {
922                 final Relationship relationship = getRelationship(tableName, relationshipName);
923                 if (relationship != null) {
924                         final Field fieldTo = getField(relationship.getToTable(), relationship.getToField());
925                         if (fieldTo != null) {
926                                 return (fieldTo.getPrimaryKey() || fieldTo.getUniqueKey());
927                         }
928                 }
929
930                 return false;
931         }
932
933         /**
934          * @param tableName
935          * @param relationshipName
936          * @return
937          */
938         private Relationship getRelationship(final String tableName, final String relationshipName) {
939                 final TableInfo info = getTableInfo(tableName);
940                 if (info == null) {
941                         Log.error("table not found: " + tableName);
942                         return null;
943                 }
944
945                 return info.relationshipsMap.get(relationshipName);
946         }
947
948         /**
949          * @param tableName
950          *            Output parameter
951          * @param relationship
952          * @param portal
953          *            TODO
954          */
955         public TableToViewDetails getPortalSuitableTableToViewDetails(final LayoutItemPortal portal) {
956                 UsesRelationship navigationRelationship = null;
957
958                 // Check whether a relationship was specified:
959                 if (portal.getNavigationType() == NavigationType.NAVIGATION_AUTOMATIC) {
960                         navigationRelationship = getPortalNavigationRelationshipAutomatic(portal);
961                 } else {
962                         navigationRelationship = portal.getNavigationRelationshipSpecific();
963                 }
964
965                 // Get the navigation table name from the chosen relationship:
966                 final String directlyRelatedTableName = portal.getTableUsed("" /* not relevant */);
967
968                 // The navigation_table_name (and therefore, the table_name output parameter,
969                 // as well) stays empty if the navrel type was set to none.
970                 String navigationTableName = null;
971                 if (navigationRelationship != null) {
972                         navigationTableName = navigationRelationship.getTableUsed(directlyRelatedTableName);
973                 } else if (portal.getNavigationType() != NavigationType.NAVIGATION_NONE) {
974                         // An empty result from get_portal_navigation_relationship_automatic() or
975                         // get_navigation_relationship_specific() means we should use the directly related table:
976                         navigationTableName = directlyRelatedTableName;
977                 }
978
979                 if (StringUtils.isEmpty(navigationTableName)) {
980                         return null;
981                 }
982
983                 if (this == null) {
984                         Log.error("document is null.");
985                         return null;
986                 }
987
988                 if (getTableIsHidden(navigationTableName)) {
989                         Log.error("navigation_table_name indicates a hidden table: " + navigationTableName);
990                         return null;
991                 }
992
993                 final TableToViewDetails result = new TableToViewDetails();
994                 result.tableName = navigationTableName;
995                 result.usesRelationship = navigationRelationship;
996                 return result;
997         }
998
999         /**
1000          * @param portal
1001          *            TODO
1002          * @return
1003          */
1004         private UsesRelationship getPortalNavigationRelationshipAutomatic(final LayoutItemPortal portal) {
1005                 if (this == null) {
1006                         return null;
1007                 }
1008
1009                 // If the related table is not hidden then we can just navigate to that:
1010                 final String direct_related_table_name = portal.getTableUsed("" /* parent table - not relevant */);
1011                 if (!getTableIsHidden(direct_related_table_name)) {
1012                         // Non-hidden tables can just be shown directly. Navigate to it:
1013                         return null;
1014                 } else {
1015                         // If the related table is hidden,
1016                         // then find a suitable related non-hidden table by finding the first layout field that mentions one:
1017                         final LayoutItemField field = getPortalFieldIsFromNonHiddenRelatedRecord(portal);
1018                         if (field != null) {
1019                                 return field; // Returns the UsesRelationship base part. (A relationship belonging to the portal's
1020                                                                 // related table.)
1021                         } else {
1022                                 // Instead, find a key field that's used in a relationship,
1023                                 // and pretend that we are showing the to field as a related field:
1024                                 final Relationship fieldIndentifies = getPortalFieldIdentifiesNonHiddenRelatedRecord(portal);
1025                                 if (fieldIndentifies != null) {
1026                                         final UsesRelationship result = new UsesRelationshipImpl();
1027                                         result.setRelationship(fieldIndentifies);
1028                                         return result;
1029                                 }
1030                         }
1031                 }
1032
1033                 // There was no suitable related table to show:
1034                 return null;
1035         }
1036
1037         /**
1038          * @param portal
1039          *            TODO
1040          * @return
1041          */
1042         private LayoutItemField getPortalFieldIsFromNonHiddenRelatedRecord(final LayoutItemPortal portal) {
1043                 // Find the first field that is from a non-hidden related table.
1044
1045                 if (this == null) {
1046                         return null;
1047                 }
1048
1049                 final LayoutItemField result = null;
1050
1051                 final String parent_table_name = portal.getTableUsed("" /* parent table - not relevant */);
1052
1053                 final List<LayoutItem> items = portal.getItems();
1054                 for (final LayoutItem item : items) {
1055                         if (item instanceof LayoutItemField) {
1056                                 final LayoutItemField field = (LayoutItemField) item;
1057                                 if (field.getHasRelationshipName()) {
1058                                         final String table_name = field.getTableUsed(parent_table_name);
1059                                         if (!(getTableIsHidden(table_name))) {
1060                                                 return field;
1061                                         }
1062                                 }
1063                         }
1064                 }
1065
1066                 return result;
1067         }
1068
1069         /**
1070          * @param used_in_relationship
1071          * @param portal
1072          *            TODO
1073          * @return
1074          */
1075         private Relationship getPortalFieldIdentifiesNonHiddenRelatedRecord(final LayoutItemPortal portal) {
1076                 // Find the first field that is from a non-hidden related table.
1077
1078                 if (this == null) {
1079                         Log.error("document is null");
1080                         return null;
1081                 }
1082
1083                 final String parent_table_name = portal.getTableUsed("" /* parent table - not relevant */);
1084
1085                 final List<LayoutItem> items = portal.getItems();
1086                 for (final LayoutItem item : items) {
1087                         if (item instanceof LayoutItemField) {
1088                                 final LayoutItemField field = (LayoutItemField) item;
1089                                 if (field.getHasRelationshipName()) {
1090                                         final Relationship relationship = getFieldUsedInRelationshipToOne(parent_table_name, field);
1091                                         if (relationship != null) {
1092                                                 final String table_name = relationship.getToTable();
1093                                                 if (!StringUtils.isEmpty(table_name)) {
1094                                                         if (!(getTableIsHidden(table_name))) {
1095                                                                 return relationship;
1096                                                         }
1097                                                 }
1098                                         }
1099                                 }
1100                         }
1101                 }
1102
1103                 return null;
1104         }
1105
1106         /**
1107          * @param tableName
1108          * @param layoutItem
1109          * @return The destination table name for navigation.
1110          */
1111         public String getLayoutItemFieldShouldHaveNavigation(final String tableName, final LayoutItemField layoutItem) {
1112                 if (StringUtils.isEmpty(tableName)) {
1113                         return null;
1114                 }
1115
1116                 if (layoutItem == null) {
1117                         return null;
1118                 }
1119
1120                 // Check whether the field controls a relationship,
1121                 // meaning it identifies a record in another table.
1122                 final Relationship fieldUsedInRelationshipToOne = getFieldUsedInRelationshipToOne(tableName, layoutItem);
1123                 if (fieldUsedInRelationshipToOne != null) {
1124                         return fieldUsedInRelationshipToOne.getToTable();
1125                 }
1126
1127                 // Check whether the field identifies a record in another table
1128                 // just because it is a primary key in that table:
1129                 final Field fieldInfo = layoutItem.getFullFieldDetails();
1130                 final boolean fieldIsRelatedPrimaryKey = layoutItem.getHasRelationshipName() && (fieldInfo != null)
1131                                 && fieldInfo.getPrimaryKey();
1132                 if (fieldIsRelatedPrimaryKey) {
1133                         return layoutItem.getRelationship().getToTable();
1134                 }
1135
1136                 return null;
1137         }
1138 }