blob: 22d9817c510fc97e35e35ca15632416084269e44 [file] [log] [blame]
/**
*
* Copyright (c) 2011, 2016 - Loetz GmbH&Co.KG (69115 Heidelberg, Germany)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Florian Pirchner - Initial implementation
*
*/
package org.eclipse.osbp.ecview.extension.presentation.vaadin.converter;
import java.lang.reflect.InvocationTargetException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Collections;
import java.util.Locale;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.eclipse.core.databinding.beans.BeanProperties;
import org.eclipse.core.databinding.beans.IBeanValueProperty;
import org.eclipse.osbp.ecview.core.common.context.IViewContext;
import org.eclipse.osbp.ecview.extension.model.converter.YQuantityToStringConverter;
import org.eclipse.osbp.runtime.common.types.ITypeProviderService;
import org.eclipse.osbp.runtime.web.vaadin.common.data.IBeanSearchService;
import org.eclipse.osbp.runtime.web.vaadin.common.data.IBeanSearchServiceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vaadin.data.Container.Filter;
import com.vaadin.data.util.converter.Converter;
import com.vaadin.data.util.filter.Compare;
/**
* This converter converters values from a "quantity object" (model) to a String
* (presentation) and back to a quantity object.
* <p>
* Therefore it uses following logic:<br>
* The <code>model value</code> is an object containing the value as a numeric
* value and the uom (UnitOfMeasure). Value and uom may be nested properties.
* <h3>Access path</h3>
* The YQuantityToStringConverter contains three "access path". An "access path"
* is a property path with dot-notation like "uom.isoCode". To access the values
* inside the given object, reflection needs to be used.
* <p>
* Sample with mappings to the YQuantityToStringConverter:
*
* <pre>
* /**
* /**
* /**
* }
*
* class Uom {
* /**
* String name;
* ...
* }
* </pre>
*
* The Quantity bean is the model value. To access the values following
* accessPath in the YQuantityToStringConverter needs to be used:<br>
* -> for value: <code>value</code> --> converter#amountPropertyPath<br>
* -> for uom-Object: <code>uom</code> --> converter#uomPropertyPath<br>
* -> for uomCode: <code>uom.iso3Code</code> --> converter#uomPropertyPath + "."
* + converter#uomCodeRelativePropertyPath<br>
* By reflection the values can become accessed.
*
* <h3>Convert To Presentation</h3> For the convert to presentation
* implementation, a number format pattern is used. The uom-String-value is
* added to the number format pattern as a constant. <br>
* <br>
* The result may look like: <code>1.200,23 kg</code>
*
* <h3>Convert To Model</h3> For the convert to model implementation, the given
* presentation-String needs to become parsed. And a new Object of type
* "model value" needs to be created. The type of the instance, is also
* contained in the YQuantityToStringConverter object. See
* {@link YQuantityToStringConverter#getQuantityTypeQualifiedName()}. To get a
* class based on the FQN use the {@link ITypeProviderService}.<br>
* The UOM needs to be converted to a valid UOM-object. Therefore use the
* {@link IBeanSearchService}.
*
* <h3>Unsupported UOMs</h3> To ensure, that only valid UOMs are being entered
* in the UI, an additional Validator needs to be implemented. It validates,
* that the entered UOM is valid. To query about valid UOMs, the
* BeanSearchService is available. See {@link IBeanSearchService}.
*
* <h3>Usage</h3> You may use this converter, to bind a Quantity-Dto to a
* TextField.
*
*/
@SuppressWarnings("serial")
public class QuantityToStringConverter implements Converter<String, Object> {
/** The Constant LOGGER. */
private static final Logger LOGGER = LoggerFactory
.getLogger(QuantityToStringConverter.class);
/** The Constant NUMBER_FORMAT_PATTERN. */
private static final String NUMBER_FORMAT_PATTERN = "##,##0.00 {$uom}";
/** The cx converter. */
private final YQuantityToStringConverter cxConverter;
/** The bean search service. */
private IBeanSearchService<Object> beanSearchService;
/** The quantity type. */
private final Class<?> quantityType;
/**
* Instantiates a new quantity to string converter.
*
* @param context
* the context
* @param cxConverter
* the cx converter
*/
@SuppressWarnings("unchecked")
public QuantityToStringConverter(IViewContext context,
YQuantityToStringConverter cxConverter) {
super();
this.cxConverter = cxConverter;
ITypeProviderService typeService = context
.getService(ITypeProviderService.class.getName());
quantityType = typeService.forName(null,
cxConverter.getQuantityTypeQualifiedName());
// use the BeanProperties to find the UnitOfMeasure by its code
IBeanValueProperty prop = BeanProperties.value(quantityType,
cxConverter.getUomPropertyPath());
IBeanSearchServiceFactory factory = context
.getService(IBeanSearchServiceFactory.class.getName());
if (factory != null) {
beanSearchService = factory.createService((Class<Object>) prop
.getValueType());
}
}
/*
* (non-Javadoc)
*
* @see
* com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
* java.lang.Class, java.util.Locale)
*/
@Override
public Object convertToModel(String valueString,
Class<? extends Object> targetType, Locale locale)
throws com.vaadin.data.util.converter.Converter.ConversionException {
try {
// parse amount and uom from the given user input
Number amount = parseAmount(valueString);
Object uom = parseUom(valueString);
// set amount and uom to the new quantity object
Object quantity = quantityType.newInstance();
applyAmount(quantity, amount);
applyUom(quantity, uom);
return quantity;
} catch (InstantiationException | IllegalAccessException
| InvocationTargetException | ParseException
| NoSuchMethodException e) {
LOGGER.error("{}", e);
return null;
}
}
/**
* Sets the uom into the quantity object.
*
* @param quantity
* the quantity
* @param uom
* the uom
* @throws IllegalAccessException
* the illegal access exception
* @throws InvocationTargetException
* the invocation target exception
* @throws NoSuchMethodException
* the no such method exception
*/
private void applyUom(Object quantity, Object uom)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
PropertyUtils.setNestedProperty(quantity,
cxConverter.getUomPropertyPath(), uom);
}
/**
* Sets the amount into the quantity object.
*
* @param quantity
* the quantity
* @param amount
* the amount
* @throws IllegalAccessException
* the illegal access exception
* @throws InvocationTargetException
* the invocation target exception
* @throws NoSuchMethodException
* the no such method exception
*/
private void applyAmount(Object quantity, Number amount)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
PropertyUtils.setNestedProperty(quantity,
cxConverter.getAmountPropertyPath(), amount);
}
/**
* Parses the amount part from the given value String.<br>
* "1200.98 kg" -> "1200.98"
*
* @param valueString
* the value string
* @return the number
* @throws ParseException
* the parse exception
*/
private Number parseAmount(String valueString) throws ParseException {
// TODO DOM - enhance with a better split criteria
String[] tokens = valueString.trim().split(" ");
String amountString = tokens.length == 2 ? tokens[0] : "";
return new DecimalFormat().parse(amountString);
}
/**
* Parses the uom part from the given value String and creates a UOM Object.<br>
* "1200.98 kg" -> "kg" -> Object{kg}
*
* @param valueString
* the value string
* @return the object
*/
private Object parseUom(String valueString) {
// TODO DOM - enhance with a better split criteria
String[] tokens = valueString.trim().split(" ");
String uomCode = tokens.length == 2 ? tokens[1] : "";
return getUOMForCode(uomCode);
}
/**
* Returns the unit of measure for the given uomCode or <code>null</code> if
* no uom could be found.
*
* @param uomCode
* the uom code
* @return the UOM for code
*/
private Object getUOMForCode(String uomCode) {
// create a filter based on the relative property path that should be
// used to show the value
Filter filter = new Compare.Equal(
cxConverter.getUomCodeRelativePropertyPath(), uomCode);
return beanSearchService.getFirstBean(
Collections.singletonList(filter), null);
}
/*
* (non-Javadoc)
*
* @see
* com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
* .Object, java.lang.Class, java.util.Locale)
*/
@Override
public String convertToPresentation(Object value,
Class<? extends String> targetType, Locale locale)
throws com.vaadin.data.util.converter.Converter.ConversionException {
try {
String pattern = NUMBER_FORMAT_PATTERN;
pattern = pattern.replace("{$uom}", getUomCode(value));
try {
DecimalFormat format = new DecimalFormat(StringEscapeUtils.unescapeHtml(pattern), new DecimalFormatSymbols(locale));
return format.format(getAmount(value));
} catch (IllegalArgumentException e) {
String msg = String.format(
"formatter %s is invalid for decimal numbers: %s",
pattern, e.getLocalizedMessage());
throw new ConversionException(msg);
}
} catch (IllegalAccessException | InvocationTargetException
| NoSuchMethodException e) {
LOGGER.error("{}", e);
return "Error";
}
}
/**
* Returns the amount value based on the AmountPropertyPath.
*
* @param value
* the value
* @return the amount
* @throws IllegalAccessException
* the illegal access exception
* @throws InvocationTargetException
* the invocation target exception
* @throws NoSuchMethodException
* the no such method exception
*/
private Object getAmount(Object value) throws IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
return PropertyUtils.getNestedProperty(value,
cxConverter.getAmountPropertyPath());
}
/**
* Returns the amount value based on the UomPropertyPath.
*
* @param value
* the value
* @return the uom code
* @throws IllegalAccessException
* the illegal access exception
* @throws InvocationTargetException
* the invocation target exception
* @throws NoSuchMethodException
* the no such method exception
*/
private String getUomCode(Object value) throws IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
// concat the uomPath and the uomCodePath to directly access the uom
// code.
return (String) PropertyUtils.getNestedProperty(
value,
cxConverter.getUomPropertyPath() + "."
+ cxConverter.getUomCodeRelativePropertyPath());
}
/*
* (non-Javadoc)
*
* @see com.vaadin.data.util.converter.Converter#getModelType()
*/
@Override
public Class<Object> getModelType() {
return Object.class;
}
/*
* (non-Javadoc)
*
* @see com.vaadin.data.util.converter.Converter#getPresentationType()
*/
@Override
public Class<String> getPresentationType() {
return String.class;
}
}