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