Java tutorial
/* * Copyright (C) 2011 Openismus GmbH * * This file is part of GWT-Glom. * * GWT-Glom is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with GWT-Glom. If not, see <http://www.gnu.org/licenses/>. */ package org.glom.web.server; import java.beans.PropertyVetoException; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Locale; import org.apache.commons.lang3.StringUtils; import org.glom.web.server.database.DetailsDBAccess; import org.glom.web.server.database.ListViewDBAccess; import org.glom.web.server.database.RelatedListDBAccess; import org.glom.web.server.database.RelatedListNavigation; import org.glom.web.server.libglom.Document; import org.glom.web.shared.DataItem; import org.glom.web.shared.DocumentInfo; import org.glom.web.shared.NavigationRecord; import org.glom.web.shared.Reports; import org.glom.web.shared.TypedDataItem; import org.glom.web.shared.libglom.CustomTitle; import org.glom.web.shared.libglom.Field; import org.glom.web.shared.libglom.Relationship; import org.glom.web.shared.libglom.Report; import org.glom.web.shared.libglom.Translatable; import org.glom.web.shared.libglom.layout.LayoutGroup; import org.glom.web.shared.libglom.layout.LayoutItem; import org.glom.web.shared.libglom.layout.LayoutItemField; import org.glom.web.shared.libglom.layout.LayoutItemImage; import org.glom.web.shared.libglom.layout.LayoutItemPortal; import org.glom.web.shared.libglom.layout.UsesRelationship; import com.mchange.v2.c3p0.ComboPooledDataSource; /** * A class to hold configuration information for a given Glom document. This class retrieves layout information from * libglom and data from the underlying PostgreSQL database. */ final class ConfiguredDocument { private Document document; private String documentID = ""; private String defaultLocaleID = ""; /** Credentials for the document as specified in the config file, * so the user does not need to specify them. * * @param docCredentials */ private Credentials credentials; private static class LayoutLocaleMap extends Hashtable<String, List<LayoutGroup>> { private static final long serialVersionUID = 6542501521673767267L; }; private static class TableLayouts { public LayoutLocaleMap listLayouts; public LayoutLocaleMap detailsLayouts; } private static class TableLayoutsForLocale extends Hashtable<String, TableLayouts> { private static final long serialVersionUID = -1947929931925049013L; public LayoutGroup getListLayout(final String tableName, final String locale) { final List<LayoutGroup> groups = getLayout(tableName, locale, false); if (groups == null) { return null; } if (groups.isEmpty()) { return null; } return groups.get(0); } public List<LayoutGroup> getDetailsLayout(final String tableName, final String locale) { return getLayout(tableName, locale, true); } public void setListLayout(final String tableName, final String locale, final LayoutGroup layout) { final List<LayoutGroup> list = new ArrayList<LayoutGroup>(); list.add(layout); setLayout(tableName, locale, list, false); } public void setDetailsLayout(final String tableName, final String locale, final List<LayoutGroup> layout) { setLayout(tableName, locale, layout, true); } private List<LayoutGroup> getLayout(final String tableName, final String locale, final boolean details) { final LayoutLocaleMap map = getMap(tableName, details); if (map == null) { return null; } return map.get(locale); } private LayoutLocaleMap getMap(final String tableName, final boolean details) { final TableLayouts tableLayouts = get(tableName); if (tableLayouts == null) { return null; } LayoutLocaleMap map = null; if (details) { map = tableLayouts.detailsLayouts; } else { map = tableLayouts.listLayouts; } return map; } private LayoutLocaleMap getMapWithAdd(final String tableName, final boolean details) { TableLayouts tableLayouts = get(tableName); if (tableLayouts == null) { tableLayouts = new TableLayouts(); put(tableName, tableLayouts); } LayoutLocaleMap map = null; if (details) { if (tableLayouts.detailsLayouts == null) { tableLayouts.detailsLayouts = new LayoutLocaleMap(); } map = tableLayouts.detailsLayouts; } else { if (tableLayouts.listLayouts == null) { tableLayouts.listLayouts = new LayoutLocaleMap(); } map = tableLayouts.listLayouts; } return map; } private void setLayout(final String tableName, final String locale, final List<LayoutGroup> layout, final boolean details) { final LayoutLocaleMap map = getMapWithAdd(tableName, details); if (map != null) { map.put(locale, layout); } } } private final TableLayoutsForLocale mapTableLayouts = new TableLayoutsForLocale(); @SuppressWarnings("unused") private ConfiguredDocument() { // disable default constructor } ConfiguredDocument(final Document document) throws PropertyVetoException { this.document = document; } Document getDocument() { return document; } String getDocumentID() { return documentID; } void setDocumentID(final String documentID) { this.documentID = documentID; } String getDefaultLocaleID() { return defaultLocaleID; } void setDefaultLocaleID(final String localeID) { this.defaultLocaleID = localeID; } /** * @return */ DocumentInfo getDocumentInfo(final String localeID) { final DocumentInfo documentInfo = new DocumentInfo(); // get arrays of table names and titles, and find the default table index final List<String> tablesVec = document.getTableNames(); final int numTables = Utils.safeLongToInt(tablesVec.size()); // we don't know how many tables will be hidden so we'll use half of the number of tables for the default size // of the ArrayList final ArrayList<String> tableNames = new ArrayList<String>(numTables / 2); final ArrayList<String> tableTitles = new ArrayList<String>(numTables / 2); boolean foundDefaultTable = false; int visibleIndex = 0; for (int i = 0; i < numTables; i++) { final String tableName = tablesVec.get(i); if (!document.getTableIsHidden(tableName)) { tableNames.add(tableName); //The comparison will only be called if we haven't already found the default table if (!foundDefaultTable && tableName.equals(document.getDefaultTable())) { documentInfo.setDefaultTableIndex(visibleIndex); foundDefaultTable = true; } tableTitles.add(document.getTableTitle(tableName, localeID)); visibleIndex++; } } // set everything we need documentInfo.setTableNames(tableNames); documentInfo.setTableTitles(tableTitles); documentInfo.setTitle(document.getDatabaseTitle(localeID)); // Fetch arrays of locale IDs and titles: final List<String> localesVec = document.getTranslationAvailableLocales(); final int numLocales = Utils.safeLongToInt(localesVec.size()); final ArrayList<String> localeIDs = new ArrayList<String>(numLocales); final ArrayList<String> localeTitles = new ArrayList<String>(numLocales); for (int i = 0; i < numLocales; i++) { final String this_localeID = localesVec.get(i); localeIDs.add(this_localeID); // Use java.util.Locale to get a title for the locale: final String[] locale_parts = this_localeID.split("_"); String locale_lang = this_localeID; if (locale_parts.length > 0) { locale_lang = locale_parts[0]; } String locale_country = ""; if (locale_parts.length > 1) { locale_country = locale_parts[1]; } final Locale locale = new Locale(locale_lang, locale_country); final String title = locale.getDisplayName(locale); localeTitles.add(title); } documentInfo.setLocaleIDs(localeIDs); documentInfo.setLocaleTitles(localeTitles); return documentInfo; } /* * Gets the layout group for the list view using the defined layout list in the document or the table fields if * there's no defined layout group for the list view. */ private LayoutGroup getValidListViewLayoutGroup(final String tableName, final String localeID) { // Try to return a cached version: final LayoutGroup result = mapTableLayouts.getListLayout(tableName, localeID); if (result != null) { updateLayoutGroupExpectedResultSize(result, tableName); return result; } final List<LayoutGroup> layoutGroupVec = document.getDataLayoutGroups(Document.LAYOUT_NAME_LIST, tableName); final int listViewLayoutGroupSize = Utils.safeLongToInt(layoutGroupVec.size()); LayoutGroup libglomLayoutGroup = null; if (listViewLayoutGroupSize > 0) { // A list layout group is defined. // We use the first group as the list. if (listViewLayoutGroupSize > 1) { Log.warn(documentID, tableName, "The size of the list layout group is greater than 1. " + "Attempting to use the first item for the layout list view."); } libglomLayoutGroup = layoutGroupVec.get(0); } else { // A list layout group is *not* defined; we are going make a LayoutGroup from the list of fields. // This is unusual. Log.info(documentID, tableName, "A list layout is not defined for this table. Displaying a list layout based on the field list."); final List<Field> fieldsVec = document.getTableFields(tableName); libglomLayoutGroup = new LayoutGroup(); for (int i = 0; i < fieldsVec.size(); i++) { final Field field = fieldsVec.get(i); final LayoutItemField layoutItemField = new LayoutItemField(); layoutItemField.setFullFieldDetails(field); libglomLayoutGroup.addItem(layoutItemField); } } // Clone the group and change the clone, to discard unwanted information // (such as translations or binary image data) and to // store some information that we do not want to calculate on the client side. // Note that we don't use clone() here, because that would need clone() implementations // in classes which are also used in the client code (though the clone() methods would // not be used) and that makes the GWT java->javascript compilation fail. final LayoutGroup cloned = (LayoutGroup) Utils.deepCopy(libglomLayoutGroup); if (cloned != null) { updateTopLevelListLayoutGroup(cloned, tableName, localeID); // Discard unwanted translations so that getTitle(void) returns what we want. updateTitlesForLocale(cloned, localeID); // Discard binary image data: updateLayoutItemImages(cloned); } // Store it in the cache for next time. mapTableLayouts.setListLayout(tableName, localeID, cloned); return cloned; } /** * @param libglomLayoutGroup */ private void updateTopLevelListLayoutGroup(final LayoutGroup layoutGroup, final String tableName, final String localeID) { final List<LayoutItem> layoutItemsVec = layoutGroup.getItems(); int primaryKeyIndex = -1; final int numItems = Utils.safeLongToInt(layoutItemsVec.size()); for (int i = 0; i < numItems; i++) { final LayoutItem layoutItem = layoutItemsVec.get(i); if (layoutItem instanceof LayoutItemField) { final LayoutItemField layoutItemField = (LayoutItemField) layoutItem; final Field field = layoutItemField.getFullFieldDetails(); if ((field != null) && field.getPrimaryKey()) { primaryKeyIndex = i; } } } // Set the primary key index for the table if (primaryKeyIndex < 0) { // Add a LayoutItemField for the primary key to the end of the item list in the LayoutGroup because it // doesn't already contain a primary key. Field primaryKey = null; final List<Field> fieldsVec = document.getTableFields(tableName); for (int i = 0; i < Utils.safeLongToInt(fieldsVec.size()); i++) { final Field field = fieldsVec.get(i); if (field.getPrimaryKey()) { primaryKey = field; break; } } if (primaryKey != null) { final LayoutItemField layoutItemField = new LayoutItemField(); layoutItemField.setName(primaryKey.getName()); layoutItemField.setFullFieldDetails(primaryKey); layoutGroup.addItem(layoutItemField); layoutGroup.setPrimaryKeyIndex(layoutGroup.getItems().size() - 1); layoutGroup.setHiddenPrimaryKey(true); } else { Log.error(document.getDatabaseTitleOriginal(), tableName, "A primary key was not found in the FieldVector for this table. Navigation buttons will not work."); } } else { layoutGroup.setPrimaryKeyIndex(primaryKeyIndex); } } private void updateLayoutGroupExpectedResultSize(final LayoutGroup layoutGroup, final String tableName) { //TODO: Remove this? /* final ListViewDBAccess listViewDBAccess = new ListViewDBAccess(document, documentID, cpds, tableName, layoutGroup); layoutGroup.setExpectedResultSize(listViewDBAccess.getExpectedResultSize()); */ } /** * * @param tableName * @param quickFind * @param start * @param length * @param useSortClause * @param sortColumnIndex * The index of the column to sort by, or -1 for none. * @param isAscending * @return */ public ArrayList<DataItem[]> getListViewData(ComboPooledDataSource cpds, String tableName, final String quickFind, final int start, final int length, final boolean useSortClause, final int sortColumnIndex, final boolean isAscending) { // Validate the table name. tableName = getTableNameToUse(tableName); // Get the LayoutGroup that represents the list view. // TODO: Performance: Avoid calling this again: final LayoutGroup libglomLayoutGroup = getValidListViewLayoutGroup(tableName, "" /* irrelevant locale */); // Create a database access object for the list view. final ListViewDBAccess listViewDBAccess = new ListViewDBAccess(document, documentID, cpds, tableName, libglomLayoutGroup); // Return the data. return listViewDBAccess.getData(quickFind, start, length, sortColumnIndex, isAscending); } DataItem[] getDetailsData(ComboPooledDataSource cpds, String tableName, final TypedDataItem primaryKeyValue) { // Validate the table name. tableName = getTableNameToUse(tableName); setDataItemType(tableName, primaryKeyValue); final DetailsDBAccess detailsDBAccess = new DetailsDBAccess(document, documentID, cpds, tableName); return detailsDBAccess.getData(primaryKeyValue); } /** * This make sure that the primary key value knows its actual type, * in case it was received via a URL parameter as a string representation: * * @param tableName The name of the table for which this is the primary key value * @param primaryKeyValue The TypedDataItem to be changes. */ private void setDataItemType(final String tableName, final TypedDataItem primaryKeyValue) { if (primaryKeyValue.isUnknownType()) { final Field primaryKeyField = document.getTablePrimaryKeyField(tableName); if (primaryKeyField == null) { Log.error(document.getDatabaseTitleOriginal(), tableName, "Could not find the primary key."); } Utils.transformUnknownToActualType(primaryKeyValue, primaryKeyField.getGlomType()); } } /** * * @param tableName * @param portal * @param foreignKeyValue * @param start * @param length * @param sortColumnIndex * The index of the column to sort by, or -1 for none. * @param isAscending * @return */ ArrayList<DataItem[]> getRelatedListData(ComboPooledDataSource cpds, String tableName, final LayoutItemPortal portal, final TypedDataItem foreignKeyValue, final int start, final int length, final int sortColumnIndex, final boolean isAscending) { if (portal == null) { Log.error("getRelatedListData(): portal is null"); return null; } // Validate the table name. tableName = getTableNameToUse(tableName); // Create a database access object for the related list final RelatedListDBAccess relatedListDBAccess = new RelatedListDBAccess(document, documentID, cpds, tableName, portal); // Return the data return relatedListDBAccess.getData(start, length, foreignKeyValue, sortColumnIndex, isAscending); } List<LayoutGroup> getDetailsLayoutGroup(String tableName, final String localeID) { // Validate the table name. tableName = getTableNameToUse(tableName); // Try to return a cached version: final List<LayoutGroup> result = mapTableLayouts.getDetailsLayout(tableName, localeID); if (result != null) { updatePortalsExtras(result, tableName); // Update expected results sizes. return result; } final List<LayoutGroup> listGroups = document.getDataLayoutGroups(Document.LAYOUT_NAME_DETAILS, tableName); // Clone the group and change the clone, to discard unwanted information // (such as translations or binary image data) // and to store some information that we do not want to calculate on the client side. // Note that we don't use clone() here, because that would need clone() implementations // in classes which are also used in the client code (though the clone() methods would // not be used) and that makes the GWT java->javascript compilation fail. final List<LayoutGroup> listCloned = new ArrayList<LayoutGroup>(); for (final LayoutGroup group : listGroups) { final LayoutGroup cloned = (LayoutGroup) Utils.deepCopy(group); if (cloned != null) { listCloned.add(cloned); } } updatePortalsExtras(listCloned, tableName); updateFieldsExtras(listCloned, tableName); // Discard unwanted translations so that getTitle(void) returns what we want. updateTitlesForLocale(listCloned, localeID); // Discard binary image data: updateLayoutItemImages(listCloned); // Store it in the cache for next time. mapTableLayouts.setDetailsLayout(tableName, localeID, listCloned); return listCloned; } /** Discard binary image data. * @param listCloned */ private void updateLayoutItemImages(final List<LayoutGroup> listGroups) { for (final LayoutGroup group : listGroups) { updateLayoutItemImages(group); } } /** Discard binary image data. * @param group */ private void updateLayoutItemImages(final LayoutGroup group) { final List<LayoutItem> childItems = group.getItems(); for (final LayoutItem item : childItems) { if (item instanceof LayoutGroup) { // Recurse: final LayoutGroup childGroup = (LayoutGroup) item; updateLayoutItemImages(childGroup); } else if (item instanceof LayoutItemImage) { final LayoutItemImage imageItem = (LayoutItemImage) item; final DataItem image = imageItem.getImage(); image.setImageData(null); //The client can now request the full data via the path in DataItem.getImageDataUrl(). } } } /** * @param result * @param tableName */ private void updatePortalsExtras(final List<LayoutGroup> listGroups, final String tableName) { for (final LayoutGroup group : listGroups) { updatePortalsExtras(group, tableName); } } /** * @param result * @param tableName */ private void updateFieldsExtras(final List<LayoutGroup> listGroups, final String tableName) { for (final LayoutGroup group : listGroups) { updateFieldsExtras(group, tableName); } } /** * @param result * @param tableName */ private void updateTitlesForLocale(final List<LayoutGroup> listGroups, final String localeID) { for (final LayoutGroup group : listGroups) { updateTitlesForLocale(group, localeID); } } private void updatePortalsExtras(final LayoutGroup group, final String tableName) { if (group instanceof LayoutItemPortal) { final LayoutItemPortal portal = (LayoutItemPortal) group; final String tableNameUsed = portal.getTableUsed(tableName); updateLayoutGroupExpectedResultSize(portal, tableNameUsed); //Do not add a primary key field if there is already one: if (portal.getPrimaryKeyIndex() != -1) return; final Relationship relationship = portal.getRelationship(); if (relationship != null) { // Cache the navigation information: // layoutItemPortal.set_name(libglomLayoutItemPortal.get_relationship_name_used()); // layoutItemPortal.setTableName(relationship.get_from_table()); // layoutItemPortal.setFromField(relationship.get_from_field()); // get the primary key for the related list table final String toTableName = relationship.getToTable(); if (!StringUtils.isEmpty(toTableName)) { // get the LayoutItemField with details from its Field in the document final List<Field> fields = document.getTableFields(toTableName); // TODO_Performance: Cache this. for (final Field field : fields) { // check the names to see if they're the same if (field.getPrimaryKey()) { final LayoutItemField layoutItemField = new LayoutItemField(); layoutItemField.setName(field.getName()); layoutItemField.setFullFieldDetails(field); portal.addItem(layoutItemField); portal.setPrimaryKeyIndex(portal.getItems().size() - 1); portal.setHiddenPrimaryKey(true); // always hidden in portals break; } } } } } final List<LayoutItem> childItems = group.getItems(); for (final LayoutItem item : childItems) { if (item instanceof LayoutGroup) { final LayoutGroup childGroup = (LayoutGroup) item; updatePortalsExtras(childGroup, tableName); } } } private void updateFieldsExtras(final LayoutGroup group, final String tableName) { final List<LayoutItem> childItems = group.getItems(); for (final LayoutItem item : childItems) { if (item instanceof LayoutGroup) { // Recurse: final LayoutGroup childGroup = (LayoutGroup) item; updateFieldsExtras(childGroup, tableName); } else if (item instanceof LayoutItemField) { final LayoutItemField field = (LayoutItemField) item; // Set whether the field should have a navigation button, // because it identifies a related record. final String navigationTableName = document.getLayoutItemFieldShouldHaveNavigation(tableName, field); if (navigationTableName != null) { field.setNavigationTableName(navigationTableName); } } } } private void updateTitlesForLocale(final LayoutGroup group, final String localeID) { updateItemTitlesForLocale(group, localeID); final List<LayoutItem> childItems = group.getItems(); for (final LayoutItem item : childItems) { // Call makeTitleOriginal on all Translatable items and all special // Translatable items that they use: if (item instanceof LayoutItemField) { final LayoutItemField layoutItemField = (LayoutItemField) item; final Field field = layoutItemField.getFullFieldDetails(); if (field != null) { field.makeTitleOriginal(localeID); } final CustomTitle customTitle = layoutItemField.getCustomTitle(); if (customTitle != null) { customTitle.makeTitleOriginal(localeID); } } updateItemTitlesForLocale(item, localeID); if (item instanceof LayoutGroup) { // Recurse: final LayoutGroup childGroup = (LayoutGroup) item; updateTitlesForLocale(childGroup, localeID); } } } private void updateItemTitlesForLocale(final LayoutItem item, final String localeID) { if (item instanceof UsesRelationship) { final UsesRelationship usesRelationship = (UsesRelationship) item; final Relationship rel = usesRelationship.getRelationship(); if (rel != null) { rel.makeTitleOriginal(localeID); } final Relationship relatedRel = usesRelationship.getRelatedRelationship(); if (relatedRel != null) { relatedRel.makeTitleOriginal(localeID); } } if (item instanceof Translatable) { final Translatable translatable = item; translatable.makeTitleOriginal(localeID); } } /* * Gets the expected row count for a related list. */ int getRelatedListRowCount(ComboPooledDataSource cpds, String tableName, final LayoutItemPortal portal, final TypedDataItem foreignKeyValue) { if (portal == null) { Log.error("getRelatedListData(): portal is null"); return 0; } // Validate the table name. tableName = getTableNameToUse(tableName); // Create a database access object for the related list final RelatedListDBAccess relatedListDBAccess = new RelatedListDBAccess(document, documentID, cpds, tableName, portal); // Return the row count return relatedListDBAccess.getExpectedResultSize(foreignKeyValue); } NavigationRecord getSuitableRecordToViewDetails(ComboPooledDataSource cpds, String tableName, final LayoutItemPortal portal, final TypedDataItem primaryKeyValue) { // Validate the table name. tableName = getTableNameToUse(tableName); setDataItemType(tableName, primaryKeyValue); final RelatedListNavigation relatedListNavigation = new RelatedListNavigation(document, documentID, cpds, tableName, portal); return relatedListNavigation.getNavigationRecord(primaryKeyValue); } LayoutGroup getListViewLayoutGroup(String tableName, final String localeID) { // Validate the table name. tableName = getTableNameToUse(tableName); return getValidListViewLayoutGroup(tableName, localeID); } /** * Gets the table name to use when accessing the database and the document. This method guards against SQL injection * attacks by returning the default table if the requested table is not in the database or if the table name has not * been set. * * @param tableName * The table name to validate. * @return The table name to use. */ private String getTableNameToUse(final String tableName) { if (StringUtils.isEmpty(tableName) || !document.getTableIsKnown(tableName)) { return document.getDefaultTable(); } return tableName; } /** * @param tableName * @param localeID * @return */ public Reports getReports(final String tableName, final String localeID) { final Reports result = new Reports(); final List<String> names = document.getReportNames(tableName); final int count = Utils.safeLongToInt(names.size()); for (int i = 0; i < count; i++) { final String name = names.get(i); final Report report = document.getReport(tableName, name); if (report == null) { continue; } final String title = report.getTitle(localeID); result.addReport(name, title); } return result; } /** * Set the credentials for the document as specified in the config file * so the user does not need to specify them. * These might be the global credentials, if the document-specific * credentials failed. * * @param docCredentials */ public void setCredentials(final Credentials docCredentials) { this.credentials = docCredentials; } /** * Get the credentials for the document as specified in the config file * so the user does not need to specify them. * These might be the global credentials, if the document-specific * credentials failed. * */ Credentials getCredentials() { return this.credentials; } }