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