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