/* * Copyright (C) 2011 Jan Pokorsky * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package cz.cas.lib.proarc.webapp.client; import com.google.gwt.core.client.Callback; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.NodeList; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.regexp.shared.SplitResult; import com.smartgwt.client.data.AdvancedCriteria; import com.smartgwt.client.data.Criterion; import com.smartgwt.client.data.DataSourceField; import com.smartgwt.client.data.Record; import com.smartgwt.client.data.RecordList; import com.smartgwt.client.data.ResultSet; import com.smartgwt.client.i18n.SmartGwtMessages; import com.smartgwt.client.types.OperatorId; import com.smartgwt.client.util.BooleanCallback; import com.smartgwt.client.util.JSONEncoder; import com.smartgwt.client.widgets.Canvas; import com.smartgwt.client.widgets.layout.Layout; import com.smartgwt.client.widgets.tile.TileGrid; import cz.cas.lib.proarc.webapp.client.widget.mods.NdkFormGenerator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EnumMap; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; /** * GWT client helper utilities. * * @author Jan Pokorsky */ public final class ClientUtils { private static final Logger LOG = Logger.getLogger(ClientUtils.class.getName()); private static final EnumMap<OperatorId, String> OPERATORS; static { OPERATORS = new EnumMap<OperatorId, String>(OperatorId.class); OPERATORS.put(OperatorId.LESS_OR_EQUAL, "<="); OPERATORS.put(OperatorId.LESS_THAN, "<"); OPERATORS.put(OperatorId.GREATER_OR_EQUAL, ">="); OPERATORS.put(OperatorId.GREATER_THAN, ">"); OPERATORS.put(OperatorId.EQUALS, "="); } /** * Helper to get modified {@link SmartGwtMessages }. * <p> It should be used instead of {@code GWT.create(SmartGwtMessages.class)}. */ public static SmartGwtMessages createSmartGwtMessages() { ModifiedSmartGwtMessages i18nSmartGwt = GWT.create(ModifiedSmartGwtMessages.class); return i18nSmartGwt; } /** * Simplified version of {@link String#format(java.lang.String, java.lang.Object[]) String.format} * For now it supports only {@code %s} format specifier. */ public static String format(String format, Object... args) { RegExp re = RegExp.compile("%s"); SplitResult split = re.split(format); StringBuilder sb = new StringBuilder(); sb.append(split.get(0)); for (int i = 1; i < split.length(); i++) { sb.append(args[i - 1]); sb.append(split.get(i)); } return sb.toString(); } /** * Logs a formatted message. * @param logger logger * @param level logging level * @param format see {@link #format(java.lang.String, java.lang.Object[]) format} doc * @param args arguments referenced by the format specifiers */ public static void log(Logger logger, Level level, String format, Object... args) { if (logger.isLoggable(level)) { logger.log(level, format(format, args)); } } /** Info {@link #log(java.util.logging.Logger, java.util.logging.Level, java.lang.String, java.lang.Object[]) log}. */ public static void info(Logger logger, String format, Object... args) { log(logger, Level.INFO, format, args); } /** Fine {@link #log(java.util.logging.Logger, java.util.logging.Level, java.lang.String, java.lang.Object[]) log}. */ public static void fine(Logger logger, String format, Object... args) { log(logger, Level.FINE, format, args); } /** Severe {@link #log(java.util.logging.Logger, java.util.logging.Level, java.lang.String, java.lang.Object[]) log}. */ public static void severe(Logger logger, String format, Object... args) { log(logger, Level.SEVERE, format, args); } /** Warning {@link #log(java.util.logging.Logger, java.util.logging.Level, java.lang.String, java.lang.Object[]) log}. */ public static void warning(Logger logger, String format, Object... args) { log(logger, Level.WARNING, format, args); } /** * Dumps Element content and traverse its children. * <p/><b>WARNING:</b> it is com.google.gwt.dom.client.Element not com.google.gwt.xml.client.Element!!! * * @param elm an element to dump * @param indent row indentation for current level * @param indentIncrement increment for next level * @param sb dumped content * @return dumped content */ public static StringBuilder dump(Element elm, String indent, String indentIncrement, StringBuilder sb) { int childCount = elm.getChildCount(); String innerText = elm.getInnerText(); String lang = elm.getLang(); String nodeName = elm.getNodeName(); short nodeType = elm.getNodeType(); String getString = elm.getString(); String tagNameWithNS = elm.getTagName(); String xmlLang = elm.getAttribute("xml:lang"); sb.append(ClientUtils.format("%sElement {nodeName: %s, nodeType: %s, tagNameWithNS: %s, lang: %s," + " childCount: %s, getString: %s, xmlLang: %s}\n", indent, nodeName, nodeType, tagNameWithNS, lang, childCount, getString, xmlLang)); NodeList<Node> childNodes = elm.getChildNodes(); indent += indentIncrement; for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.getItem(i); if (Element.is(child)) { dump(Element.as(child), indent, indentIncrement, sb); } else { sb.append(ClientUtils.format("%sNode: nodeType: %s, nodeName: %s, childCount: %s, nodeValue: %s\n", indent, child.getNodeType(), child.getNodeName(), child.getChildCount(), child.getNodeValue())); } } return sb; } public static String dump(Record r, String msg) { StringBuilder sb = new StringBuilder(); if (r == null) { return ClientUtils.format("%s, record is NULL", msg); } sb.append(ClientUtils.format("%s, getAttributes:\n", msg)); for (String attr : r.getAttributes()) { try { Object value = r.getAttributeAsObject(attr); sb.append(ClientUtils.format(" attr: %s, value: %s, class: %s\n", attr, value, safeGetClass(value))); } catch (Exception ex) { String value = r.getAttribute(attr); sb.append(ClientUtils.format(" !FAILED: attr: %s, value: %s, class: %s\n", attr, value, safeGetClass(value))); // Logger.getLogger("").log(Level.SEVERE, attr, ex); } } sb.append("-- toMap:\n"); Map<?, ?> m; try { m = r.toMap(); } catch (Exception ex) { // Logger.getLogger("").log(Level.SEVERE, "Record.toMap", ex); sb.append("Record.toMap FAILED"); return sb.toString(); } dump(m, " ", " ", sb); for (Map.Entry<?, ?> e : m.entrySet()) { Object value = e.getValue(); sb.append(ClientUtils.format(" map.key: %s, value: %s, value class %s\n", e.getKey(), value, safeGetClass(value))); if (value instanceof List) { List<?> l = (List) value; for (Object valItem : l) { sb.append(ClientUtils.format(" item.value: %s, value class %s\n", valItem, safeGetClass(valItem))); } } } return sb.toString(); } public static StringBuilder dump(List<?> l, String indent, String indentIncrement, StringBuilder sb) { for (Object valItem : l) { sb.append(ClientUtils.format("%sitem.value: %s, value class %s\n", indent, valItem, safeGetClass(valItem))); } return sb; } public static StringBuilder dump(Map<?, ?> m, String indent, String indentIncrement, StringBuilder sb) { for (Map.Entry<?, ?> e : m.entrySet()) { Object value = e.getValue(); sb.append(ClientUtils.format("%smap.key: %s, value: %s, value class %s\n", indent, e.getKey(), value, safeGetClass(value))); if (value instanceof List) { dump((List) value, indent, indentIncrement, sb); } else if (value instanceof Map) { dump((Map) value, indent + indentIncrement, indentIncrement, sb); } } return sb; } /** dumps object in JSON */ public static String dump(Object jso) { String dump; if (jso != null) { if (jso instanceof Record) { jso = ((Record) jso).getJsObj(); } else if (jso instanceof RecordList) { jso = ((RecordList) jso).getJsObj(); } try { dump = new JSONEncoder().encode(jso); } catch (Exception ex) { // this occurs in development mode sometimes; log it silently dump = String.valueOf(jso) + ", NPE: raise log level for details."; LOG.log(Level.FINE, dump, ex); } } else { dump = String.valueOf(jso); } return dump; } /** * Same like {@code value.geClass()} but prevents {@link NullPointerException}. * * @return class or {@code null} */ public static Class<?> safeGetClass(Object value) { return value != null ? value.getClass() : null; } /** * Helper to scroll to a particular tile. * @param grid a tile grid * @param tileIndex index of the tile */ public static void scrollToTile(TileGrid grid, int tileIndex) { int tileHeight = grid.getTileHeight(); int tilesPerLine = grid.getTilesPerLine(); int tileHMargin = grid.getTileHMargin(); int tileRow = tileIndex / tilesPerLine - 1; int tileColumn = tileIndex % tilesPerLine; int top = tileHMargin / 2 + (tileRow * (tileHeight + tileHMargin)); grid.scrollTo(1, top); } /** * Sets layout members just in case they differ from current members. */ public static void setMembers(Layout l, Canvas... members) { Canvas[] oldies = l.getMembers(); if (!Arrays.equals(oldies, members)) { l.setMembers(members); } } /** * Removes all attributes with {@code null} value. * * Useful before passing record to {@link DataSource} that encodes {@code null} * attributes as {@code "name":"null"} in JSON. * @param r record to process * @return copy of the record without {@code null} attributes */ public static Record removeNulls(Record r) { boolean hasNull = false; HashMap<Object, Object> nonNunlls = new HashMap<Object, Object>(); Map<?, ?> recordMap = r.toMap(); for (Map.Entry<?, ?> entry : recordMap.entrySet()) { Object value = entry.getValue(); if (value instanceof Collection && ((Collection) value).isEmpty()) { hasNull = true; } else if (value instanceof String && ((String) value).isEmpty()) { hasNull = true; } else if ("__ref".equals(entry.getKey())) { // ignore GWT attributes hasNull = true; } else if (value != null) { nonNunlls.put(entry.getKey(), value); } else { hasNull = true; } } return hasNull ? new Record(nonNunlls) : r; } /** * Removes empty and unrelated nodes from the data tree. * It skips empty collections, strings, synthetic GWT/SmartGWT/ProArc attributes. * @param r data to normalize * @return the reduced data tree or {@code null} if none value remains */ public static Record normalizeData(Record r) { Map<?,?> m = r.toMap(); m = normalizeData(m); return m == null ? null : new Record(m); } /** * Normalizes a map of values. It skips empty collections, strings, synthetic * GWT/SmartGWT/ProArc attributes. * @param m the map to normalize * @return the normalized map or {@code null} if none value remains */ private static Map<?,?> normalizeData(Map m) { for (Iterator<Map.Entry> it = m.entrySet().iterator(); it.hasNext();) { Entry entry = it.next(); Object value = entry.getValue(); if ("__ref".equals(entry.getKey())) { // ignore GWT attributes it.remove(); } else if (NdkFormGenerator.HIDDEN_FIELDS_NAME.equals(entry.getKey())) { it.remove(); } else if (value instanceof List) { List list = (List) value; Object data = normalizeData(list); if (data == null || value instanceof Collection && ((Collection) value).isEmpty()) { it.remove(); } else { entry.setValue(data); } } else if (value instanceof Collection || value instanceof Map) { // GWT 2.5.1: fix instanceof to handle Map together with Collection here value = normalizeObjectData(value); if (value instanceof Collection && ((Collection) value).isEmpty()) { it.remove(); } } else if (value instanceof String && ((String) value).isEmpty()) { it.remove(); } else if (value != null) { // no-op } else { it.remove(); } } if (m.isEmpty()) { return null; } return m; } /** * Normalizes a list of items. It it skips empty collections, strings, .... * @param l the list to normalize * @return the list of normalized items or the normalized item * or {@code null} if none value remains */ private static Object normalizeData(List l) { for (Iterator it = l.iterator(); it.hasNext();) { Object value = it.next(); value = normalizeObjectData(value); if (value instanceof Collection && ((Collection) value).isEmpty()) { it.remove(); } else if (value instanceof String && ((String) value).isEmpty()) { it.remove(); } else if (value != null) { // no-op } else { it.remove(); } } if (l.isEmpty()) { return null; } else if (l.size() == 1) { return l.get(0); } return l; } private static Object normalizeObjectData(Object obj) { Object result; if (obj instanceof Record) { result = normalizeData((Record) obj); } else if (obj instanceof Map) { result = normalizeData((Map) obj); } else if (obj instanceof List) { result = normalizeData((List) obj); } else { result = obj; } return result; } /** * Replacement for {@link ResultSet#getRange(int, int) } to work around * {@link ArrayStoreException} thrown in production mode. * * @see <a href='http://forums.smartclient.com/showpost.php?p=85402&postcount=34'>proposed workaround</a> * @see <a href='http://forums.smartclient.com/showpost.php?p=85402&postcount=38'>Fixed in SmartGWT 3.1</a> * @since SmartGWT 3.0 */ public static void getRangeWorkAround(ResultSet resultSet, int start, int end) { try { resultSet.getRange(start, end); } catch (ArrayStoreException ex) { LOG.log(Level.SEVERE, null, ex); } } /** * Helper to get value map from the result set as a linked hash map. * It specifies origin contract of {@link ResultSet#getValueMap(String, String) ResultSet.getValueMap} * to make the map usable for e.g. * {@link com.smartgwt.client.widgets.form.fields.FormItem#setValueMap(java.util.LinkedHashMap) FormItem}. * * @param rs result set * @param idField field name holding IDs * @param displayField field name holding display values * @return linked map */ public static LinkedHashMap<?, ?> getValueMap(ResultSet rs, String idField, String displayField) { Map<?, ?> valueMap = rs.getValueMap(idField, displayField); // ResultSet implementation (SmartGWT 3.0) returns the map as LinkedHashMap, // no extra processing required for now return (LinkedHashMap<?, ?>) valueMap; } public static final BooleanCallback EMPTY_BOOLEAN_CALLBACK = new BooleanCallback() { @Override public void execute(Boolean value) { // no op } }; private static final Callback<?,?> EMPTY_CALLBACK = new Callback<Object, Object>() { @Override public void onFailure(Object reason) { } @Override public void onSuccess(Object result) { } }; @SuppressWarnings("unchecked") public static <T,F> Callback<T,F> emptyCallback() { return (Callback<T, F>) EMPTY_CALLBACK; } /** * Copies given attribute from each record to standalone array. */ public static String[] toFieldValues(Record[] records, String attributeName) { String[] items = new String[records.length]; for (int i = 0; i < records.length; i++) { items[i] = records[i].getAttribute(attributeName); } return items; } /** * Gets advanced criteria as HTTP GET params. * @param ac advanced criteria * @param map map of params */ public static void advanceCriteriaAsParams(AdvancedCriteria ac, HashMap<String, Object> map, HashMap<String, String> dateFields) { Criterion[] criteria = ac.getCriteria(); if (criteria == null) { return ; } for (Criterion criterion : criteria) { String fieldName = criterion.getFieldName(); if (criterion.isAdvanced() && fieldName == null) { advanceCriteriaAsParams(criterion.asAdvancedCriteria(), map, dateFields); } else { String filterName = dateFields.get(fieldName); if (filterName != null) { ArrayList<Object> dates = (ArrayList<Object>) map.get(filterName); if (dates == null) { dates = new ArrayList<Object>(); map.put(filterName, dates); } String operatorStr = OPERATORS.get(criterion.getOperator()); if (operatorStr != null) { dates.add(operatorStr); } dates.add(criterion.getValueAsDate()); } else { map.put(fieldName, criterion.getValueAsString()); } } } } public static final class DataSourceFieldBuilder<T extends DataSourceField> { public static final OperatorId[] TEXT_OPERATIONS = { OperatorId.EQUALS, OperatorId.NOT_EQUAL, OperatorId.ICONTAINS, OperatorId.INOT_CONTAINS, OperatorId.ISTARTS_WITH, OperatorId.IENDS_WITH}; private final T field; public static <T extends DataSourceField> DataSourceFieldBuilder<T> field(T field) { return new DataSourceFieldBuilder<T>(field); } public DataSourceFieldBuilder(T field) { this.field = field; } public DataSourceFieldBuilder<T> required() { field.setRequired(true); return this; } public DataSourceFieldBuilder<T> primaryKey() { field.setPrimaryKey(true); return this; } public DataSourceFieldBuilder<T> hidden() { field.setHidden(true); return this; } public DataSourceFieldBuilder<T> filter(boolean filter) { field.setCanFilter(filter); return this; } public DataSourceFieldBuilder<T> validOperators(OperatorId... ids) { field.setValidOperators(ids); return this; } public T build() { return field; } } /** * Helper to wait on running asynchronous tasks. * It runs as soon as all expectations are released. */ public static abstract class SweepTask implements Runnable, BooleanCallback { private int semaphore = 0; /** * Provides task implementation. */ protected abstract void processing(); /** * Call to expect further async task. */ public SweepTask expect() { semaphore++; return this; } /** * Call when some async task is done. */ public void release() { semaphore--; if (semaphore == 0) { processing(); } } /** * Async call of release in case the value is true. */ @Override public void execute(Boolean value) { if (Boolean.TRUE.equals(value)) { release(); } } /** * Async call of release. * @param value */ @Override public void run() { release(); } } }