Document: Add save().
[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.File;
23 import java.io.IOException;
24 import java.text.NumberFormat;
25 import java.util.ArrayList;
26 import java.util.Hashtable;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Map.Entry;
30
31 import javax.xml.parsers.DocumentBuilder;
32 import javax.xml.parsers.DocumentBuilderFactory;
33 import javax.xml.parsers.ParserConfigurationException;
34 import javax.xml.transform.OutputKeys;
35 import javax.xml.transform.Transformer;
36 import javax.xml.transform.TransformerConfigurationException;
37 import javax.xml.transform.TransformerException;
38 import javax.xml.transform.TransformerFactory;
39 import javax.xml.transform.dom.DOMSource;
40 import javax.xml.transform.stream.StreamResult;
41
42 import org.apache.commons.lang3.StringUtils;
43 import org.glom.web.shared.libglom.CustomTitle;
44 import org.glom.web.shared.libglom.Field;
45 import org.glom.web.shared.libglom.NumericFormat;
46 import org.glom.web.shared.libglom.Relationship;
47 import org.glom.web.shared.libglom.Report;
48 import org.glom.web.shared.libglom.Translatable;
49 import org.glom.web.shared.libglom.layout.Formatting;
50 import org.glom.web.shared.libglom.layout.LayoutGroup;
51 import org.glom.web.shared.libglom.layout.LayoutItem;
52 import org.glom.web.shared.libglom.layout.LayoutItemField;
53 import org.glom.web.shared.libglom.layout.LayoutItemNotebook;
54 import org.glom.web.shared.libglom.layout.LayoutItemPortal;
55 import org.glom.web.shared.libglom.layout.LayoutItemPortal.NavigationType;
56 import org.glom.web.shared.libglom.layout.TableToViewDetails;
57 import org.glom.web.shared.libglom.layout.UsesRelationship;
58 import org.glom.web.shared.libglom.layout.UsesRelationshipImpl;
59 import org.glom.web.shared.libglom.layout.reportparts.LayoutItemGroupBy;
60 import org.jfree.util.Log;
61 import org.w3c.dom.Element;
62 import org.w3c.dom.Node;
63 import org.w3c.dom.NodeList;
64 import org.xml.sax.SAXException;
65
66 import com.google.common.io.Files;
67
68 /**
69  * @author Murray Cumming <murrayc@openismus.com>
70  * 
71  */
72 public class Document {
73
74         @SuppressWarnings("serial")
75         private static class TableInfo extends Translatable {
76                 public boolean isDefault;
77                 public boolean isHidden;
78
79                 private final Hashtable<String, Field> fieldsMap = new Hashtable<String, Field>();
80                 private final Hashtable<String, Relationship> relationshipsMap = new Hashtable<String, Relationship>();
81                 private final Hashtable<String, Report> reportsMap = new Hashtable<String, Report>();
82
83                 private List<LayoutGroup> layoutGroupsList = new ArrayList<LayoutGroup>();
84                 private List<LayoutGroup> layoutGroupsDetails = new ArrayList<LayoutGroup>();
85         }
86
87         private String fileURI = "";
88         private org.w3c.dom.Document xmlDocument = null;
89
90         private final Translatable databaseTitle = new Translatable();
91         private String translationOriginalLocale = "";
92         private final List<String> translationAvailableLocales = new ArrayList<String>();
93         private boolean isExample = false;
94         private HostingMode hostingMode = HostingMode.HOSTING_MODE_POSTGRES_CENTRAL;
95         private String connectionServer = "";
96         private String connectionDatabase = "";
97         private int connectionPort = 0;
98         private final Hashtable<String, TableInfo> tablesMap = new Hashtable<String, TableInfo>();
99
100         private static final String NODE_ROOT = "glom_document";
101         private static final String ATTRIBUTE_IS_EXAMPLE = "is_example";
102         private static final String ATTRIBUTE_TRANSLATION_ORIGINAL_LOCALE = "translation_original_locale";
103         private static final String NODE_CONNECTION = "connection";
104         private static final String ATTRIBUTE_CONNECTION_HOSTING_MODE = "hosting_mode";
105         private static final String ATTRIBUTE_CONNECTION_SERVER = "server";
106         private static final String ATTRIBUTE_CONNECTION_DATABASE = "database";
107         private static final String ATTRIBUTE_CONNECTION_PORT = "port";
108         private static final String NODE_TABLE = "table";
109         private static final String ATTRIBUTE_NAME = "name";
110         private static final String ATTRIBUTE_TITLE = "title";
111         private static final String ATTRIBUTE_DEFAULT = "default";
112         private static final String ATTRIBUTE_HIDDEN = "hidden";
113         private static final String NODE_TRANSLATIONS_SET = "trans_set";
114         private static final String NODE_TRANSLATIONS = "trans";
115         private static final String ATTRIBUTE_TRANSLATION_LOCALE = "loc";
116         private static final String ATTRIBUTE_TRANSLATION_TITLE = "val";
117         private static final String NODE_REPORTS = "reports";
118         private static final String NODE_REPORT = "report";
119         private static final String NODE_FIELDS = "fields";
120         private static final String NODE_FIELD = "field";
121         private static final String ATTRIBUTE_PRIMARY_KEY = "primary_key";
122         private static final String ATTRIBUTE_FIELD_TYPE = "type";
123         private static final String NODE_FORMATTING = "formatting";
124         // private static final String ATTRIBUTE_TEXT_FORMAT_MULTILINE = "format_text_multiline";
125         private static final String ATTRIBUTE_USE_THOUSANDS_SEPARATOR = "format_thousands_separator";
126         private static final String ATTRIBUTE_DECIMAL_PLACES = "format_decimal_places";
127         private static final String NODE_RELATIONSHIPS = "relationships";
128         private static final String NODE_RELATIONSHIP = "relationship";
129         private static final String ATTRIBUTE_RELATIONSHIP_FROM_FIELD = "key";
130         private static final String ATTRIBUTE_RELATIONSHIP_TO_TABLE = "other_table";
131         private static final String ATTRIBUTE_RELATIONSHIP_TO_FIELD = "other_key";
132         private static final String NODE_DATA_LAYOUTS = "data_layouts";
133         private static final String NODE_DATA_LAYOUT = "data_layout";
134         private static final String NODE_DATA_LAYOUT_GROUPS = "data_layout_groups";
135         private static final String NODE_DATA_LAYOUT_GROUP = "data_layout_group";
136         private static final String ATTRIBUTE_LAYOUT_GROUP_COLUMNS_COUNT = "columns_count";
137         private static final String NODE_DATA_LAYOUT_NOTEBOOK = "data_layout_notebook";
138         private static final String NODE_DATA_LAYOUT_PORTAL = "data_layout_portal";
139         private static final String NODE_DATA_LAYOUT_PORTAL_NAVIGATIONRELATIONSHIP = "portal_navigation_relationship";
140         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE = "navigation_type";
141         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE_AUTOMATIC = "automatic";
142         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE_SPECIFIC = "specific";
143         private static final String ATTRIBUTE_PORTAL_NAVIGATION_TYPE_NONE = "none";
144         private static final String ATTRIBUTE_RELATIONSHIP_NAME = "relationship";
145         private static final String ATTRIBUTE_RELATED_RELATIONSHIP_NAME = "related_relationship";
146         private static final String NODE_DATA_LAYOUT_ITEM = "data_layout_item";
147         private static final String NODE_CUSTOM_TITLE = "title_custom";
148         private static final String ATTRIBUTE_CUSTOM_TITLE_USE_CUSTOM = "use_custom";
149         private static final String NODE_DATA_LAYOUT_ITEM_GROUPBY = "data_layout_item_groupby";
150         private static final String NODE_GROUPBY = "groupby";
151         private static final String NODE_SECONDARY_FIELDS = "secondary_fields";
152         private static final String ATTRIBUTE_USE_DEFAULT_FORMATTING = "use_default_formatting";
153         private static final String LAYOUT_NAME_DETAILS = "details";
154         private static final String LAYOUT_NAME_LIST = "list";
155
156         public void setFileURI(final String fileURI) {
157                 this.fileURI = fileURI;
158         }
159
160         public String getFileURI() {
161                 return fileURI;
162         }
163
164         // TODO: Make sure these have the correct values.
165         public enum LoadFailureCodes {
166                 LOAD_FAILURE_CODE_NONE, LOAD_FAILURE_CODE_NOT_FOUND, LOAD_FAILURE_CODE_FILE_VERSION_TOO_NEW
167         };
168
169         public boolean load() {
170                 final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
171                 DocumentBuilder documentBuilder = null;
172                 try {
173                         documentBuilder = dbf.newDocumentBuilder();
174                 } catch (final ParserConfigurationException e) {
175                         // TODO Auto-generated catch block
176                         e.printStackTrace();
177                         return false;
178                 }
179
180                 try {
181                         xmlDocument = documentBuilder.parse(fileURI);
182                 } catch (final SAXException e) {
183                         // TODO Auto-generated catch block
184                         e.printStackTrace();
185                         return false;
186                 } catch (final IOException e) {
187                         // TODO Auto-generated catch block
188                         e.printStackTrace();
189                         return false;
190                 }
191
192                 final Element rootNode = xmlDocument.getDocumentElement();
193                 if (rootNode.getNodeName() != NODE_ROOT) {
194                         Log.error("Unexpected XML root node name found: " + rootNode.getNodeName());
195                         return false;
196                 }
197
198                 databaseTitle.setTitleOriginal(rootNode.getAttribute(ATTRIBUTE_TITLE));
199
200                 translationOriginalLocale = rootNode.getAttribute(ATTRIBUTE_TRANSLATION_ORIGINAL_LOCALE);
201                 translationAvailableLocales.add(translationOriginalLocale); // Just a cache.
202
203                 isExample = getAttributeAsBoolean(rootNode, ATTRIBUTE_IS_EXAMPLE);
204
205                 final Element nodeConnection = getElementByName(rootNode, NODE_CONNECTION);
206                 if (nodeConnection != null) {
207                         final String strHostingMode = nodeConnection.getAttribute(ATTRIBUTE_CONNECTION_HOSTING_MODE);
208                         if (strHostingMode == "postgres_central") {
209                                 hostingMode = HostingMode.HOSTING_MODE_POSTGRES_CENTRAL;
210                         } else if (strHostingMode == "sqlite") {
211                                 hostingMode = HostingMode.HOSTING_MODE_SQLITE;
212                         } else {
213                                 hostingMode = HostingMode.HOSTING_MODE_POSTGRES_SELF;
214                         }
215
216                         connectionServer = nodeConnection.getAttribute(ATTRIBUTE_CONNECTION_SERVER);
217                         connectionDatabase = nodeConnection.getAttribute(ATTRIBUTE_CONNECTION_DATABASE);
218                         connectionPort = getAttributeAsDecimal(nodeConnection, ATTRIBUTE_CONNECTION_PORT);
219                 }
220
221                 // We first load the fields, relationships, etc,
222                 // for all tables:
223                 final List<Node> listTableNodes = getChildrenByTagName(rootNode, NODE_TABLE);
224                 for (final Node node : listTableNodes) {
225                         if (!(node instanceof Element)) {
226                                 continue;
227                         }
228
229                         final Element element = (Element) node;
230                         final TableInfo info = loadTableNodeBasic(element);
231                         tablesMap.put(info.getName(), info);
232                 }
233
234                 // We then load the layouts for all tables, because they
235                 // need the fields and relationships for all tables:
236                 for (final Node node : listTableNodes) {
237                         if (!(node instanceof Element)) {
238                                 continue;
239                         }
240
241                         final Element element = (Element) node;
242                         final String tableName = element.getAttribute(ATTRIBUTE_NAME);
243
244                         // We first load the fields, relationships, etc:
245                         final TableInfo info = getTableInfo(tableName);
246                         if (info == null) {
247                                 continue;
248                         }
249
250                         // We then load the layouts afterwards, because they
251                         // need the fields and relationships:
252                         loadTableLayouts(element, info);
253
254                         tablesMap.put(info.getName(), info);
255                 }
256
257                 return true;
258         };
259
260         private Element getElementByName(final Element parentElement, final String tagName) {
261                 final List<Node> listNodes = getChildrenByTagName(parentElement, tagName);
262                 if (listNodes == null) {
263                         return null;
264                 }
265
266                 if (listNodes.size() == 0) {
267                         return null;
268                 }
269
270                 return (Element) listNodes.get(0);
271         }
272
273         private boolean getAttributeAsBoolean(final Element node, final String attributeName) {
274                 final String str = node.getAttribute(attributeName);
275                 if (str == null) {
276                         return false;
277                 }
278
279                 return str.equals("true");
280         }
281
282         private void setAttributeAsBoolean(final Element node, final String attributeName, boolean value) {
283                 final String str = value ? "true" : "false";
284                 node.setAttribute(attributeName, str);
285         }
286
287         private int getAttributeAsDecimal(final Element node, final String attributeName) {
288                 final String str = node.getAttribute(attributeName);
289                 if (StringUtils.isEmpty(str)) {
290                         return 0;
291                 }
292
293                 return Integer.valueOf(str);
294         }
295
296         private void setAttributeAsDecimal(Element node, String attributeName, int value) {
297                 final NumberFormat format = NumberFormat.getInstance(Locale.US);
298                 format.setGroupingUsed(false); // TODO: Does this change it system-wide?
299                 final String str = format.format(value);
300                 node.setAttribute(attributeName, str);
301         }
302
303         /**
304          * Load a title and its translations.
305          * 
306          * @param node
307          *            The XML Element that may contain a title attribute and a trans_set of translations of the title.
308          * @param title
309          */
310         private void loadTitle(final Element node, final Translatable title) {
311                 title.setName(node.getAttribute(ATTRIBUTE_NAME));
312
313                 title.setTitleOriginal(node.getAttribute(ATTRIBUTE_TITLE));
314
315                 final Element nodeSet = getElementByName(node, NODE_TRANSLATIONS_SET);
316                 if (nodeSet == null) {
317                         return;
318                 }
319
320                 final List<Node> listNodes = getChildrenByTagName(nodeSet, NODE_TRANSLATIONS);
321                 if (listNodes == null) {
322                         return;
323                 }
324
325                 for (final Node transNode : listNodes) {
326                         if (!(transNode instanceof Element)) {
327                                 continue;
328                         }
329
330                         final Element element = (Element) transNode;
331
332                         final String locale = element.getAttribute(ATTRIBUTE_TRANSLATION_LOCALE);
333                         final String translatedTitle = element.getAttribute(ATTRIBUTE_TRANSLATION_TITLE);
334                         if (!StringUtils.isEmpty(locale) && !StringUtils.isEmpty(translatedTitle)) {
335                                 title.setTitle(translatedTitle, locale);
336
337                                 // Remember any new translation locales in our cached list:
338                                 if (!translationAvailableLocales.contains(locale)) {
339                                         translationAvailableLocales.add(locale);
340                                 }
341                         }
342                 }
343         }
344
345         private void saveTitle(org.w3c.dom.Document doc, Element node, final Translatable title) {
346                 node.setAttribute(ATTRIBUTE_NAME, title.getName());
347
348                 node.setAttribute(ATTRIBUTE_TITLE, title.getTitleOriginal());
349
350                 final Element nodeSet = createElement(doc, node, NODE_TRANSLATIONS_SET);
351
352                 for (Entry<String, String> entry : title.getTranslationsMap().entrySet()) {
353                         final Element element = createElement(doc, nodeSet, NODE_TRANSLATIONS);
354
355                         element.setAttribute(ATTRIBUTE_TRANSLATION_LOCALE, entry.getKey());
356                         element.setAttribute(ATTRIBUTE_TRANSLATION_TITLE, entry.getValue());
357                 }
358         }
359
360         /**
361          * @param tableNode
362          * @return
363          */
364         private TableInfo loadTableNodeBasic(final Element tableNode) {
365                 final TableInfo info = new TableInfo();
366                 loadTitle(tableNode, info);
367                 final String tableName = info.getName();
368
369                 info.isDefault = getAttributeAsBoolean(tableNode, ATTRIBUTE_DEFAULT);
370                 info.isHidden = getAttributeAsBoolean(tableNode, ATTRIBUTE_HIDDEN);
371
372                 // These should be loaded before the fields, because the fields use them.
373                 final Element relationshipsNode = getElementByName(tableNode, NODE_RELATIONSHIPS);
374                 if (relationshipsNode != null) {
375                         final List<Node> listNodes = getChildrenByTagName(relationshipsNode, NODE_RELATIONSHIP);
376                         for (final Node node : listNodes) {
377                                 if (!(node instanceof Element)) {
378                                         continue;
379                                 }
380
381                                 final Element element = (Element) node;
382                                 final Relationship relationship = new Relationship();
383                                 loadTitle(element, relationship);
384                                 relationship.setFromTable(tableName);
385                                 relationship.setFromField(element.getAttribute(ATTRIBUTE_RELATIONSHIP_FROM_FIELD));
386                                 relationship.setToTable(element.getAttribute(ATTRIBUTE_RELATIONSHIP_TO_TABLE));
387                                 relationship.setToField(element.getAttribute(ATTRIBUTE_RELATIONSHIP_TO_FIELD));
388
389                                 info.relationshipsMap.put(relationship.getName(), relationship);
390                         }
391                 }
392
393                 final Element fieldsNode = getElementByName(tableNode, NODE_FIELDS);
394                 if (fieldsNode != null) {
395                         final List<Node> listNodes = getChildrenByTagName(fieldsNode, NODE_FIELD);
396                         for (final Node node : listNodes) {
397                                 if (!(node instanceof Element)) {
398                                         continue;
399                                 }
400
401                                 final Element element = (Element) node;
402                                 final Field field = new Field();
403                                 loadField(element, field);
404
405                                 info.fieldsMap.put(field.getName(), field);
406                         }
407                 }
408
409                 return info;
410         }
411
412         private void saveTableNodeBasic(org.w3c.dom.Document doc, final Element tableNode, final TableInfo info) {
413                 saveTitle(doc, tableNode, info);
414
415                 setAttributeAsBoolean(tableNode, ATTRIBUTE_DEFAULT, info.isDefault);
416                 setAttributeAsBoolean(tableNode, ATTRIBUTE_HIDDEN, info.isHidden);
417
418                 final Element relationshipsNode = createElement(doc, tableNode, NODE_RELATIONSHIPS);
419                 for (Relationship relationship : info.relationshipsMap.values()) {
420                         final Element element = createElement(doc, relationshipsNode, NODE_RELATIONSHIP);
421                         saveTitle(doc, element, relationship);
422
423                         element.setAttribute(ATTRIBUTE_RELATIONSHIP_FROM_FIELD, relationship.getFromField());
424                         element.setAttribute(ATTRIBUTE_RELATIONSHIP_TO_TABLE, relationship.getToTable());
425                         element.setAttribute(ATTRIBUTE_RELATIONSHIP_TO_FIELD, relationship.getToField());
426                 }
427
428                 final Element fieldsNode = createElement(doc, tableNode, NODE_FIELDS);
429                 for (Field field : info.fieldsMap.values()) {
430                         final Element element = createElement(doc, fieldsNode, NODE_FIELD);
431                         saveField(doc, element, field);
432                 }
433         }
434
435         /**
436          * @param doc
437          * @param element
438          * @param field
439          */
440         private void saveField(org.w3c.dom.Document doc, Element element, Field field) {
441                 saveTitle(doc, element, field);
442
443                 String fieldTypeStr = "";
444
445                 switch (field.getGlomType()) {
446                 case TYPE_BOOLEAN:
447                         fieldTypeStr = "Boolean";
448                         break;
449                 case TYPE_DATE:
450                         fieldTypeStr = "Date";
451                         break;
452                 case TYPE_IMAGE:
453                         fieldTypeStr = "Image";
454                         break;
455                 case TYPE_NUMERIC:
456                         fieldTypeStr = "Number";
457                         break;
458                 case TYPE_TEXT:
459                         fieldTypeStr = "Text";
460                         break;
461                 case TYPE_TIME:
462                         fieldTypeStr = "Time";
463                         break;
464                 default:
465                         break;
466                 }
467                 element.setAttribute(ATTRIBUTE_FIELD_TYPE, fieldTypeStr);
468
469                 setAttributeAsBoolean(element, ATTRIBUTE_PRIMARY_KEY, field.getPrimaryKey());
470
471                 final Element elementFormatting = createElement(doc, element, NODE_FORMATTING);
472                 saveFormatting(elementFormatting, field.getFormatting());
473         }
474
475         /**
476          * @param elementFormatting
477          * @param formatting
478          */
479         private void saveFormatting(Element element, Formatting formatting) {
480                 // formatting.setTextFormatMultiline(getAttributeAsBoolean(elementFormatting, ATTRIBUTE_TEXT_FORMAT_MULTILINE));
481
482                 final NumericFormat numericFormatting = formatting.getNumericFormat();
483                 if (numericFormatting != null) {
484                         setAttributeAsBoolean(element, ATTRIBUTE_USE_THOUSANDS_SEPARATOR,
485                                         numericFormatting.getUseThousandsSeparator());
486                         setAttributeAsDecimal(element, ATTRIBUTE_DECIMAL_PLACES, numericFormatting.getDecimalPlaces());
487                 }
488         }
489
490         /**
491          * @param tableNode
492          * @param info
493          */
494         private void loadTableLayouts(final Element tableNode, final TableInfo info) {
495                 final String tableName = info.getName();
496
497                 final Element layoutsNode = getElementByName(tableNode, NODE_DATA_LAYOUTS);
498                 if (layoutsNode != null) {
499                         final List<Node> listNodes = getChildrenByTagName(layoutsNode, NODE_DATA_LAYOUT);
500                         for (final Node node : listNodes) {
501                                 if (!(node instanceof Element)) {
502                                         continue;
503                                 }
504
505                                 final Element element = (Element) node;
506                                 final String name = element.getAttribute(ATTRIBUTE_NAME);
507                                 final List<LayoutGroup> listLayoutGroups = loadLayoutNode(element, tableName);
508                                 if (name.equals(LAYOUT_NAME_DETAILS)) {
509                                         info.layoutGroupsDetails = listLayoutGroups;
510                                 } else if (name.equals(LAYOUT_NAME_LIST)) {
511                                         info.layoutGroupsList = listLayoutGroups;
512                                 } else {
513                                         Log.error("loadTableNode(): unexpected layout name: " + name);
514                                 }
515                         }
516                 }
517
518                 final Element reportsNode = getElementByName(tableNode, NODE_REPORTS);
519                 if (reportsNode != null) {
520                         final List<Node> listNodes = getChildrenByTagName(reportsNode, NODE_REPORT);
521                         for (final Node node : listNodes) {
522                                 if (!(node instanceof Element)) {
523                                         continue;
524                                 }
525
526                                 final Element element = (Element) node;
527                                 final Report report = new Report();
528                                 loadReport(element, report, tableName);
529
530                                 info.reportsMap.put(report.getName(), report);
531                         }
532                 }
533         }
534
535         /**
536          * @param node
537          * @return
538          */
539         private List<LayoutGroup> loadLayoutNode(final Element node, final String tableName) {
540                 if (node == null) {
541                         return null;
542                 }
543
544                 final List<LayoutGroup> result = new ArrayList<LayoutGroup>();
545
546                 final List<Node> listNodes = getChildrenByTagName(node, NODE_DATA_LAYOUT_GROUPS);
547                 for (final Node nodeGroups : listNodes) {
548                         if (!(nodeGroups instanceof Element)) {
549                                 continue;
550                         }
551
552                         final Element elementGroups = (Element) nodeGroups;
553
554                         final NodeList list = elementGroups.getChildNodes();
555                         final int num = list.getLength();
556                         for (int i = 0; i < num; i++) {
557                                 final Node nodeLayoutGroup = list.item(i);
558                                 if (nodeLayoutGroup == null) {
559                                         continue;
560                                 }
561
562                                 if (!(nodeLayoutGroup instanceof Element)) {
563                                         continue;
564                                 }
565
566                                 final Element element = (Element) nodeLayoutGroup;
567                                 final String tagName = element.getTagName();
568                                 if (tagName == NODE_DATA_LAYOUT_GROUP) {
569                                         final LayoutGroup group = new LayoutGroup();
570                                         loadDataLayoutGroup(element, group, tableName);
571                                         result.add(group);
572                                 } else if (tagName == NODE_DATA_LAYOUT_NOTEBOOK) {
573                                         final LayoutItemNotebook group = new LayoutItemNotebook();
574                                         loadDataLayoutGroup(element, group, tableName);
575                                         result.add(group);
576                                 } else if (tagName == NODE_DATA_LAYOUT_PORTAL) {
577                                         final LayoutItemPortal portal = new LayoutItemPortal();
578                                         loadDataLayoutPortal(element, portal, tableName);
579                                         result.add(portal);
580                                 }
581                         }
582                 }
583
584                 return result;
585         }
586
587         /**
588          * @param element
589          * @param tableName
590          * @param portal
591          */
592         private void loadUsesRelationship(final Element element, final String tableName, final UsesRelationship item) {
593                 if (element == null) {
594                         return;
595                 }
596
597                 if (item == null) {
598                         return;
599                 }
600
601                 final String relationshipName = element.getAttribute(ATTRIBUTE_RELATIONSHIP_NAME);
602                 Relationship relationship = null;
603                 if (!StringUtils.isEmpty(relationshipName)) {
604                         // std::cout << "  debug in : tableName=" << tableName << ", relationshipName=" << relationship_name <<
605                         // std::endl;
606                         relationship = getRelationship(tableName, relationshipName);
607                         item.setRelationship(relationship);
608
609                         if (relationship == null) {
610                                 Log.error("relationship not found: " + relationshipName + ", in table: " + tableName);
611                         }
612                 }
613
614                 // TODO: Unit test loading of doubly-related fields.
615                 final String relatedRelationshipName = element.getAttribute(ATTRIBUTE_RELATED_RELATIONSHIP_NAME);
616                 if (!StringUtils.isEmpty(relatedRelationshipName) && (relationship != null)) {
617                         final Relationship relatedRelationship = getRelationship(relationship.getToTable(), relatedRelationshipName);
618                         item.setRelatedRelationship(relatedRelationship);
619
620                         if (relatedRelationship == null) {
621                                 Log.error("related relationship not found in table=" + relationship.getToTable() + ",  name="
622                                                 + relatedRelationshipName);
623                         }
624                 }
625         }
626
627         /**
628          * getElementsByTagName() is recursive, but we do not want that.
629          * 
630          * @param node
631          * @param
632          * @return
633          */
634         private List<Node> getChildrenByTagName(final Element parentNode, final String tagName) {
635                 final List<Node> result = new ArrayList<Node>();
636
637                 final NodeList list = parentNode.getElementsByTagName(tagName);
638                 final int num = list.getLength();
639                 for (int i = 0; i < num; i++) {
640                         final Node node = list.item(i);
641                         if (node == null) {
642                                 continue;
643                         }
644
645                         final Node itemParentNode = node.getParentNode();
646                         if (itemParentNode.equals(parentNode)) {
647                                 result.add(node);
648                         }
649                 }
650
651                 return result;
652         }
653
654         /**
655          * @param element
656          * @param group
657          */
658         private void loadDataLayoutGroup(final Element nodeGroup, final LayoutGroup group, final String tableName) {
659                 loadTitle(nodeGroup, group);
660
661                 // Read the column count:
662                 int columnCount = getAttributeAsDecimal(nodeGroup, ATTRIBUTE_LAYOUT_GROUP_COLUMNS_COUNT);
663                 if (columnCount < 1) {
664                         columnCount = 1; // 0 is a useless default.
665                 }
666                 group.setColumnCount(columnCount);
667
668                 // Get the child items:
669                 final NodeList listNodes = nodeGroup.getChildNodes();
670                 final int num = listNodes.getLength();
671                 for (int i = 0; i < num; i++) {
672                         final Node node = listNodes.item(i);
673                         if (!(node instanceof Element)) {
674                                 continue;
675                         }
676
677                         final Element element = (Element) node;
678                         final String tagName = element.getTagName();
679                         if (tagName == NODE_DATA_LAYOUT_GROUP) {
680                                 final LayoutGroup childGroup = new LayoutGroup();
681                                 loadDataLayoutGroup(element, childGroup, tableName);
682                                 group.addItem(childGroup);
683                         } else if (tagName == NODE_DATA_LAYOUT_NOTEBOOK) {
684                                 final LayoutItemNotebook childGroup = new LayoutItemNotebook();
685                                 loadDataLayoutGroup(element, childGroup, tableName);
686                                 group.addItem(childGroup);
687                         } else if (tagName == NODE_DATA_LAYOUT_PORTAL) {
688                                 final LayoutItemPortal childGroup = new LayoutItemPortal();
689                                 loadDataLayoutPortal(element, childGroup, tableName);
690                                 group.addItem(childGroup);
691                         } else if (element.getTagName() == NODE_DATA_LAYOUT_ITEM) {
692                                 final LayoutItemField item = new LayoutItemField();
693                                 loadDataLayoutItemField(element, item, tableName);
694                                 group.addItem(item);
695                         } else if (element.getTagName() == NODE_DATA_LAYOUT_ITEM_GROUPBY) {
696                                 final LayoutItemGroupBy item = new LayoutItemGroupBy();
697                                 loadDataLayoutItemGroupBy(element, item, tableName);
698                                 group.addItem(item);
699                         }
700                 }
701         }
702
703         /**
704          * @param element
705          * @param item
706          * @param tableName
707          */
708         private void loadDataLayoutItemGroupBy(final Element element, final LayoutItemGroupBy item, final String tableName) {
709                 loadDataLayoutGroup(element, item, tableName);
710
711                 final Element elementGroupBy = getElementByName(element, NODE_GROUPBY);
712                 if (elementGroupBy == null) {
713                         return;
714                 }
715
716                 final LayoutItemField fieldGroupBy = new LayoutItemField();
717                 loadDataLayoutItemField(elementGroupBy, fieldGroupBy, tableName);
718                 item.setFieldGroupBy(fieldGroupBy);
719
720                 final Element elementSecondaryFields = getElementByName(element, NODE_SECONDARY_FIELDS);
721                 if (elementSecondaryFields == null) {
722                         return;
723                 }
724
725                 final Element elementLayoutGroup = getElementByName(elementSecondaryFields, NODE_DATA_LAYOUT_GROUP);
726                 if (elementLayoutGroup != null) {
727                         final LayoutGroup secondaryLayoutGroup = new LayoutGroup();
728                         loadDataLayoutGroup(elementLayoutGroup, secondaryLayoutGroup, tableName);
729                         item.setSecondaryFields(secondaryLayoutGroup);
730                 }
731         }
732
733         /**
734          * @param element
735          * @param item
736          */
737         private void loadDataLayoutItemField(final Element element, final LayoutItemField item, final String tableName) {
738                 item.setName(element.getAttribute(ATTRIBUTE_NAME));
739                 loadUsesRelationship(element, tableName, item);
740
741                 final Element elementCustomTitle = getElementByName(element, NODE_CUSTOM_TITLE);
742                 if (elementCustomTitle != null) {
743                         final CustomTitle customTitle = item.getCustomTitle();
744                         customTitle.setUseCustomTitle(getAttributeAsBoolean(elementCustomTitle, ATTRIBUTE_CUSTOM_TITLE_USE_CUSTOM));
745                         loadTitle(elementCustomTitle, customTitle); // LayoutItemField doesn't use its own title member.
746                 }
747
748                 // Get the actual field:
749                 final String fieldName = item.getName();
750                 final String inTableName = item.getTableUsed(tableName);
751                 final Field field = getField(inTableName, fieldName);
752                 item.setFullFieldDetails(field);
753
754                 item.setUseDefaultFormatting(getAttributeAsBoolean(element, ATTRIBUTE_USE_DEFAULT_FORMATTING));
755
756                 final Element elementFormatting = getElementByName(element, NODE_FORMATTING);
757                 if (elementFormatting != null) {
758                         loadFormatting(elementFormatting, item.getFormatting());
759                 }
760         }
761
762         /**
763          * @param element
764          * @param childGroup
765          */
766         private void loadDataLayoutPortal(final Element element, final LayoutItemPortal portal, final String tableName) {
767                 loadUsesRelationship(element, tableName, portal);
768                 final String relatedTableName = portal.getTableUsed(tableName);
769                 loadDataLayoutGroup(element, portal, relatedTableName);
770
771                 final Element elementNavigation = getElementByName(element, NODE_DATA_LAYOUT_PORTAL_NAVIGATIONRELATIONSHIP);
772                 if (elementNavigation != null) {
773                         final String navigationTypeAsString = elementNavigation.getAttribute(ATTRIBUTE_PORTAL_NAVIGATION_TYPE);
774                         if (StringUtils.isEmpty(navigationTypeAsString)
775                                         || navigationTypeAsString == ATTRIBUTE_PORTAL_NAVIGATION_TYPE_AUTOMATIC) {
776                                 portal.setNavigationType(LayoutItemPortal.NavigationType.NAVIGATION_AUTOMATIC);
777                         } else if (navigationTypeAsString == ATTRIBUTE_PORTAL_NAVIGATION_TYPE_NONE) {
778                                 portal.setNavigationType(LayoutItemPortal.NavigationType.NAVIGATION_NONE);
779                         } else if (navigationTypeAsString == ATTRIBUTE_PORTAL_NAVIGATION_TYPE_SPECIFIC) {
780                                 // Read the specified relationship name:
781                                 final UsesRelationship relationshipNavigationSpecific = new UsesRelationshipImpl();
782                                 loadUsesRelationship(elementNavigation, relatedTableName, relationshipNavigationSpecific);
783                                 portal.setNavigationRelationshipSpecific(relationshipNavigationSpecific);
784                         }
785                 }
786
787         }
788
789         /**
790          * @param element
791          * @param field
792          */
793         private void loadField(final Element element, final Field field) {
794                 loadTitle(element, field);
795
796                 Field.GlomFieldType fieldType = Field.GlomFieldType.TYPE_INVALID;
797                 final String fieldTypeStr = element.getAttribute(ATTRIBUTE_FIELD_TYPE);
798                 if (!StringUtils.isEmpty(fieldTypeStr)) {
799                         if (fieldTypeStr.equals("Boolean")) {
800                                 fieldType = Field.GlomFieldType.TYPE_BOOLEAN;
801                         } else if (fieldTypeStr.equals("Date")) {
802                                 fieldType = Field.GlomFieldType.TYPE_DATE;
803                         } else if (fieldTypeStr.equals("Image")) {
804                                 fieldType = Field.GlomFieldType.TYPE_IMAGE;
805                         } else if (fieldTypeStr.equals("Number")) {
806                                 fieldType = Field.GlomFieldType.TYPE_NUMERIC;
807                         } else if (fieldTypeStr.equals("Text")) {
808                                 fieldType = Field.GlomFieldType.TYPE_TEXT;
809                         } else if (fieldTypeStr.equals("Time")) {
810                                 fieldType = Field.GlomFieldType.TYPE_TIME;
811                         }
812                 }
813
814                 field.setGlomFieldType(fieldType);
815
816                 field.setPrimaryKey(getAttributeAsBoolean(element, ATTRIBUTE_PRIMARY_KEY));
817
818                 final Element elementFormatting = getElementByName(element, NODE_FORMATTING);
819                 if (elementFormatting != null) {
820                         loadFormatting(elementFormatting, field.getFormatting());
821                 }
822         }
823
824         /**
825          * @param elementFormatting
826          * @param formatting
827          */
828         private void loadFormatting(final Element elementFormatting, final Formatting formatting) {
829                 if (elementFormatting == null) {
830                         return;
831                 }
832
833                 if (formatting == null) {
834                         return;
835                 }
836
837                 // formatting.setTextFormatMultiline(getAttributeAsBoolean(elementFormatting, ATTRIBUTE_TEXT_FORMAT_MULTILINE));
838
839                 final NumericFormat numericFormatting = formatting.getNumericFormat();
840                 if (numericFormatting != null) {
841                         numericFormatting.setUseThousandsSeparator(getAttributeAsBoolean(elementFormatting,
842                                         ATTRIBUTE_USE_THOUSANDS_SEPARATOR));
843                         numericFormatting.setDecimalPlaces(getAttributeAsDecimal(elementFormatting, ATTRIBUTE_DECIMAL_PLACES));
844                 }
845
846         }
847
848         /**
849          * @param element
850          * @param reportNode
851          */
852         private void loadReport(final Element element, final Report report, final String tableName) {
853                 report.setName(element.getAttribute(ATTRIBUTE_NAME));
854                 loadTitle(element, report);
855
856                 final List<LayoutGroup> listLayoutGroups = loadLayoutNode(element, tableName);
857
858                 // A report can actually only have one LayoutGroup,
859                 // though it uses the same XML structure as List and Details layouts,
860                 // which (wrongly) suggests that it can have more than one group.
861                 LayoutGroup layoutGroup = null;
862                 if (!listLayoutGroups.isEmpty()) {
863                         layoutGroup = listLayoutGroups.get(0);
864                 }
865
866                 report.setLayoutGroup(layoutGroup);
867         }
868
869         private TableInfo getTableInfo(final String tableName) {
870                 return tablesMap.get(tableName);
871         }
872
873         public enum HostingMode {
874                 HOSTING_MODE_POSTGRES_CENTRAL, HOSTING_MODE_POSTGRES_SELF, HOSTING_MODE_SQLITE
875         };
876
877         public String getDatabaseTitle(final String locale) {
878                 return databaseTitle.getTitle(locale);
879         }
880
881         public String getDatabaseTitleOriginal() {
882                 return databaseTitle.getTitleOriginal();
883         }
884
885         public List<String> getTranslationAvailableLocales() {
886                 return translationAvailableLocales;
887         }
888
889         public Document.HostingMode getHostingMode() {
890                 return hostingMode;
891         }
892
893         /**
894          * @param hostingMode
895          */
896         public void setHostingMode(HostingMode hostingMode) {
897                 this.hostingMode = hostingMode;
898         }
899
900         public String getConnectionServer() {
901                 return connectionServer;
902         }
903
904         public long getConnectionPort() {
905                 return connectionPort;
906         }
907
908         public String getConnectionDatabase() {
909                 return connectionDatabase;
910         }
911
912         public List<String> getTableNames() {
913                 // TODO: Return a Set?
914                 return new ArrayList<String>(tablesMap.keySet());
915         }
916
917         public boolean getTableIsHidden(final String tableName) {
918                 final TableInfo info = getTableInfo(tableName);
919                 if (info == null) {
920                         return false;
921                 }
922
923                 return info.isHidden;
924         }
925
926         public String getTableTitle(final String tableName, final String locale) {
927                 final TableInfo info = getTableInfo(tableName);
928                 if (info == null) {
929                         return "";
930                 }
931
932                 return info.getTitle(locale);
933         }
934
935         public String getDefaultTable() {
936                 for (final TableInfo info : tablesMap.values()) {
937                         if (info.isDefault) {
938                                 return info.getName();
939                         }
940                 }
941
942                 return "";
943         }
944
945         public boolean getTableIsKnown(final String tableName) {
946                 final TableInfo info = getTableInfo(tableName);
947                 if (info == null) {
948                         return false;
949                 }
950
951                 return true;
952         }
953
954         public List<Field> getTableFields(final String tableName) {
955                 final TableInfo info = getTableInfo(tableName);
956                 if (info == null) {
957                         return null;
958                 }
959
960                 return new ArrayList<Field>(info.fieldsMap.values());
961         }
962
963         public Field getField(final String tableName, final String strFieldName) {
964                 final TableInfo info = getTableInfo(tableName);
965                 if (info == null) {
966                         return null;
967                 }
968
969                 return info.fieldsMap.get(strFieldName);
970         }
971
972         public List<LayoutGroup> getDataLayoutGroups(final String layoutName, final String parentTableName) {
973                 final TableInfo info = getTableInfo(parentTableName);
974                 if (info == null) {
975                         return new ArrayList<LayoutGroup>();
976                 }
977
978                 if (layoutName == LAYOUT_NAME_DETAILS) {
979                         return info.layoutGroupsDetails;
980                 } else if (layoutName == LAYOUT_NAME_LIST) {
981                         return info.layoutGroupsList;
982                 } else {
983                         return new ArrayList<LayoutGroup>();
984                 }
985         }
986
987         public List<String> getReportNames(final String tableName) {
988                 final TableInfo info = getTableInfo(tableName);
989                 if (info == null) {
990                         return new ArrayList<String>();
991                 }
992
993                 return new ArrayList<String>(info.reportsMap.keySet());
994         }
995
996         public Report getReport(final String tableName, final String reportName) {
997                 final TableInfo info = getTableInfo(tableName);
998                 if (info == null) {
999                         return null;
1000                 }
1001
1002                 return info.reportsMap.get(reportName);
1003         }
1004
1005         /**
1006          * @param tableName
1007          * @param field
1008          * @return
1009          */
1010         public Relationship getFieldUsedInRelationshipToOne(final String tableName, final LayoutItemField layoutField) {
1011
1012                 if (layoutField == null) {
1013                         Log.error("layoutField was null");
1014                         return null;
1015                 }
1016
1017                 Relationship result = null;
1018
1019                 final String tableUsed = layoutField.getTableUsed(tableName);
1020                 final TableInfo info = getTableInfo(tableUsed);
1021                 if (info == null) {
1022                         // This table is special. We would not create a relationship to it using a field:
1023                         // if(tableUsed == GLOM_STANDARD_TABLE_PREFS_TABLE_NAME)
1024                         // return result;
1025
1026                         Log.error("table not found: " + tableUsed);
1027                         return null;
1028                 }
1029
1030                 // Look at each relationship:
1031                 final String fieldName = layoutField.getName();
1032                 for (final Relationship relationship : info.relationshipsMap.values()) {
1033                         if (relationship != null) {
1034                                 // If the relationship uses the field
1035                                 if (StringUtils.equals(relationship.getFromField(), fieldName)) {
1036                                         // if the to_table is not hidden:
1037                                         if (!getTableIsHidden(relationship.getToTable())) {
1038                                                 // TODO_Performance: The use of this convenience method means we get the full relationship
1039                                                 // information again:
1040                                                 if (getRelationshipIsToOne(tableName, relationship.getName())) {
1041                                                         result = relationship;
1042                                                 }
1043                                         }
1044                                 }
1045                         }
1046                 }
1047
1048                 return result;
1049         }
1050
1051         /**
1052          * @param tableName
1053          * @param relationshipName
1054          * @return
1055          */
1056         private boolean getRelationshipIsToOne(final String tableName, final String relationshipName) {
1057                 final Relationship relationship = getRelationship(tableName, relationshipName);
1058                 if (relationship != null) {
1059                         final Field fieldTo = getField(relationship.getToTable(), relationship.getToField());
1060                         if (fieldTo != null) {
1061                                 return (fieldTo.getPrimaryKey() || fieldTo.getUniqueKey());
1062                         }
1063                 }
1064
1065                 return false;
1066         }
1067
1068         /**
1069          * @param tableName
1070          * @param relationshipName
1071          * @return
1072          */
1073         private Relationship getRelationship(final String tableName, final String relationshipName) {
1074                 final TableInfo info = getTableInfo(tableName);
1075                 if (info == null) {
1076                         Log.error("table not found: " + tableName);
1077                         return null;
1078                 }
1079
1080                 return info.relationshipsMap.get(relationshipName);
1081         }
1082
1083         /**
1084          * @param tableName
1085          *            Output parameter
1086          * @param relationship
1087          * @param portal
1088          *            TODO
1089          */
1090         public TableToViewDetails getPortalSuitableTableToViewDetails(final LayoutItemPortal portal) {
1091                 UsesRelationship navigationRelationship = null;
1092
1093                 // Check whether a relationship was specified:
1094                 if (portal.getNavigationType() == NavigationType.NAVIGATION_AUTOMATIC) {
1095                         navigationRelationship = getPortalNavigationRelationshipAutomatic(portal);
1096                 } else {
1097                         navigationRelationship = portal.getNavigationRelationshipSpecific();
1098                 }
1099
1100                 // Get the navigation table name from the chosen relationship:
1101                 final String directlyRelatedTableName = portal.getTableUsed("" /* not relevant */);
1102
1103                 // The navigation_table_name (and therefore, the table_name output parameter,
1104                 // as well) stays empty if the navrel type was set to none.
1105                 String navigationTableName = null;
1106                 if (navigationRelationship != null) {
1107                         navigationTableName = navigationRelationship.getTableUsed(directlyRelatedTableName);
1108                 } else if (portal.getNavigationType() != NavigationType.NAVIGATION_NONE) {
1109                         // An empty result from get_portal_navigation_relationship_automatic() or
1110                         // get_navigation_relationship_specific() means we should use the directly related table:
1111                         navigationTableName = directlyRelatedTableName;
1112                 }
1113
1114                 if (StringUtils.isEmpty(navigationTableName)) {
1115                         return null;
1116                 }
1117
1118                 if (this == null) {
1119                         Log.error("document is null.");
1120                         return null;
1121                 }
1122
1123                 if (getTableIsHidden(navigationTableName)) {
1124                         Log.error("navigation_table_name indicates a hidden table: " + navigationTableName);
1125                         return null;
1126                 }
1127
1128                 final TableToViewDetails result = new TableToViewDetails();
1129                 result.tableName = navigationTableName;
1130                 result.usesRelationship = navigationRelationship;
1131                 return result;
1132         }
1133
1134         /**
1135          * @param portal
1136          *            TODO
1137          * @return
1138          */
1139         private UsesRelationship getPortalNavigationRelationshipAutomatic(final LayoutItemPortal portal) {
1140                 if (this == null) {
1141                         return null;
1142                 }
1143
1144                 // If the related table is not hidden then we can just navigate to that:
1145                 final String direct_related_table_name = portal.getTableUsed("" /* parent table - not relevant */);
1146                 if (!getTableIsHidden(direct_related_table_name)) {
1147                         // Non-hidden tables can just be shown directly. Navigate to it:
1148                         return null;
1149                 } else {
1150                         // If the related table is hidden,
1151                         // then find a suitable related non-hidden table by finding the first layout field that mentions one:
1152                         final LayoutItemField field = getPortalFieldIsFromNonHiddenRelatedRecord(portal);
1153                         if (field != null) {
1154                                 return field; // Returns the UsesRelationship base part. (A relationship belonging to the portal's
1155                                                                 // related table.)
1156                         } else {
1157                                 // Instead, find a key field that's used in a relationship,
1158                                 // and pretend that we are showing the to field as a related field:
1159                                 final Relationship fieldIndentifies = getPortalFieldIdentifiesNonHiddenRelatedRecord(portal);
1160                                 if (fieldIndentifies != null) {
1161                                         final UsesRelationship result = new UsesRelationshipImpl();
1162                                         result.setRelationship(fieldIndentifies);
1163                                         return result;
1164                                 }
1165                         }
1166                 }
1167
1168                 // There was no suitable related table to show:
1169                 return null;
1170         }
1171
1172         /**
1173          * @param portal
1174          *            TODO
1175          * @return
1176          */
1177         private LayoutItemField getPortalFieldIsFromNonHiddenRelatedRecord(final LayoutItemPortal portal) {
1178                 // Find the first field that is from a non-hidden related table.
1179
1180                 if (this == null) {
1181                         return null;
1182                 }
1183
1184                 final LayoutItemField result = null;
1185
1186                 final String parent_table_name = portal.getTableUsed("" /* parent table - not relevant */);
1187
1188                 final List<LayoutItem> items = portal.getItems();
1189                 for (final LayoutItem item : items) {
1190                         if (item instanceof LayoutItemField) {
1191                                 final LayoutItemField field = (LayoutItemField) item;
1192                                 if (field.getHasRelationshipName()) {
1193                                         final String table_name = field.getTableUsed(parent_table_name);
1194                                         if (!(getTableIsHidden(table_name))) {
1195                                                 return field;
1196                                         }
1197                                 }
1198                         }
1199                 }
1200
1201                 return result;
1202         }
1203
1204         /**
1205          * @param used_in_relationship
1206          * @param portal
1207          *            TODO
1208          * @return
1209          */
1210         private Relationship getPortalFieldIdentifiesNonHiddenRelatedRecord(final LayoutItemPortal portal) {
1211                 // Find the first field that is from a non-hidden related table.
1212
1213                 if (this == null) {
1214                         Log.error("document is null");
1215                         return null;
1216                 }
1217
1218                 final String parent_table_name = portal.getTableUsed("" /* parent table - not relevant */);
1219
1220                 final List<LayoutItem> items = portal.getItems();
1221                 for (final LayoutItem item : items) {
1222                         if (item instanceof LayoutItemField) {
1223                                 final LayoutItemField field = (LayoutItemField) item;
1224                                 if (field.getHasRelationshipName()) {
1225                                         final Relationship relationship = getFieldUsedInRelationshipToOne(parent_table_name, field);
1226                                         if (relationship != null) {
1227                                                 final String table_name = relationship.getToTable();
1228                                                 if (!StringUtils.isEmpty(table_name)) {
1229                                                         if (!(getTableIsHidden(table_name))) {
1230                                                                 return relationship;
1231                                                         }
1232                                                 }
1233                                         }
1234                                 }
1235                         }
1236                 }
1237
1238                 return null;
1239         }
1240
1241         /**
1242          * @param tableName
1243          * @param layoutItem
1244          * @return The destination table name for navigation.
1245          */
1246         public String getLayoutItemFieldShouldHaveNavigation(final String tableName, final LayoutItemField layoutItem) {
1247                 if (StringUtils.isEmpty(tableName)) {
1248                         return null;
1249                 }
1250
1251                 if (layoutItem == null) {
1252                         return null;
1253                 }
1254
1255                 // Check whether the field controls a relationship,
1256                 // meaning it identifies a record in another table.
1257                 final Relationship fieldUsedInRelationshipToOne = getFieldUsedInRelationshipToOne(tableName, layoutItem);
1258                 if (fieldUsedInRelationshipToOne != null) {
1259                         return fieldUsedInRelationshipToOne.getToTable();
1260                 }
1261
1262                 // Check whether the field identifies a record in another table
1263                 // just because it is a primary key in that table:
1264                 final Field fieldInfo = layoutItem.getFullFieldDetails();
1265                 final boolean fieldIsRelatedPrimaryKey = layoutItem.getHasRelationshipName() && (fieldInfo != null)
1266                                 && fieldInfo.getPrimaryKey();
1267                 if (fieldIsRelatedPrimaryKey) {
1268                         return layoutItem.getRelationship().getToTable();
1269                 }
1270
1271                 return null;
1272         }
1273
1274         /**
1275          * @param isExample
1276          */
1277         public void setIsExampleFile(boolean isExample) {
1278                 this.isExample = isExample;
1279         }
1280
1281         /**
1282          */
1283         public boolean getIsExampleFile() {
1284                 return isExample;
1285         }
1286
1287         /**
1288          * @return
1289          */
1290         public boolean save() {
1291                 final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
1292                 DocumentBuilder documentBuilder = null;
1293                 try {
1294                         documentBuilder = dbf.newDocumentBuilder();
1295                 } catch (final ParserConfigurationException e) {
1296                         // TODO Auto-generated catch block
1297                         e.printStackTrace();
1298                         return false;
1299                 }
1300
1301                 final org.w3c.dom.Document doc = documentBuilder.newDocument();
1302                 final Element rootNode = doc.createElement(NODE_ROOT);
1303                 doc.appendChild(rootNode);
1304
1305                 rootNode.setAttribute(ATTRIBUTE_TITLE, databaseTitle.getTitleOriginal());
1306                 rootNode.setAttribute(ATTRIBUTE_TRANSLATION_ORIGINAL_LOCALE, translationOriginalLocale);
1307                 setAttributeAsBoolean(rootNode, ATTRIBUTE_IS_EXAMPLE, isExample);
1308
1309                 String strHostingMode = "";
1310                 if (hostingMode == HostingMode.HOSTING_MODE_POSTGRES_CENTRAL) {
1311                         strHostingMode = "postgres_central";
1312                 } else if (hostingMode == HostingMode.HOSTING_MODE_SQLITE) {
1313                         strHostingMode = "sqlite";
1314                 } else {
1315                         strHostingMode = "postgres_self";
1316                 }
1317                 final Element nodeConnection = createElement(doc, rootNode, NODE_CONNECTION);
1318                 nodeConnection.setAttribute(ATTRIBUTE_CONNECTION_HOSTING_MODE, strHostingMode);
1319                 nodeConnection.setAttribute(ATTRIBUTE_CONNECTION_SERVER, connectionServer);
1320                 nodeConnection.setAttribute(ATTRIBUTE_CONNECTION_DATABASE, connectionDatabase);
1321                 setAttributeAsDecimal(nodeConnection, ATTRIBUTE_CONNECTION_PORT, connectionPort);
1322
1323                 // for all tables:
1324                 for (TableInfo table : tablesMap.values()) {
1325                         final Element nodeTable = createElement(doc, rootNode, NODE_TABLE);
1326                         saveTableNodeBasic(doc, nodeTable, table);
1327                         saveTableLayouts(doc, nodeTable, table);
1328                 }
1329
1330                 TransformerFactory transformerFactory = TransformerFactory.newInstance();
1331                 Transformer transformer;
1332                 try {
1333                         transformer = transformerFactory.newTransformer();
1334                 } catch (TransformerConfigurationException e) {
1335                         // TODO Auto-generated catch block
1336                         e.printStackTrace();
1337                         return false;
1338                 }
1339
1340                 // TODO: This probably distorts text nodes,
1341                 // so careful when we load/save them. For instance, scripts.
1342                 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1343                 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
1344
1345                 // Make sure that the parent directory exists:
1346                 final File file = new File(fileURI);
1347                 try {
1348                         Files.createParentDirs(file);
1349                 } catch (IOException e) {
1350                         // TODO Auto-generated catch block
1351                         e.printStackTrace();
1352                         return false;
1353                 }
1354
1355                 DOMSource source = new DOMSource(doc);
1356                 StreamResult result = new StreamResult(file);
1357
1358                 // Output to console for testing
1359                 // StreamResult result = new StreamResult(System.out);
1360
1361                 try {
1362                         transformer.transform(source, result);
1363                 } catch (TransformerException e) {
1364                         // TODO Auto-generated catch block
1365                         e.printStackTrace();
1366                         return false;
1367                 }
1368
1369                 return true;
1370         }
1371
1372         /**
1373          * @param doc
1374          * @param nodeTable
1375          * @param table
1376          */
1377         private void saveTableLayouts(org.w3c.dom.Document doc, Element tableNode, TableInfo table) {
1378
1379                 final Element layoutsNode = createElement(doc, tableNode, NODE_DATA_LAYOUTS);
1380
1381                 final Element nodeLayoutDetails = createElement(doc, layoutsNode, NODE_DATA_LAYOUT);
1382                 nodeLayoutDetails.setAttribute(ATTRIBUTE_NAME, LAYOUT_NAME_DETAILS);
1383                 saveLayoutNode(doc, nodeLayoutDetails, table.layoutGroupsDetails);
1384
1385                 final Element nodeLayoutList = createElement(doc, layoutsNode, NODE_DATA_LAYOUT);
1386                 nodeLayoutList.setAttribute(ATTRIBUTE_NAME, LAYOUT_NAME_LIST);
1387                 saveLayoutNode(doc, nodeLayoutList, table.layoutGroupsList);
1388
1389                 final Element reportsNode = createElement(doc, tableNode, NODE_REPORTS);
1390                 for (Report report : table.reportsMap.values()) {
1391                         final Element element = createElement(doc, reportsNode, NODE_REPORT);
1392                         saveReport(doc, element, report);
1393                 }
1394
1395         }
1396
1397         /**
1398          * @param doc
1399          * @param element
1400          * @param report
1401          */
1402         private void saveReport(final org.w3c.dom.Document doc, final Element element, final Report report) {
1403                 // TODO Auto-generated method stub
1404
1405         }
1406
1407         private void saveLayoutNode(final org.w3c.dom.Document doc, Element element, final List<LayoutGroup> layoutGroups) {
1408                 final Element elementGroups = createElement(doc, element, NODE_DATA_LAYOUT_GROUPS);
1409
1410                 for (final LayoutGroup layoutGroup : layoutGroups) {
1411                         if (layoutGroup instanceof LayoutItemNotebook) {
1412                                 final Element elementGroup = createElement(doc, elementGroups, NODE_DATA_LAYOUT_NOTEBOOK);
1413                                 saveDataLayoutGroup(doc, elementGroup, layoutGroup);
1414                         } else if (layoutGroup instanceof LayoutItemPortal) {
1415                                 final Element elementGroup = createElement(doc, elementGroups, NODE_DATA_LAYOUT_PORTAL);
1416                                 saveDataLayoutPortal(doc, elementGroup, (LayoutItemPortal) layoutGroup);
1417                         } else {
1418                                 final Element elementGroup = createElement(doc, elementGroups, NODE_DATA_LAYOUT_GROUP);
1419                                 saveDataLayoutGroup(doc, elementGroup, layoutGroup);
1420                         }
1421                 }
1422
1423         }
1424
1425         /**
1426          * @param doc
1427          * @param elementGroup
1428          * @param layoutGroup
1429          */
1430         private void saveDataLayoutPortal(final org.w3c.dom.Document doc, final Element element,
1431                         final LayoutItemPortal portal) {
1432                 saveUsesRelationship(element, portal);
1433                 saveDataLayoutGroup(doc, element, portal);
1434
1435                 final Element elementNavigation = createElement(doc, element, NODE_DATA_LAYOUT_PORTAL_NAVIGATIONRELATIONSHIP);
1436                 String navigationTypeAsString = "";
1437                 switch (portal.getNavigationType()) {
1438                 case NAVIGATION_AUTOMATIC:
1439                         navigationTypeAsString = ATTRIBUTE_PORTAL_NAVIGATION_TYPE_AUTOMATIC;
1440                         break;
1441                 case NAVIGATION_NONE:
1442                         navigationTypeAsString = ATTRIBUTE_PORTAL_NAVIGATION_TYPE_NONE;
1443                         break;
1444                 case NAVIGATION_SPECIFIC:
1445                         navigationTypeAsString = ATTRIBUTE_PORTAL_NAVIGATION_TYPE_SPECIFIC;
1446                         break;
1447                 default:
1448                         break;
1449                 }
1450                 elementNavigation.setAttribute(ATTRIBUTE_PORTAL_NAVIGATION_TYPE, navigationTypeAsString);
1451
1452                 if (navigationTypeAsString == ATTRIBUTE_PORTAL_NAVIGATION_TYPE_SPECIFIC) {
1453                         // Write the specified relationship name:
1454                         saveUsesRelationship(elementNavigation, portal.getNavigationRelationshipSpecific());
1455                 }
1456         }
1457
1458         /**
1459          * @param doc
1460          * @param elementGroup
1461          * @param layoutGroup
1462          */
1463         private void saveDataLayoutGroup(final org.w3c.dom.Document doc, final Element nodeGroup, final LayoutGroup group) {
1464                 saveTitle(doc, nodeGroup, group);
1465
1466                 // Write the column count:
1467                 setAttributeAsDecimal(nodeGroup, ATTRIBUTE_LAYOUT_GROUP_COLUMNS_COUNT, group.getColumnCount());
1468
1469                 // Write the child items:
1470                 for (LayoutItem layoutItem : group.getItems()) {
1471                         if (layoutItem instanceof LayoutItemPortal) {
1472                                 final Element element = createElement(doc, nodeGroup, NODE_DATA_LAYOUT_PORTAL);
1473                                 saveDataLayoutPortal(doc, element, (LayoutItemPortal) layoutItem);
1474                         } else if (layoutItem instanceof LayoutItemNotebook) {
1475                                 final Element element = createElement(doc, nodeGroup, NODE_DATA_LAYOUT_NOTEBOOK);
1476                                 saveDataLayoutGroup(doc, element, (LayoutItemNotebook) layoutItem);
1477                         } else if (layoutItem instanceof LayoutGroup) {
1478                                 final Element element = createElement(doc, nodeGroup, NODE_DATA_LAYOUT_GROUP);
1479                                 saveDataLayoutGroup(doc, element, (LayoutGroup) layoutItem);
1480                         } else if (layoutItem instanceof LayoutItemField) {
1481                                 final Element element = createElement(doc, nodeGroup, NODE_DATA_LAYOUT_ITEM);
1482                                 saveDataLayoutItemField(doc, element, (LayoutItemField) layoutItem);
1483                         } else if (layoutItem instanceof LayoutItemGroupBy) {
1484                                 final Element element = createElement(doc, nodeGroup, NODE_DATA_LAYOUT_ITEM_GROUPBY);
1485                                 saveDataLayoutItemGroupBy(doc, element, (LayoutItemGroupBy) layoutItem);
1486                         }
1487                 }
1488         }
1489
1490         /**
1491          * @param doc
1492          * @param element
1493          * @param layoutItem
1494          */
1495         private void saveDataLayoutItemGroupBy(org.w3c.dom.Document doc, Element element, LayoutItemGroupBy item) {
1496                 saveDataLayoutGroup(doc, element, item);
1497
1498                 final Element elementGroupBy = createElement(doc, element, NODE_GROUPBY);
1499                 saveDataLayoutItemField(doc, elementGroupBy, item.getFieldGroupBy());
1500
1501                 final Element elementSecondaryFields = createElement(doc, element, NODE_SECONDARY_FIELDS);
1502                 final Element elementLayoutGroup = createElement(doc, elementSecondaryFields, NODE_DATA_LAYOUT_GROUP);
1503                 saveDataLayoutGroup(doc, elementLayoutGroup, item.getSecondaryFields());
1504         }
1505
1506         /**
1507          * @param doc
1508          * @param element
1509          * @param layoutItem
1510          */
1511         private void saveDataLayoutItemField(final org.w3c.dom.Document doc, final Element element,
1512                         final LayoutItemField item) {
1513                 element.setAttribute(ATTRIBUTE_NAME, item.getName());
1514                 saveUsesRelationship(element, item);
1515
1516                 final CustomTitle customTitle = item.getCustomTitle();
1517                 if (customTitle != null) {
1518                         final Element elementCustomTitle = createElement(doc, element, NODE_CUSTOM_TITLE);
1519                         setAttributeAsBoolean(elementCustomTitle, ATTRIBUTE_CUSTOM_TITLE_USE_CUSTOM,
1520                                         customTitle.getUseCustomTitle());
1521                         saveTitle(doc, elementCustomTitle, customTitle); // LayoutItemField doesn't use its own title member.
1522                 }
1523
1524                 setAttributeAsBoolean(element, ATTRIBUTE_USE_DEFAULT_FORMATTING, item.getUseDefaultFormatting());
1525
1526                 final Element elementFormatting = createElement(doc, element, NODE_FORMATTING);
1527                 saveFormatting(elementFormatting, item.getFormatting());
1528         }
1529
1530         /**
1531          * @param element
1532          * @param item
1533          */
1534         private void saveUsesRelationship(Element element, UsesRelationship item) {
1535                 final Relationship relationship = item.getRelationship();
1536                 if (relationship != null) {
1537                         element.setAttribute(ATTRIBUTE_RELATIONSHIP_NAME, relationship.getName());
1538                 }
1539
1540                 final Relationship relatedRelationship = item.getRelatedRelationship();
1541                 if (relatedRelationship != null) {
1542                         element.setAttribute(ATTRIBUTE_RELATED_RELATIONSHIP_NAME, relatedRelationship.getName());
1543                 }
1544         }
1545
1546         /**
1547          * @param rootNode
1548          * @param nodeConnection
1549          * @return
1550          */
1551         private Element createElement(final org.w3c.dom.Document doc, final Element parentNode, final String name) {
1552                 Element node = doc.createElement(name);
1553                 parentNode.appendChild(node);
1554                 return node;
1555         }
1556
1557         public String getSelfHostedDirectoryPath() {
1558                 final String uriFile = getFileURI();
1559                 if (!StringUtils.isEmpty(uriFile)) {
1560                         final File file = new File(uriFile);
1561                         final File parent = file.getParentFile();
1562                         if (parent == null) {
1563                                 // TODO: Warn.
1564                                 return "";
1565                         }
1566
1567                         File dataDir = null;
1568                         switch (hostingMode) {
1569                         case HOSTING_MODE_POSTGRES_SELF:
1570                                 dataDir = new File(parent, "glom_postgres_data");
1571                                 break;
1572                         case HOSTING_MODE_POSTGRES_CENTRAL:
1573                                 dataDir = parent;
1574                                 break;
1575                         case HOSTING_MODE_SQLITE:
1576                                 dataDir = parent;
1577                                 break;
1578                         default:
1579                                 // TODO: Warn.
1580                                 break;
1581                         }
1582
1583                         if (dataDir != null) {
1584                                 return dataDir.getPath();
1585                         }
1586                 }
1587
1588                 // TODO: std::cerr << G_STRFUNC << ": returning empty string." << std::endl;
1589                 return "";
1590         }
1591
1592         /**
1593          */
1594         public void setConnectionDatabase(String databaseName) {
1595                 connectionDatabase = databaseName;
1596
1597         }
1598 }