All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.tsl2.nano.bean.BeanUtil Maven / Gradle / Ivy

Go to download

TSL2 Framework Descriptor (currency-handling, generic formatter, descriptors for beans, collections, actions and values)

There is a newer version: 2.5.1
Show newest version
/*
 * Copyright © 2002-2009 Thomas Schneider
 * Alle Rechte vorbehalten.
 * Weiterverbreitung, Benutzung, Vervielfältigung oder Offenlegung,
 * auch auszugsweise, nur mit Genehmigung.
 * 
 * $Id$ 
 */
package de.tsl2.nano.bean;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StreamTokenizer;
import java.text.Format;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.logging.Log;

import de.tsl2.nano.bean.def.Bean;
import de.tsl2.nano.bean.def.BeanCollector;
import de.tsl2.nano.bean.def.BeanDefinition;
import de.tsl2.nano.bean.def.IAttributeDefinition;
import de.tsl2.nano.bean.def.NamedValue;
import de.tsl2.nano.collection.CollectionUtil;
import de.tsl2.nano.core.ManagedException;
import de.tsl2.nano.core.Messages;
import de.tsl2.nano.core.cls.BeanAttribute;
import de.tsl2.nano.core.cls.BeanClass;
import de.tsl2.nano.core.cls.IAttribute;
import de.tsl2.nano.core.log.LogFactory;
import de.tsl2.nano.core.util.DefaultFormat;
import de.tsl2.nano.core.util.FormatUtil;
import de.tsl2.nano.core.util.ListSet;
import de.tsl2.nano.core.util.NumberUtil;
import de.tsl2.nano.core.util.ObjectUtil;
import de.tsl2.nano.core.util.StringUtil;
import de.tsl2.nano.core.util.Util;
import de.tsl2.nano.format.RegExpFormat;
import de.tsl2.nano.util.operation.IConverter;

/**
 * A Utility-Class for beans
 * 
 * @author ts 05.03.2009
 * @version $Revision$
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class BeanUtil extends ObjectUtil {
    private static final Log LOG = LogFactory.getLog(BeanUtil.class);
    /**
     * as BeanUtil.copyProperties(..) does only a shallow copy, this method does a deep recursive copy.
     * 

* uses lamdas of jdk 1.8. handles: iterable, enum, number - not handled yet: map, array */ /* @SuppressWarnings({ "rawtypes", "unchecked" }) private static Object deepCopy(Object src, Object dest) throws Exception { BeanUtils.copyProperties(src, dest); PropertyDescriptor[] pdSrc = BeanUtils.getPropertyDescriptors(src.getClass()); PropertyDescriptor pdDest; String name; Object vold, vnew; Class type; for (int i = 0; i < pdSrc.length; i++) { name = pdSrc[i].getName(); vold = null; vnew = null; pdDest = BeanUtils.getPropertyDescriptor(dest.getClass(), name); if (pdDest != null && pdSrc[i].getReadMethod() != null && (vold = pdSrc[i].getReadMethod().invoke(src)) != null && (pdDest.getReadMethod() == null || (vnew = pdDest.getReadMethod().invoke(dest)) == null)) { type = pdDest.getReadMethod().getReturnType(); //create instance of enum, number or entity vnew = type.isEnum() ? Enum.valueOf((Class) type, ((Enum) vold).name()) : Number.class.isAssignableFrom(type) ? type.getConstructor(String.class).newInstance(vold.toString()) : type.newInstance(); //set the new value and go into recursion pdDest.getWriteMethod().invoke(dest, deepCopy(vold, vnew)); } else if (vold != null && Iterable.class.isAssignableFrom(type = pdDest.getReadMethod().getReturnType())) { //handle collections with recursion Collection it = (Collection) vold; Collection cnew = (Collection) vnew; Object genType = pdDest.getReadMethod().getGenericReturnType(); if (genType instanceof ParameterizedType) { genType = ((ParameterizedType) genType).getActualTypeArguments()[0]; } if (genType instanceof Class) { Class gtype = (Class) genType; boolean javaType = gtype.getPackage().getName().startsWith("java"); it.forEach((Object o) -> { try { cnew.add(javaType ? o : deepCopy(o, gtype.newInstance())); } catch (Exception e) { throw new RuntimeException(e); } }); } } } return dest; } */ /** * creates new objects, cloned from defaultClone and with increasing attribute value - evaluated by first+count+step. * @param defaultClone * @param attributeName * @param first first attribute value - defaultClone can contain the value or this must not be null * @param count number of objects to create * @param step step of increase * @return list of new created objects */ public static List create(S defaultClone, String attributeName, Object first, int count, double step) { Object clone, value; ArrayList result = new ArrayList(); if (first == null) { first = Bean.getBean(defaultClone).getAttribute(attributeName).getValue(); } long start = NumberUtil.toNumber(first); long end = (long) (start + count * step); for (long i = start; i < end; i+=step) { clone = clone(defaultClone); value = NumberUtil.fromNumber(i, first.getClass()); Bean.getBean(clone).getAttribute(attributeName).setValue(value); result.add(clone); } return result; } /** * wraps all attributes having a collection as value into a new {@link ListSet} instance to unbind a * {@link #clone()} instance. * * @param src instance to wrap the attribute values for * @return the instance itself */ public static S createOwnCollectionInstances(S src) { BeanClass bc = (BeanClass) BeanClass.getBeanClass(src.getClass()); List attributes = bc.getAttributes(); for (IAttribute a : attributes) { if (Collection.class.isAssignableFrom(a.getType())) { Collection v = (Collection) a.getValue(src); if (v != null) { LOG.debug("creating own collection instance for " + a.getName() + " with" + v.size() + " elements"); a.setValue(src, new ListSet(v)); } } } return src; } /** * calls a method through reflection * * @param clazz class to load * @param method method to call * @param args optional arguments * @return result of method calling */ public static Object call(String clazz, String method, Object... args) { return BeanClass.createBeanClass(clazz).callMethod(null, method, null, args); } /** * calls {@link #call(String, String, Object...)} to start any java method * * @param args */ public static final void main(String args[]) { System.out.println("calling BeanUtil.main"); String clazz = args[0]; String method = args[1]; Object[] objs = CollectionUtil.copyOfRange(args, 2, args.length, Object[].class); call(clazz, method, objs); } /** * fills a map with all bean-attribute-names and their values * * @param o bean * @return see {@link Bean#toValueMap()} */ public static Map toValueMap(Object o) { return toValueMap(o, false, false, false); } /** * fills a map with all bean-attribute-names and their values * * @param o bean * @param useClassPrefix if true, the class-name will be used as prefix for the key * @param onlySingleValues if true, collections will be ignored * @param onlyFilterAttributes if true, all other than filterAttributes will be ignored * @param filterAttributes attributes to be filtered (ignored, if onlyFilterAttributes) * @return see {@link Bean#toValueMap()} */ public static Map toValueMap(Object o, boolean useClassPrefix, boolean onlySingleValues, boolean onlyFilterAttributes, String... filterAttributes) { BeanDefinition beandef; // if (onlyFilterAttributes && filterAttributes.length > 0) { // //attributes will be changed - so we have to use an own instance // beandef = new BeanDefinition(BeanClass.getDefiningClass(o.getClass())); // beandef.setAttributeFilter(filterAttributes); // } else { beandef = BeanDefinition.getBeanDefinition(BeanClass.getDefiningClass(o.getClass())); // } return beandef.toValueMap(o, useClassPrefix, onlySingleValues, onlyFilterAttributes, filterAttributes); } /** * delegates to {@link #toValueMap(Object, String, boolean, boolean, String...)} with onlyFilteredAttributes = true. */ public static Map toValueMap(Object o, String keyPrefix, boolean onlySingleValues, String... filterAttributes) { return toValueMap(o, keyPrefix, onlySingleValues, true, filterAttributes); } /** * fills a map with all bean-attribute-names and their values * * @param o java instance * @param keyPrefix key prefix to be used for each attribute name. must not be null - use an empty string instead! * @param onlySingleValues if true, collections will be ignored * @param onlyFilteredAttributes if true, only the filterAttributes will be filled - otherwise, all others will be * filled. * @param filterAttributes attributes to be filtered * @return see {@link Bean#toValueMap()} */ public static Map toValueMap(Object o, String keyPrefix, boolean onlySingleValues, boolean onlyFilteredAttributes, String... filterAttributes) { BeanDefinition beandef = BeanDefinition.getBeanDefinition(BeanClass.getDefiningClass(o.getClass())); // if (onlyFilteredAttributes && filterAttributes.length > 0) { // //attributes will be changed - so we have to use an own instance // beandef = new BeanDefinition(BeanClass.getDefiningClass(o.getClass())); // beandef.setAttributeFilter(filterAttributes); // } else { // beandef = BeanDefinition.getBeanDefinition(BeanClass.getDefiningClass(o.getClass())); // } return beandef.toValueMap(o, keyPrefix, onlySingleValues, onlyFilteredAttributes, filterAttributes); } /** * delegates to {@link #toFormattedMap(Object, String, boolean)}. */ public static Map toFormattedMap(Object bean) { return toFormattedMap(bean, null, true, new DefaultFormat()); } /** * provides a map containing all formatted single value attributes of the given bean * * @param bean bean to evaluate * @param keyPrefix (optional) key name prefix (normally ending with a dot) * @param translateKeys if true, all keys will be translated * @param filterAttributes attributes to be filtered (ignored) * @param format formatter (use {@link DefaultFormat} if unknown) * @return map containing formatted values */ public static Map toFormattedMap(Object bean, String keyPrefix, boolean translateKeys, Format format, String... filterAttributes) { final Map valueMap = keyPrefix != null ? BeanUtil.toValueMap(bean, keyPrefix, true, filterAttributes) : BeanUtil.toValueMap(bean, true, true, false, filterAttributes); final Set keySet = valueMap.keySet(); final Map formattedMap = new LinkedHashMap(); for (final String k : keySet) { final String key = translateKeys ? Messages.getString(k) : k; formattedMap.put(key, format.format(valueMap.get(k))); } return formattedMap; } /** * convenience for {@link #fromFlatFile(Reader, Class, String...)}. */ public static Collection fromFlatFile(String fileName, Class rootType, String... attributeNames) { try { return fromFlatFile(new BufferedReader(new FileReader(new File(fileName))), rootType, null, attributeNames); } catch (final FileNotFoundException e) { ManagedException.forward(e); return null; } } /** * convenience for {@link #fromFlatFile(Reader, Class, String...)}. */ public static Collection fromFlatFile(String fileName, String separation, Class rootType, String... attributeNames) { try { return fromFlatFile(new BufferedReader(new FileReader(new File(fileName))), separation, rootType, null, attributeNames); } catch (final FileNotFoundException e) { ManagedException.forward(e); return null; } } /** * reads a flat (like csv) file and tries to put the values to the given bean type. the given bean type must have a * default constructor. * * @param * @param r normally a buffered reader. * @param rootType root bean type to be instantiated and filled to the result collection * @param attributeNames (optional) simple attribute names or point-separated relation expressions. use null to * ignore the token * @return filled collection of beans */ public static Collection fromFlatFile(Reader r, Class rootType, Map formats, String... attributeNames) { return fromFlatFile(r, null, Bean.newBean(rootType), formats, attributeNames); } /** * reads a flat (like csv) file and tries to put the values to the given bean type. the given bean type must have a * default constructor. * * @param * @param r normally a buffered reader. * @param separation separation character. if this is null, the attributeNames must contain at least one * column-index. * @param rootType root bean type to be instantiated and filled to the result collection * @param attributeNames (optional) simple attribute names or point-separated relation expressions. use null to * ignore the token * @return filled collection of beans */ public static Collection fromFlatFile(Reader r, String separation, Class rootType, Map formats, String... attributeNames) { return fromFlatFile(r, separation, Bean.newBean(rootType), formats, attributeNames); } /** * reads a flat file (like csv) and tries to put the values to the given bean type. the given bean type should have * a default constructor. the given bean holds an example instance of your root-type. it is possible to provide an * overridden bean, to implement the method {@link Bean#newInstance(Object...)} to initialize your desired instance. * a new instance will be created on each new line. *

* there are two possibilities to use this method:
* - with a field separator (like comma or semicolon)
* - with line-column definitions (like '1-10:myBeanAttributeName) *

* * with the first alternative, you give a separator (not null) and the pure attribute names of the given rootType * (included in your bean). it is possible to give attribute-relations like 'myAttr1.myAttr2.myAttr3'. to ignore * fields, use null as beanattribute-name. *

* * the second alternative needs all beanattribute names with a column-prefix like 'begin-end:attributename'. for * example: 1-11:date. it is possible to use bean relations as in the first alternative, too. *

* Please notice, that the column-indexes are one-based - the first column is 1 - and the end-index will not be * included like in String.substring(begin, end). e.g. to read '01.01.2001' you need 1-11. the indexes are * equivalent to standard texteditors like notepad++. * * @param root type * @param r normally a buffered reader. * @param separation separation character. if this is null, the attributeNames must contain at least one * column-index. * @param bean root bean type to be instantiated and filled to the result collection * @param attributeFormats (optional) some format-instances to parse to the right object. used if found, otherwise * standard formatters will be used. * @param attributeNames simple attribute names or point-separated relation expressions. use null to ignore the * token * @return filled collection of beans */ public static Collection fromFlatFile(Reader r, String separation, Bean bean, Map attributeFormats, String... attributeNames) { /* * do some validation checks */ if (attributeNames.length == 0) { throw ManagedException.implementationError("give at least one attribute-name to be filled!", null); } if (separation == null) { boolean hasColumnIndexes = false; for (String n : attributeNames) { if (n != null && n.contains(":")) { hasColumnIndexes = true; break; } } if (!hasColumnIndexes) { throw ManagedException .implementationError( "if you don't give a separation-character, you should give at least one column-index in your attribute-names", null); } } final Collection result = new LinkedList(); final StreamTokenizer st = new StreamTokenizer(r); /* * to remove parsing of numbers by the tokenizer we have to reset all! * ugly jdk implementation - perhaps we should use Scanner. */ st.resetSyntax(); st.wordChars(0x00, 0xFF); // st.quoteChar('\"'); st.whitespaceChars('\r', '\r'); st.whitespaceChars('\n', '\n'); st.eolIsSignificant(true); st.commentChar('#'); // st.slashSlashComments(true); // st.slashStarComments(true); int ttype = 0; final Class rootType = bean.getClazz(); // bean.newInstance(); final Map errors = new Hashtable(); final String rootInfo = rootType.getSimpleName() + "."; /* * prepare the format cache to parse strings with performance */ final Map formatCache = new HashMap(); if (attributeFormats != null) { formatCache.putAll(attributeFormats); } /* * prepared fixed columns */ int begin, end; String attrName; final Map attributeColumns = new LinkedHashMap(attributeNames.length); for (int i = 0; i < attributeNames.length; i++) { if (separation != null || attributeNames[i] == null) { attributeColumns.put((attributeNames[i] != null ? attributeNames[i] : "null:" + String.valueOf(i)), null); } else { begin = Integer.valueOf(StringUtil.substring(attributeNames[i], null, "-")); end = Integer.valueOf(StringUtil.substring(attributeNames[i], "-", ":")); if (end <= begin || begin < 0 || end < 1) { throw new IllegalArgumentException("The given range " + attributeNames[i] + " is illegal!"); } attrName = StringUtil.substring(attributeNames[i], ":", null); //store one-based indexes attributeColumns.put(attrName, new Point(begin - 1, end - 1)); } } final Set cols = attributeColumns.keySet(); boolean filled = false; /* * do the reading, collecting all errors to throw only one exception at the end */ try { String t; while ((ttype = st.nextToken()) != StreamTokenizer.TT_EOF) { if (ttype != StreamTokenizer.TT_EOL && st.sval.trim().length() > 0) { bean.newInstance(); int lastSep = 0; for (final String attr : cols) { final Point c = attributeColumns.get(attr); if (c != null) { if (c.x >= st.sval.length() || c.y > st.sval.length()) { throw new StringIndexOutOfBoundsException("The range " + c.x + "-" + c.y + " is not available on line " + st.lineno() + " with length " + st.sval.length() + ":" + st.sval); } t = st.sval.substring(c.x, c.y); } else { t = StringUtil.substring(st.sval, null, separation, lastSep); } lastSep += t.length() + (c != null ? 0 : separation.length()); //at line end, no separation char will occur if (st.sval.length() < lastSep) { lastSep = st.sval.length(); } if (attr == null || attr.startsWith("null:")) { LOG.info("ignoring line " + st.lineno() + ", token '" + t + "' at column " + (lastSep - t.length())); continue; } t = StringUtil.trim(t, "\""); final String info = "reading line " + st.lineno() + ":'" + t + "' into " + rootInfo + attr; try { Object newValue = null; if (!Util.isEmpty(t)) { final BeanAttribute beanAttribute = BeanAttribute.getBeanAttribute(rootType, attr); Format parser = formatCache.get(attr); if (parser == null) { parser = FormatUtil.getDefaultFormat(beanAttribute.getType(), true); formatCache.put(attr, parser); } newValue = parser.parseObject(t); bean.setValue(beanAttribute.getName(), newValue); } LOG.info(info + "(" + newValue + ")"); filled = true; } catch (final Exception e) { LOG.info("problem on " + info); LOG.error(e.toString()); errors.put(info, e); } } if (filled) { result.add(bean.getInstance()); } } } } catch (final Exception e) { ManagedException.forward(e); } if (errors.size() > 0) { throw new ManagedException(StringUtil.toFormattedString(errors, 80, true)); } LOG.info("import finished - imported items: " + result.size() + " of type " + rootType.getSimpleName()); return result; } /** * delegates to {@link #present(BeanCollector, String, String, String, String, String, String, String, String)}. */ public static String presentAsCSV(BeanCollector collector) { return present(collector, "", "", "", "\n", "", ",", null, null); } /** * delegates to {@link #present(BeanCollector, String, String, String, String, String, String, String, String)}. */ public static String presentAsTabSheet(BeanCollector collector) { return present(collector, "", "", "", "\n", "", "\t", null, null); } /** * creates a simple html-table as presentation for the given collector. * * @param collector * @return see {@link #present(BeanCollector, String, String, String, String, String, String, String, String)} */ public static String presentAsHtmlTable(BeanCollector collector) { return present(collector, "

\n", "
", "", "\n", "", "\"", "", ":
\""); } /** * creates a string representing all items with all attributes of the given beancollector (holding a collection of * items). *

* All parameters without nameBegin and nameEnd must not be null! * * @param collector holding a list - defining the attribute presentation. * @param header text header * @param footer text footer * @param rowBegin text on a new line * @param rowEnd text on line end * @param colBegin text on new column * @param colEnd text on column end * @param nameBegin (optional) if not null, starting text of a fields name. if null, no field name will be presented * @param nameEnd (optional) if not null, ending text of a fields name. if null, no field name will be presented * @return string presentation of given collector */ public static String present(BeanCollector collector, String header, String footer, String rowBegin, String rowEnd, String colBegin, String colEnd, String nameBegin, String nameEnd) { Collection c = collector.getCurrentData(); List attributes = collector.getBeanAttributes(); StringBuilder buf = new StringBuilder(c.size() * attributes.size() * 30 + 100); buf.append(header); for (Object o : c) { buf.append(rowBegin); for (IAttributeDefinition a : attributes) { buf.append(colBegin + (nameBegin != null && nameEnd != null ? nameBegin + a.getName() + nameEnd : "") + collector.getColumnText(o, a) + colEnd); } buf.append(rowEnd); } buf.append(footer); return buf.toString(); } /** * simple delegation to {@link #valueOf(Object, Object)}. */ public static final T defaultValue(T value, T defaultIfNull) { return valueOf(value, defaultIfNull); } /** * returns the value itself - or if null the defaultIfNull * * @param * @param value value or null * @param defaultIfNull default value if value is null * @return value of defaultIfNull */ public static final T valueOf(T value, T defaultIfNull) { return value != null ? value : defaultIfNull; } /** * createUUID (see {@link UUID#randomUUID()} * * @return random uuid string (128-bit value) */ public static final String createUUID() { return UUID.randomUUID().toString(); } /** * creates a hashcode through all single-value attibutes of given bean instance * * @param bean instance to evaluate the hashcode for * @param attributes (optional) attributes to be used for hashcode * @return new hashcode for given bean instance */ public static int hashCodeReflect(Object bean, String... attributes) { final int prime = 31; int result = 1; BeanClass bc = BeanClass.getBeanClass(bean.getClass()); if (attributes == null) { attributes = bc.getAttributeNames(); } Object v; for (int i = 0; i < attributes.length; i++) { v = BeanClass.getValue(bean, attributes[i]); result = prime * result + (v == null ? 0 : v.hashCode()); } return result; } /** * if not already a bean or beancollector, the given object will wrapped into a bean or beancollector. if it is not * serializable, a map of values will be packed into a beancollector. * * @param obj * @return */ public static BeanDefinition getBean(Object obj) { return (BeanDefinition) (obj instanceof BeanDefinition ? obj : (Util.isContainer(obj) ? BeanCollector.getBeanCollector(CollectionUtil.getContainer(obj), 0) : obj instanceof Serializable ? Bean.getBean((Serializable) obj) : BeanCollector.getBeanCollector(CollectionUtil.getContainer(BeanUtil.toValueMap(obj)), 0))); } public static Collection asNamedCollection(Map m) { LinkedList list = new LinkedList(); NamedValue.putAll(m, list); return list; } /** * delegates to {@link #getParser(Class, String, String, String, IConverter, boolean)} using @id attribute and * cache. */ public static RegExpFormat getParser(final Class type, String pattern, final IConverter converter) { //workaround to have a simple instance for calling getIdAttribute(). poor performance - but works TYPE instance = BeanClass.createInstance(type); return RegExpFormat.getParser(type, BeanContainer.getIdAttribute(instance).getName(), pattern, null, converter, true); } } /** * To avoid using package awt, we can't use java.awt.Point - but we need a simple Point. */ class Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }