/** * */ package org.gcube.accounting.datamodel; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import org.gcube.accounting.datamodel.decorators.AggregatedField; import org.gcube.accounting.datamodel.decorators.ComputedField; import org.gcube.accounting.datamodel.decorators.FieldAction; import org.gcube.accounting.datamodel.decorators.FieldDecorator; import org.gcube.accounting.datamodel.decorators.RequiredField; import org.gcube.accounting.datamodel.validations.annotations.NotEmpty; import org.gcube.accounting.datamodel.validations.annotations.ValidLong; import org.gcube.accounting.datamodel.validations.annotations.ValidOperationResult; import org.gcube.common.scope.api.ScopeProvider; import org.gcube.documentstore.exception.InvalidValueException; import org.gcube.documentstore.records.AggregatedRecord; import org.gcube.documentstore.records.Record; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Luca Frosini (ISTI - CNR) http://www.lucafrosini.com/ * */ public abstract class BasicUsageRecord implements UsageRecord { /** * Generated Serial Version UID */ private static final long serialVersionUID = -2060728578456796388L; private static Logger logger = LoggerFactory.getLogger(BasicUsageRecord.class); @RequiredField @NotEmpty private static final String ID = Record.ID; @RequiredField @NotEmpty private static final String CONSUMER_ID = UsageRecord.CONSUMER_ID; @RequiredField @ValidLong private static final String CREATION_TIME = Record.CREATION_TIME; @RequiredField @NotEmpty private static final String RECORD_TYPE = Record.RECORD_TYPE; // TODO MOVE TO RECORD_TYPE @Deprecated @RequiredField @NotEmpty public static final String USAGE_RECORD_TYPE = "usageRecordType"; @RequiredField @NotEmpty private static final String SCOPE = UsageRecord.SCOPE; @RequiredField @ValidOperationResult private static final String OPERATION_RESULT = UsageRecord.OPERATION_RESULT; /** resource-specific properties */ protected Map> resourceProperties; protected Map> validation; protected Set requiredFields; /** * {@inheritDoc} */ @Override public Set getRequiredFields() { return requiredFields; } protected Set computedFields; private static final String START_TIME = AggregatedRecord.START_TIME; private static final String END_TIME = AggregatedRecord.END_TIME; private static final String OPERATION_COUNT = AggregatedUsageRecord.OPERATION_COUNT; protected Set aggregatedFields; protected static Set getAllFields(Class type) { Set fields = new HashSet(); for (Class c = type; c != null; c = c.getSuperclass()) fields.addAll(Arrays.asList(c.getDeclaredFields())); return fields; } protected void initializeValidation() { //logger.trace("Initializing Field Validators"); Set fields = getAllFields(this.getClass()); for(Field field : fields){ boolean defaultAccessibility = field.isAccessible(); field.setAccessible(true); String keyString; try { keyString = (String) field.get(null); } catch (Exception e) { continue; } List fieldValidators = new ArrayList(); validation.put(keyString, fieldValidators); for (Annotation annotation : field.getAnnotations()){ if (annotation.annotationType().isAnnotationPresent(FieldDecorator.class)){ Class managedClass = ((FieldDecorator)annotation.annotationType().getAnnotation(FieldDecorator.class)).managed(); FieldAction validator; try { validator = managedClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { logger.error("{} {}", keyString, annotation, e); continue; } fieldValidators.add(validator); } if(annotation.annotationType().isAssignableFrom(RequiredField.class)){ requiredFields.add(keyString); } if(annotation.annotationType().isAssignableFrom(AggregatedField.class)){ aggregatedFields.add(keyString); } if(annotation.annotationType().isAssignableFrom(ComputedField.class)){ computedFields.add(keyString); } } field.setAccessible(defaultAccessibility); } /* logger.trace("Required Fields {}", requiredFields); logger.trace("Aggregated Fields {}", aggregatedFields); logger.trace("Computed Fields {}", computedFields); */ } protected void cleanExtraFields(){ Set neededFields = this.requiredFields; neededFields.addAll(this.aggregatedFields); Set keysToRemove = new HashSet(); Set propertyKeys = this.resourceProperties.keySet(); for(String propertyName : propertyKeys){ if(!neededFields.contains(propertyName)){ keysToRemove.add(propertyName); } } for(String keyToRemove : keysToRemove){ this.resourceProperties.remove(keyToRemove); } } /** * Initialize variable */ private void init() { this.validation = new HashMap>(); this.requiredFields = new HashSet(); this.aggregatedFields = new HashSet(); this.computedFields = new HashSet(); this.resourceProperties = new HashMap>(); initializeValidation(); try { this.setScope(ScopeProvider.instance.get()); } catch(Exception e) { logger.warn("Unable to automaticcally set the scope using scope provider. The record will not be valid if the scope will not be explicitly set."); } } public BasicUsageRecord(){ init(); this.resourceProperties.put(ID, UUID.randomUUID().toString()); this.setRecordType(); Calendar calendar = Calendar.getInstance(); this.resourceProperties.put(CREATION_TIME, calendar.getTimeInMillis()); } public BasicUsageRecord(Map> properties) throws InvalidValueException { init(); setResourceProperties(properties); if(this instanceof AggregatedUsageRecord){ this.resourceProperties.put(AggregatedUsageRecord.AGGREGATED, true); cleanExtraFields(); } } @Deprecated public String getUsageRecordType() { return getRecordType(); } @Override public String getRecordType() { return (String) this.resourceProperties.get(RECORD_TYPE); } protected abstract String giveMeUsageRecordType(); @Deprecated protected void setUsageRecordType(){ setRecordType(); } protected void setRecordType(){ this.resourceProperties.put(RECORD_TYPE, this.giveMeUsageRecordType()); this.resourceProperties.put(USAGE_RECORD_TYPE, this.giveMeUsageRecordType()); } /** * {@inheritDoc} */ @Override public String getId() { return (String) this.resourceProperties.get(ID); } /** * {@inheritDoc} */ @Override public void setId(String id) throws InvalidValueException { setResourceProperty(ID, id); } /** * {@inheritDoc} */ @Override public String getConsumerId() { return (String) this.resourceProperties.get(CONSUMER_ID); } /** * {@inheritDoc} */ @Override public void setConsumerId(String consumerId) throws InvalidValueException { setResourceProperty(CONSUMER_ID, consumerId); } protected Calendar timestampStringToCalendar(long millis){ Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(millis); return calendar; } /** * {@inheritDoc} */ @Override public Calendar getCreationTime() { long millis = (Long) this.resourceProperties.get(CREATION_TIME); return timestampStringToCalendar(millis); } /** * {@inheritDoc} */ @Override public void setCreationTime(Calendar creationTime) throws InvalidValueException { setResourceProperty(CREATION_TIME, creationTime.getTimeInMillis()); } /** * {@inheritDoc} */ @Override public String getScope() { return (String) this.resourceProperties.get(SCOPE); } /** * {@inheritDoc} */ @Override public void setScope(String scope) throws InvalidValueException { setResourceProperty(SCOPE, scope); } /** * {@inheritDoc} */ @Override public Map> getResourceProperties() { return new HashMap>(this.resourceProperties); } /** * {@inheritDoc} */ @Override public void setResourceProperties(Map> properties) throws InvalidValueException { Map> validated = validateProperties(properties); this.resourceProperties = new HashMap>(validated); } /** * {@inheritDoc} */ @Override public Comparable getResourceProperty(String key) { return this.resourceProperties.get(key); } /** * {@inheritDoc} */ @Override public void setResourceProperty(String key, Comparable value) throws InvalidValueException { Comparable checkedValue = validateField(key, value); if(checkedValue == null){ this.resourceProperties.remove(key); }else{ this.resourceProperties.put(key, checkedValue); } } // AGGREGATION /* --------------------------------------- */ /** * Return the left end of the time interval covered by this {#UsageRecord} * @return Start Time */ protected long getStartTimeInMillis() { return (Long) this.resourceProperties.get(START_TIME); } /** * Return the left end of the time interval covered by this {#UsageRecord} * @return Start Time */ protected Calendar getStartTimeAsCalendar() { long millis = getStartTimeInMillis(); return timestampStringToCalendar(millis); } /** * Set the left end of the time interval covered by this {#UsageRecord} * @param startTime Start Time * @throws InvalidValueException */ protected void setStartTime(Calendar startTime) throws InvalidValueException { setResourceProperty(START_TIME, startTime.getTimeInMillis()); } /** * Return the right end of the time interval covered by this {#UsageRecord} * @return End Time */ protected long getEndTimeInMillis() { return (Long) this.resourceProperties.get(END_TIME); } /** * Return the right end of the time interval covered by this {#UsageRecord} * @return End Time */ protected Calendar getEndTimeAsCalendar() { long millis = getEndTimeInMillis(); return timestampStringToCalendar(millis); } /** * Set the right end of the time interval covered by this {#UsageRecord} * @param endTime End Time * @throws InvalidValueException */ protected void setEndTime(Calendar endTime) throws InvalidValueException { setResourceProperty(END_TIME, endTime.getTimeInMillis()); } protected int getOperationCount() { return (Integer) this.resourceProperties.get(OPERATION_COUNT); } protected void setOperationCount(int operationCount) throws InvalidValueException { setResourceProperty(OPERATION_COUNT, operationCount); } /* --------------------------------------- */ protected Comparable validateField(String key, Comparable value) throws InvalidValueException { if(key == null){ throw new InvalidValueException("The key of property to set cannot be null"); } Comparable checkedValue = value; List fieldValidators = validation.get(key); if(fieldValidators!=null){ for(FieldAction fieldValidator : fieldValidators){ if(aggregatedFields.contains(key)){ // TODO } if(computedFields.contains(key)){ logger.debug("{} is a computed field. To be calculated all the required fields to calcutalate it MUST be set. In any case the provided value will be ignored.", key); } try { checkedValue = fieldValidator.validate(key, checkedValue, this); } catch (InvalidValueException e) { logger.error(String.format("The provided value %s is NOT valid for field with key %s.", checkedValue.toString(), key)); throw e; } } } return checkedValue; } protected Map> validateProperties(Map> properties) throws InvalidValueException{ Map> validated = new HashMap>(); for(String key : properties.keySet()){ Comparable value = properties.get(key); validated.put(key, validateField(key, value)); } return validated; } /** * {@inheritDoc} */ @Override public void validate() throws InvalidValueException { validateProperties(this.resourceProperties); Set notPresentProperties = new HashSet(); for(String key : this.requiredFields){ if(!this.resourceProperties.containsKey(key)){ notPresentProperties.add(key); } } if(!notPresentProperties.isEmpty()){ String pluralManagement = notPresentProperties.size() == 1 ? "y" : "ies"; throw new InvalidValueException(String.format("The Usage Record does not contain the following required propert%s %s", pluralManagement, notPresentProperties.toString())); } } @Override public String toString(){ return resourceProperties.toString(); } /** * {@inheritDoc} */ @Override public OperationResult getOperationResult(){ return OperationResult.valueOf((String) this.resourceProperties.get(OPERATION_RESULT)); } /** * {@inheritDoc} * @throws InvalidValueException */ @Override public void setOperationResult(OperationResult operationResult) throws InvalidValueException { setResourceProperty(OPERATION_RESULT, operationResult); } /** * Compare this UsageRecord instance with the one provided as argument * @param usageRecord the Usage Record to compare * @return 0 is and only if the UsageRecord provided as parameter * contains all and ONLY the parameters contained in this instance. * If the number of parameters differs, the methods return the difference * between the number of parameter in this instance and the ones in the * UsageRecord provided as parameter. * If the size is the same but the UsageRecord provided as parameter does * not contains all parameters in this instance, -1 is returned. */ @Override public int compareTo(Record record) { Set>> thisSet = this.resourceProperties.entrySet(); Set>> usageRecordSet = record.getResourceProperties().entrySet(); if(thisSet.size() != usageRecordSet.size()){ return thisSet.size() - usageRecordSet.size(); } if(usageRecordSet.containsAll(thisSet)){ return 0; } return 1; } }