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