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