/*
* 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();
}
}
}