jfxtras.icalendarfx.properties.VPropertyBase Maven / Gradle / Ivy
package jfxtras.icalendarfx.properties;
import java.lang.reflect.Method;
import java.time.DateTimeException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import jfxtras.icalendarfx.VElement;
import jfxtras.icalendarfx.VParent;
import jfxtras.icalendarfx.VParentBase;
import jfxtras.icalendarfx.content.SingleLineContent;
import jfxtras.icalendarfx.parameters.NonStandardParameter;
import jfxtras.icalendarfx.parameters.VParameter;
import jfxtras.icalendarfx.parameters.ValueParameter;
import jfxtras.icalendarfx.properties.calendar.CalendarScale;
import jfxtras.icalendarfx.properties.calendar.ProductIdentifier;
import jfxtras.icalendarfx.properties.calendar.Version;
import jfxtras.icalendarfx.properties.component.misc.NonStandardProperty;
import jfxtras.icalendarfx.properties.component.relationship.UniqueIdentifier;
import jfxtras.icalendarfx.utilities.ICalendarUtilities;
import jfxtras.icalendarfx.utilities.StringConverter;
/**
* Base iCalendar property class
* Contains property value, value parameter (ValueType) and other-parameters
* Also contains several support methods used by other properties
*
* concrete subclasses
* @see UniqueIdentifier
* @see CalendarScale
* @see Method
* @see ProductIdentifier
* @see Version
*
* @author David Bal
*
* @param - type of implementing subclass
* @param - type of property value
*/
public abstract class VPropertyBase extends VParentBase implements VProperty
{
private VParent myParent;
@Override
public void setParent(VParent parent)
{
myParent = parent;
}
@Override
public VParent getParent()
{
return myParent;
}
/**
* PROPERTY VALUE
*
* Example: for the property content LOCATION:The park the property
* value is the string "The park"
*/
@Override
public T getValue()
{
return value;
}
private T value; // initialized in constructor
@Override
public void setValue(T value)
{
this.value = value;
}
public U withValue(T value)
{
setValue(value);
return (U) this;
} // in constructor
/** The propery's value converted by string converted to content string */
protected String valueContent()
{
/* default code below works for all properties with a single value. Properties with multiple embedded values,
* such as RequestStatus, require an overridden method */
String value = null;
if (getValue() == null)
{
value = getUnknownValue();
} else if (getConverter().toString(getValue()) == null)
{
value = getUnknownValue();
} else
{
value = getConverter().toString(getValue());
}
return (value == null) ? "" : value;
}
// class of value. If collection, returns type of element instead.
// Used to verify value class is allowed for the property type
private Class valueClass;
private Class> getValueClass()
{
if (valueClass == null)
{
if (getValue() != null)
{
if (getValue() instanceof Collection)
{
if (! ((Collection>) getValue()).isEmpty())
{
return ((Collection>) getValue()).iterator().next().getClass();
} else
{
return null;
}
}
return getValue().getClass();
}
return null;
} else
{
return valueClass;
}
}
/** The name of the property, such as DESCRIPTION
* Remains the default value unless set by a non-standard property*/
@Override
public String name()
{
if (propertyName == null)
{
return propertyType.toString();
}
return propertyName;
}
protected String propertyName;
// /**
// * PROPERTY TYPE
// *
// * The enumerated type of the property.
// * Some essential methods are in the enumerated type.
// */
// public PropertyType propertyType()
// {
// return propertyType;
// }
final private VPropertyElement propertyType;
/*
* Unknown values
* contains exact string for unknown property value
*/
private String unknownValue;
protected String getUnknownValue()
{
return unknownValue;
}
private void setUnknownValue(String value)
{
unknownValue = value;
}
/**
* VALUE TYPE
* Value Data Types
* RFC 5545, 3.2.20, page 29
*
* To specify the value for text values in a property or property parameter.
* This parameter is optional for properties when the default value type is used.
*
* Examples:
* VALUE=DATE-TIME (Date-Time is default value, so it isn't necessary to specify)
* VALUE=DATE
*/
final protected ValueType defaultValueType;
final protected Collection allowedValueTypes;
@Override
public ValueParameter getValueType() { return valueType; }
private ValueParameter valueType;
@Override
public void setValueType(ValueParameter valueType)
{
if (valueType == null || isValueTypeValid(valueType.getValue()))
{
orderChild(this.valueType, valueType);
this.valueType = valueType;
valueParamenterConverter(valueType); // convert new value
} else
{
throw new IllegalArgumentException("Invalid Value Date Type:" + valueType.getValue() + ", allowed = " + allowedValueTypes);
}
}
public void setValueType(ValueType value)
{
setValueType(new ValueParameter(value));
}
public U withValueType(ValueType value)
{
setValueType(value);
return (U) this;
}
// Synch value with type produced by string converter
private void valueParamenterConverter(ValueParameter newValueParameter)
{
if (! isCustomConverter())
{
// Convert property value string, if present
if (modifiedValue() != null)
{
String modifiedValue = modifiedValue();
T newPropValue = getConverter().fromString(modifiedValue);
this.value = newPropValue;
}
}
// verify value class is allowed
if (newValueParameter != null && getValueClass() != null) // && ! newValue.getValue().allowedClasses().contains(getValueClass()))
{
boolean isMatch = newValueParameter.getValue().allowedClasses()
.stream()
.map(c -> getValueClass().isAssignableFrom(c))
.findAny()
.isPresent();
if (! isMatch)
{
throw new IllegalArgumentException("Value class " + getValueClass().getSimpleName() +
" doesn't match allowed value classes: " + newValueParameter.getValue().allowedClasses());
}
}
}
/**
* NON-STANDARD PARAMETERS
*
* x-param = x-name "=" param-value *("," param-value)
; A non-standard, experimental parameter.
*/
private List nonStandardParams;
@Override
public List getNonStandard()
{
return nonStandardParams;
}
@Override
public void setNonStandard(List nonStandardParams)
{
if (this.nonStandardParams != null)
{
this.nonStandardParams.forEach(e -> orderChild(e, null)); // remove old elements
}
this.nonStandardParams = nonStandardParams;
if (nonStandardParams != null)
{
nonStandardParams.forEach(e -> orderChild(e));
}
}
/**
* Sets the value of the {@link #NonStandardParameter()}
*
* @return - this class for chaining
*/
public U withNonStandard(List nonStandardParams)
{
if (getNonStandard() == null)
{
setNonStandard(new ArrayList<>());
}
getNonStandard().addAll(nonStandardParams);
if (nonStandardParams != null)
{
nonStandardParams.forEach(c -> orderChild(c));
}
return (U) this;
}
/**
* NON-STANDARD PARAMETERS
*
* Sets the value of the {@link #NonStandardParameter()} by parsing a vararg of
* iCalendar content text representing individual {@link NonStandardParameter} objects.
*
* @return - this class for chaining
*/
public U withNonStandard(String...nonStandardParams)
{
List list = Arrays.stream(nonStandardParams)
.map(c -> NonStandardParameter.parse(c))
.collect(Collectors.toList());
return withNonStandard(list);
}
/**
* Sets the value of the {@link #NonStandardParameter()} from a vararg of {@link NonStandardParameter} objects.
*
* @return - this class for chaining
*/
public U withNonStandard(NonStandardParameter...nonStandardParams)
{
return withNonStandard(Arrays.asList(nonStandardParams));
}
// property value as string - kept if string converter changes the value can change
// needed to make subsequent conversions if value type changes.
protected String actualValueContent = null;
// Note: in subclasses additional text can be concatenated to string (e.g. ZonedDateTime classes add time zone as prefix)
protected String modifiedValue()
{
if (actualValueContent == null)
{
T value2 = getValue();
if (value2 != null)
{
return getConverter().toString(getValue());
}
return null;
} else
{
return actualValueContent;
}
}
/**
* STRING CONVERTER
*
* Get the property's value string converter. There is a default converter in ValueType associated
* with the default value type of the property. For most value types that converter is
* acceptable. However, for the TEXT value type it often needs to be replaced.
* For example, the value type for TimeZoneIdentifier is TEXT, but the Java object is
* ZoneId. A different converter is required to make the conversion to ZoneId.
*/
protected StringConverter getConverter()
{
if (converter == null)
{
ValueType valueType = (getValueType() == null) ? defaultValueType : getValueType().getValue();
return valueType.getConverter(); // use default converter assigned to value type if no customer converter assigned
}
return converter;
}
private StringConverter converter;
protected void setConverter(StringConverter converter) { this.converter = converter; }
private boolean isCustomConverter()
{
return converter != null;
}
/*
* CONSTRUCTORS
*/
// protected VPropertyBase(Collection allowedValueTypes, ValueType defaultValueType)
protected VPropertyBase()
{
super();
propertyType = VPropertyElement.fromClass(getClass());
this.allowedValueTypes = propertyType.allowedValueTypes();
this.defaultValueType = propertyType.defaultValueType();
// this.allowedValueTypes = VPropertyElement.propertyAllowedValueTypes(getClass());
// this.defaultValueType = VPropertyElement.defaultValueType(getClass());
// this.defaultValueType = defaultValueType;
// propertyType = PropertyType.enumFromClass(getClass());
// if (propertyType != PropertyType.NON_STANDARD)
// {
// setPropertyName(propertyType.toString());
// }
contentLineGenerator = new SingleLineContent(
orderer,
(Void) -> name(),
50);
}
// Used by Attachment
public VPropertyBase(Class valueClass, String contentLine)
{
this();
this.valueClass = valueClass;
setConverterByClass(valueClass);
parseContent(contentLine);
}
// copy constructor
public VPropertyBase(VPropertyBase source)
{
this();
setConverter(source.getConverter());
T valueCopy = copyValue(source.getValue());
setValue(valueCopy);
source.copyChildrenInto(this);
setParent(source.getParent());
}
// constructor with only value
public VPropertyBase(T value)
{
this();
setValue(value);
}
// return a copy of the value
protected T copyValue(T source)
{
return source; // for mutable values override in subclasses
}
// Set converter when using constructor with class parameter
protected void setConverterByClass(Class valueClass)
{
// do nothing - hook to override in subclass for functionality
}
/**
* Handle non-standard property name
*/
@Override
protected List parseContent(String unfoldedContent)
{
List messages = new ArrayList<>();
String propertyName = elementName(unfoldedContent);
boolean isNameless = propertyName == null;
if (isNameless)
{
unfoldedContent = ICalendarUtilities.PROPERTY_VALUE_KEY + unfoldedContent; // add delimiter to designate content as value
} else if (propertyName.startsWith(VPropertyElement.NON_STANDARD_PROPERTY.toString()))
{
((NonStandardProperty) this).setPropertyName(propertyName);
}
ICalendarUtilities.parseInlineElementsToListPair(unfoldedContent)
.stream()
// .peek(System.out::println)
.forEach(entry -> processInLineChild(messages, entry.getKey(), entry.getValue(), VParameter.class));
return messages;
}
@Override
protected void processInLineChild(
List messages,
String childName,
String content,
Class extends VElement> singleLineChildClass)
{
if (childName == ICalendarUtilities.PROPERTY_VALUE_KEY)
{
if (content != null)
{
try {
actualValueContent = content;
T value = getConverter().fromString(modifiedValue());
if (value == null)
{
setUnknownValue(content);
} else
{
setValue(value);
if (value.toString() == "UNKNOWN") // enum name indicating unknown value
{
setUnknownValue(content);
}
}
} catch (IllegalArgumentException | DateTimeException e)
{
Message message = new Message(this,
"Invalid element:" + e.getMessage() + modifiedValue(),
MessageEffect.MESSAGE_ONLY);
messages.add(message);
}
}
} else
{
super.processInLineChild(messages, childName, content, singleLineChildClass);
}
}
@Override
public List errors()
{
List errors = super.errors();
if (getValue() == null)
{
// errors.add(name() + " value is null. The property MUST have a value."); // Google uses empty properties
}
final ValueType valueType;
if (getValueType() != null)
{
valueType = getValueType().getValue();
boolean isValueTypeOK = isValueTypeValid(valueType);
if (! isValueTypeOK)
{
errors.add(name() + " value type " + getValueType().getValue() + " is not supported. Supported types include:" +
allowedValueTypes.stream().map(v -> v.toString()).collect(Collectors.joining(",")));
}
} else
{
// use default valueType
valueType = defaultValueType;
}
List valueTypeErrorList = valueType.createErrorList(getValue());
if (valueTypeErrorList != null)
{
errors.addAll(valueTypeErrorList);
}
return errors;
}
/* test if value type is valid */
private boolean isValueTypeValid(ValueType value)
{
boolean isValueTypeOK = allowedValueTypes.contains(value);
boolean isUnknownType = value.equals(ValueType.UNKNOWN);
// boolean isNonStandardProperty = propertyType().equals(PropertyType.NON_STANDARD);
return (isValueTypeOK || isUnknownType);
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder(super.toString());
builder.append(":" + valueContent());
// return folded line
return ICalendarUtilities.foldLine(builder).toString();
}
@Override // Note: can't check equality of parents - causes stack overflow
public boolean equals(Object obj)
{
boolean childrenEquals = super.equals(obj);
if (! childrenEquals) return false;
VPropertyBase,?> testObj = (VPropertyBase,?>) obj;
boolean valueEquals = (getValue() == null) ? testObj.getValue() == null : getValue().equals(testObj.getValue());
if (! valueEquals) return false;
boolean nameEquals = name().equals(testObj.name());
return nameEquals;
}
@Override // Note: can't check hashCode of parents - causes stack overflow
public int hashCode()
{
int hash = super.hashCode();
final int prime = 31;
hash = prime * hash + ((converter == null) ? 0 : converter.hashCode());
hash = prime * hash + ((propertyName == null) ? 0 : propertyName.hashCode());
hash = prime * hash + ((actualValueContent == null) ? 0 : actualValueContent.hashCode());
hash = prime * hash + ((unknownValue == null) ? 0 : unknownValue.hashCode());
hash = prime * hash + ((value == null) ? 0 : value.hashCode());
return hash;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy