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