/* * Copyright (c) 2017 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * 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 org.obiba.magma; import java.lang.reflect.Constructor; import java.util.Collection; import java.util.NoSuchElementException; import java.util.Set; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; /** * The meta-data of a {@code Value}. {@code Value} instances can be obtained for {@code ValueSet} instances. When this * value requires description, this is done through an instance of {@code Variable}. */ public interface Variable extends AttributeAware { /** * The name of the variable. A variable's name must be unique within its {@code Collection}. * * @return the name of the variable. */ String getName(); /** * Returns the {@code entityType} this variable is associated with. * * @return */ String getEntityType(); /** * Returns true when this variable is for values of the specified {@code entityType} * * @param type the type of entity to test * @return true when this variable is for values of the specified {@code entityType}, false otherwise. */ boolean isForEntityType(String type); /** * Returns true when this variable is repeatable. A repeatable variable is one where multiple {@code Value} instances * may exist within a single {@code ValueSet}. A single {@code Value} within a {@code ValueSet} is referenced by an * {@link Occurrence} instance. * * @return true when this variable may have multiple values within a single {@code ValueSet} */ boolean isRepeatable(); /** * When a variable is repeatable, the repeated values are grouped together, this method returns the name of this * group. The name is arbitrary but must be unique within a {@code Collection}. * * @return the name of the repeating group */ String getOccurrenceGroup(); /** * Returns the {@code ValueType} of this variable instance. Any {@code Value} obtained for this {@code variable} * should be of this type. * * @return the {@code ValueType} of this variable. */ ValueType getValueType(); /** * The SI code of the measurement unit if applicable. * * @return unit */ String getUnit(); /** * The IANA mime-type of binary data if applicable. * * @return the IANA mime-type */ String getMimeType(); /** * Used when this variable value is a pointer to another {@code VariableEntity}. The value is considered to point to * the referenced entity's {@code identifier}. * * @return the {@code entityType} that this value points to, this method returns null when the variable doesn't point * to another entity. */ String getReferencedEntityType(); /** * Get the index (or position) of this variable in the list of variables of a table. Default value is 0, in which case * the natural order should apply. * * @return */ int getIndex(); /** * Returns true if this variable has at least on {@code Category} * * @return */ boolean hasCategories(); /** * Returns the set of categories for this {@code Variable}. This method returns null when the variable has no * categories. To determine if a {@code Variable} instance has categories, use the {@code #hasCategories()} method. * * @return a {@code Set} of {@code Category} instances or null if none exist */ Set<Category> getCategories(); @Nullable Category getCategory(String categoryName); /** * Returns true when {@code value} is equal to a {@code Category} marked as {@code missing} or when * {@code Value#isNull} returns true * * @param value the value to test * @return true when the value is considered {@code missing}, false otherwise. */ boolean isMissingValue(Value value); boolean areAllCategoriesMissing(); String getVariableReference(@NotNull ValueTable table); /** * A builder for {@code Variable} instances. This uses the builder pattern for constructing {@code Variable} * instances. */ class Builder extends AttributeAwareBuilder<Builder> { private VariableBean variable = new VariableBean(); @SuppressWarnings("ConstantConditions") public Builder(@NotNull String name, @NotNull ValueType type, @NotNull String entityType) { if(name == null) throw new IllegalArgumentException("name cannot be null"); if(type == null) throw new IllegalArgumentException("type cannot be null"); if(entityType == null) throw new IllegalArgumentException("entityType cannot be null"); if(name.contains(":")) throw new IllegalArgumentException("variable name cannot contain ':'"); if(name.isEmpty()) throw new IllegalArgumentException("variable name cannot be empty"); variable.name = name; variable.valueType = type; variable.entityType = entityType; } protected Builder(Builder builder) { variable = builder.variable; } @Override protected ListMultimap<String, Attribute> getAttributes() { return variable.attributes; } @Override protected Builder getBuilder() { return this; } public static Builder newVariable(String name, ValueType type, String entityType) { return new Builder(name, type, entityType); } public static Builder sameAs(Variable variable, boolean sameCategories) { Builder builder = newVariable(variable.getName(), variable.getValueType(), variable.getEntityType()) .unit(variable.getUnit()).mimeType(variable.getMimeType()) .referencedEntityType(variable.getReferencedEntityType()).index(variable.getIndex()); if(variable.isRepeatable()) { builder.repeatable().occurrenceGroup(variable.getOccurrenceGroup()); } for(Attribute a : variable.getAttributes()) { builder.addAttribute(a); } if(sameCategories) { for(Category c : variable.getCategories()) { builder.addCategory(c); } } return builder; } public static Builder sameAs(Variable variable) { return sameAs(variable, true); } /** * Values from the provided override {@code Variable} will overwrite values in the {@code Variable} currently being * built. * * @param override The {@code Variable} contains values that will override values in the {@code Variable} currently * being built. */ public Builder overrideWith(Variable override) { variable.name = override.getName(); if(override.getValueType() != null) variable.valueType = override.getValueType(); if(override.getEntityType() != null) variable.entityType = override.getEntityType(); if(override.getMimeType() != null) variable.mimeType = override.getMimeType(); if(override.getOccurrenceGroup() != null) variable.occurrenceGroup = override.getOccurrenceGroup(); if(override.getUnit() != null) variable.unit = override.getUnit(); variable.repeatable = override.isRepeatable(); variable.index = override.getIndex(); variable.attributes = (LinkedListMultimap<String, Attribute>) overrideAttributes(getAttributes(), override.getAttributes()); for(Category category : override.getCategories()) { overrideCategories(variable.categories, category); } return this; } private void overrideCategories(Collection<Category> categories, Category overrideCategory) { if(categoryWithNameExists(categories, overrideCategory.getName())) { Category existingCategory = getCategoryWithName(categories, overrideCategory.getName()); Category.Builder builder = Category.Builder.sameAs(existingCategory); if(overrideCategory.getCode() != null) builder.withCode(overrideCategory.getCode()); builder.missing(overrideCategory.isMissing()); builder.clearAttributes(); for(Attribute a : overrideAttributes(existingCategory.getAttributes(), overrideCategory.getAttributes()) .values()) { builder.addAttribute(a); } categories.remove(existingCategory); categories.add(builder.build()); } else { categories.add(overrideCategory); } } private static boolean categoryWithNameExists(Iterable<Category> categories, String name) { try { getCategoryWithName(categories, name); return true; } catch(NoSuchElementException e) { return false; } } private static Category getCategoryWithName(Iterable<Category> categories, final String name) throws NoSuchElementException { return Iterables.find(categories, new Predicate<Category>() { @Override public boolean apply(Category input) { return input != null && input.getName().equals(name); } }); } @SuppressWarnings("UnusedDeclaration") public Builder clearAttributes() { getAttributes().clear(); return this; } @SuppressWarnings("UnusedDeclaration") public Builder clearCategories() { variable.categories.clear(); return this; } /** * Tests whether this {@code Builder} instance is constructing a variable with any of the the specified names. * * @param name one or more names to test * @return true if any of the specified names is equal to the variable's name */ public boolean isName(String... name) { for(String aName : name) { if(variable.name.equals(aName)) { return true; } } return false; } @SuppressWarnings("ConstantConditions") public Builder name(@NotNull String name) { if(name == null) throw new IllegalArgumentException("name cannot be null"); variable.name = name; return this; } @SuppressWarnings("ConstantConditions") public Builder type(@NotNull ValueType type) { if(type == null) throw new IllegalArgumentException("type cannot be null"); variable.valueType = type; return this; } @SuppressWarnings("UnusedDeclaration") public boolean isType(ValueType type) { return variable.valueType == type; } public Variable build() { return variable; } public Builder occurrenceGroup(String name) { variable.occurrenceGroup = name; return this; } public Builder repeatable(boolean repeatable) { variable.repeatable = repeatable; return this; } public Builder repeatable() { return repeatable(true); } public Builder unit(String unit) { // TODO: Should we parse the unit and make it's valid? Using jscience API for example. variable.unit = unit; return this; } public Builder mimeType(String mimeType) { variable.mimeType = mimeType; return this; } public Builder referencedEntityType(String entityType) { variable.referencedEntityType = entityType; return this; } public Builder index(Integer index) { variable.index = index == null ? 0 : index; return this; } public Builder index(String index) { try { index(Integer.valueOf(index)); } catch(NumberFormatException e) { // ignored } return this; } public Builder addCategory(String name, String code) { return addCategory(name, code, null); } public Builder addCategory(String name, String code, @Nullable Iterable<Category.BuilderVisitor> visitors) { Category.Builder categoryBuilder = Category.Builder.newCategory(name).withCode(code); if(visitors != null) { for(Category.BuilderVisitor categoryVisitor : visitors) { categoryBuilder.accept(categoryVisitor); } } variable.categories.add(categoryBuilder.build()); return this; } public Builder addCategory(String name, String code, boolean missing) { variable.categories.add(Category.Builder.newCategory(name).withCode(code).missing(missing).build()); return this; } public Builder addCategory(Category category) { variable.categories.add(category); return this; } /** * Add an array of category labels. The resulting {@code Category} instances will have a null {@code code} value. * This method is useful for creating categories out of {@code enum} constants for example. * * @param names * @return this */ public Builder addCategories(String... names) { for(String name : names) { variable.categories.add(Category.Builder.newCategory(name).build()); } return this; } public Builder addCategories(Iterable<Category> categories) { for(Category category : categories) { variable.categories.add(category); } return this; } /** * Accepts a {@code BuilderVisitor} to allow it to visit this {@code Builder} instance. * * @param visitor the visitor to accept; cannot be null. * @return this */ public Builder accept(BuilderVisitor visitor) { visitor.visit(this); return this; } /** * Accepts a collection of visitors and calls {@code #accept(BuilderVisitor)} on each instance. * * @param visitors the collection of visitors to accept * @return this */ public Builder accept(Iterable<? extends BuilderVisitor> visitors) { for(BuilderVisitor visitor : visitors) { accept(visitor); } return this; } /** * Extends this builder to perform additional building capabilities for different variable nature. The specified * type must extend {@code Variable.Builder} and expose a public constructor that takes a single * {@code Variable.Builder} parameter; the constructor should call its super class' constructor with the same * signature. * <p/> * An example class * <p/> * <pre> * public class BuilderExtension extends Variable.Builder { * public BuilderExtension(Variable.Builder builder) { * super(builder); * } * * public BuilderExtension withExtension(String value) { * addAttribute("extension", value); * return this; * } * } * </pre> * * @param <T> * @param type the {@code Builder} type to construct * @return an instance of {@code T} that extends {@code Builder} */ public <T extends Builder> T extend(Class<T> type) { try { Constructor<T> ctor = type.getConstructor(Builder.class); return ctor.newInstance(this); } catch(NoSuchMethodException e) { throw new IllegalArgumentException("Builder extension type '" + type.getName() + "' must expose a public constructor that takes a single argument of type '" + Builder.class.getName() + "'."); } catch(Exception e) { throw new IllegalArgumentException("Cannot instantiate builder extension type '" + type.getName() + "'", e); } } } /** * Visitor pattern for contributing to a {@code Builder} instance through composition. */ interface BuilderVisitor { /** * Visit a builder instance and contribute to the variable being built. * * @param builder the instance to contribute to. */ void visit(Builder builder); } class Reference { private Reference() {} public static String getReference(@NotNull ValueTable table, Variable variable) { return table.getTableReference() + ":" + variable.getName(); } public static String getReference(String datasource, String table, String variable) { return ValueTable.Reference.getReference(datasource, table) + ":" + variable; } } }