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