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