Java tutorial
/******************************************************************************* * Copyright 2016 Intuit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package com.intuit.wasabi.experimentobjects; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.base.Preconditions; import com.intuit.wasabi.experimentobjects.exceptions.InvalidIdentifierException; import io.swagger.annotations.ApiModelProperty; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.joda.time.DateMidnight; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.LongBuffer; import java.util.*; import static com.intuit.wasabi.experimentobjects.Experiment.State.*; import static java.util.Arrays.asList; import static java.util.UUID.randomUUID; /** * An experiment */ public class Experiment implements Cloneable, ExperimentBase { @ApiModelProperty(value = "unique experiment ID", dataType = "UUID", required = true) private Experiment.ID id; @ApiModelProperty(value = "experiment label; unique within the application", dataType = "String", required = true) private Experiment.Label label; @ApiModelProperty(value = "name of the application; e.g. \"QBO\"", dataType = "String", required = true) private Application.Name applicationName; @ApiModelProperty(value = "earliest time the experiment allows bucket assignments", required = true) private Date startTime; @ApiModelProperty(value = "latest time the experiment allows bucket assignments", required = true) private Date endTime; @ApiModelProperty(value = "probability of an eligible user being assigned into the experiment; " + "in range: (0, 1]", required = true) private Double samplingPercent; @ApiModelProperty(value = "description of the experiment", required = false) private String description; @ApiModelProperty(value = "defines a user segment, i.e., if the rule validates to true, user is part of the segment", required = false) private String rule; @ApiModelProperty(value = "defines a user segment in json, i.e., if the rule validates to true, user is part of the segment", required = false) private String ruleJson; @ApiModelProperty(value = "time experiment was created", required = true) private Date creationTime; @ApiModelProperty(value = "last time experiment was modified", required = true) private Date modificationTime; @ApiModelProperty(value = "state of the experiment", required = true) private State state; @ApiModelProperty(value = "is personalization enabled for this experiment", required = false) private Boolean isPersonalizationEnabled; @ApiModelProperty(value = "model name", required = false) private String modelName; @ApiModelProperty(value = "model version no.", required = false) private String modelVersion; @ApiModelProperty(value = "is this a rapid experiment", required = false) private Boolean isRapidExperiment; @ApiModelProperty(value = "maximum number of users to allow before pausing the experiment", required = false) private Integer userCap; @ApiModelProperty(value = "creator of the experiment", required = false) private String creatorID; protected Experiment() { super(); } public static Builder withID(ID id) { return new Builder(id); } public static Builder from(Experiment experiment) { return new Builder(experiment); } @Override public Experiment.ID getID() { return id; } public void setID(Experiment.ID id) { this.id = id; } public Date getCreationTime() { return creationTime; } public void setCreationTime(Date creationTime) { this.creationTime = creationTime; } public Date getModificationTime() { return modificationTime; } public void setModificationTime(Date modificationTime) { this.modificationTime = modificationTime; } @Override public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String getRule() { return rule; } public void setRule(String rule) { this.rule = rule; } public String getRuleJson() { return ruleJson; } public void setRuleJson(String ruleJson) { this.ruleJson = ruleJson; } public Double getSamplingPercent() { return samplingPercent; } public void setSamplingPercent(Double samplingPercent) { this.samplingPercent = samplingPercent; } @Override public Date getStartTime() { return startTime; } public void setStartTime(Date startTime) { this.startTime = startTime; } @Override public Date getEndTime() { return endTime; } public void setEndTime(Date endTime) { this.endTime = endTime; } @Override public State getState() { return state; } public void setState(State state) { this.state = state; } @Override public Experiment.Label getLabel() { return label; } public void setLabel(Experiment.Label label) { this.label = label; } @Override public Application.Name getApplicationName() { return applicationName; } public void setApplicationName(Application.Name applicationName) { this.applicationName = applicationName; } @Override public Boolean getIsPersonalizationEnabled() { return isPersonalizationEnabled; } public void setIsPersonalizationEnabled(Boolean isPersonalizationEnabled) { this.isPersonalizationEnabled = isPersonalizationEnabled; } public String getModelName() { return modelName; } public void setModelName(String modelName) { this.modelName = modelName; } public String getModelVersion() { return modelVersion; } public void setModelVersion(String modelVersion) { this.modelVersion = modelVersion; } public Integer getUserCap() { return userCap; } public void setUserCap(Integer userCap) { this.userCap = userCap; } public Boolean getIsRapidExperiment() { return isRapidExperiment; } public void setIsRapidExperiment(Boolean isRapidExperiment) { this.isRapidExperiment = isRapidExperiment; } public String getCreatorID() { return creatorID; } public void setCreatorID(String creatorID) { this.creatorID = creatorID; } @Override public int hashCode() { return new HashCodeBuilder(1, 31).append(id).append(creationTime).append(modificationTime) .append(description).append(rule).append(samplingPercent).append(startTime).append(endTime) .append(state).append(label).append(applicationName).append(isPersonalizationEnabled) .append(modelName).append(modelVersion).append(isRapidExperiment).append(userCap).append(creatorID) .toHashCode(); } @Override public boolean equals(Object obj) { if (obj == null) return false; if (obj == this) return true; if (!(obj instanceof Experiment)) return false; Experiment other = (Experiment) obj; return new EqualsBuilder().append(id, other.getID()).append(creationTime, other.getCreationTime()) .append(modificationTime, other.getModificationTime()).append(description, other.getDescription()) .append(rule, other.getRule()).append(samplingPercent, other.getSamplingPercent()) .append(startTime, other.getStartTime()).append(endTime, other.getEndTime()) .append(state, other.getState()).append(label, other.getLabel()) .append(applicationName, other.getApplicationName()) .append(isPersonalizationEnabled, other.getIsPersonalizationEnabled()) .append(modelName, other.getModelName()).append(modelVersion, other.getModelVersion()) .append(isRapidExperiment, other.getIsRapidExperiment()).append(userCap, other.getUserCap()) .append(creatorID, other.getCreatorID()).isEquals(); } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } @Override public Experiment clone() { try { return (Experiment) super.clone(); } catch (CloneNotSupportedException e) { // Should never happen throw new RuntimeException(e); } } /** * Validates if this experiment could change state to <tt>desiredState</tt>. * * @param desiredState the state to transition in to * @return <code>true</code> if state change is valid, <code>false</code> * otherwise */ public boolean isStateTransitionValid(State desiredState) { if (state == null) { return false; } else { return state.isStateTransitionAllowed(desiredState); } } /** * Signals if this experiment allows to make changes to its data. This is * the case if the current state is DRAFT. * * @return <code>true</code> if changes are valid, <code>false</code> otherwise */ @JsonIgnore public boolean isChangeable() { return state.equals(DRAFT) || state.equals(RUNNING) || state.equals(PAUSED); } /** * Signals if this experiment is deleted. * * @return <code>true</code> if state == DELETED, <code>false</code> otherwise */ @JsonIgnore public boolean isDeleted() { return state.equals(State.DELETED); } /** * Calculates the last day of the experiment. * * This is generally the experiment end date, but may be an earlier date if * the experiment was TERMINATED early. In this case the modification date * is used. * * @return earliestDay */ @JsonIgnore public DateMidnight calculateLastDay() { DateMidnight earliestDay = new DateMidnight(endTime); if (state.equals(State.TERMINATED)) { DateMidnight modifiedDay = new DateMidnight(modificationTime); if (modifiedDay.isBefore(earliestDay)) { earliestDay = modifiedDay; } } return earliestDay; } //TODO: redesign state and state transition to be state machine public enum State { DRAFT, RUNNING, PAUSED, TERMINATED, DELETED; public static State toExperimentState(String state) { for (State experimentState : State.values()) { if (experimentState.name().equalsIgnoreCase(state)) { return experimentState; } } return null; } public boolean isActiveState() { return this.equals(RUNNING) || this.equals(PAUSED); } public boolean isStateTransitionAllowed(State newState) { return ExperimentStateTransition.isStateTransitionAllowed(this, newState); } public enum ExperimentStateTransition { DRAFT(State.DRAFT, State.RUNNING, State.PAUSED, State.DELETED), RUNNING(State.RUNNING, State.PAUSED, State.TERMINATED), PAUSED(State.PAUSED, State.RUNNING, State.TERMINATED), TERMINATED(State.TERMINATED, State.DELETED), DELETED(State.DELETED); private static final Map<State, ArrayList<State>> m = new EnumMap<>(State.class); static { for (ExperimentStateTransition trans : ExperimentStateTransition.values()) { for (State s : trans.getAllowedStateTransitions()) { m.put(s, new ArrayList<>(trans.getAllowedStateTransitions())); } } } private final transient List<State> allowedStateTransitions; ExperimentStateTransition(State... allowedTransitions) { this.allowedStateTransitions = asList(allowedTransitions); } /** * @param oldState original experiment state * @param newState target experiment state * @return a boolean value if the state transition is allowed */ protected static boolean isStateTransitionAllowed(State oldState, State newState) { return m.get(oldState).contains(newState); } private List<State> getAllowedStateTransitions() { return allowedStateTransitions; } } } public static class Builder { private Experiment instance; private Builder(ID id) { super(); instance = new Experiment(); instance.id = Preconditions.checkNotNull(id); } private Builder(Experiment other) { this(other.getID()); instance.creationTime = copyDate(other.creationTime); instance.modificationTime = copyDate(other.modificationTime); instance.description = other.description; instance.rule = other.rule; instance.ruleJson = other.ruleJson; instance.samplingPercent = other.samplingPercent; instance.startTime = copyDate(other.startTime); instance.endTime = copyDate(other.endTime); instance.state = other.state; instance.label = other.label; instance.applicationName = other.applicationName; instance.isPersonalizationEnabled = other.isPersonalizationEnabled; instance.modelName = other.modelName; instance.modelVersion = other.modelVersion; instance.isRapidExperiment = other.isRapidExperiment; instance.userCap = other.userCap; instance.creatorID = other.creatorID; } private Date copyDate(Date date) { return date != null ? new Date(date.getTime()) : null; } public Builder withIsPersonalizationEnabled(Boolean isPersonalizationEnabled) { instance.isPersonalizationEnabled = isPersonalizationEnabled; return this; } public Builder withIsRapidExperiment(Boolean isRapidExperiment) { instance.isRapidExperiment = isRapidExperiment; return this; } public Builder withUserCap(Integer userCap) { instance.userCap = userCap; return this; } public Builder withModelName(String modelName) { instance.modelName = modelName; return this; } public Builder withModelVersion(String modelVersion) { instance.modelVersion = modelVersion; return this; } public Builder withCreationTime(final Date creationTime) { this.instance.creationTime = creationTime; return this; } public Builder withModificationTime(final Date modificationTime) { instance.modificationTime = modificationTime; return this; } public Builder withDescription(final String description) { instance.description = description; return this; } public Builder withRule(final String rule) { instance.rule = rule; return this; } public Builder withSamplingPercent(final Double samplingPercent) { instance.samplingPercent = samplingPercent; return this; } public Builder withStartTime(final Date startTime) { instance.startTime = startTime; return this; } public Builder withEndTime(final Date endTime) { instance.endTime = endTime; return this; } public Builder withState(final State state) { instance.state = state; return this; } public Builder withLabel(final Experiment.Label label) { instance.label = label; return this; } public Builder withApplicationName(final Application.Name appName) { instance.applicationName = appName; return this; } public Builder withCreatorID(final String creatorID) { instance.creatorID = creatorID; return this; } public Experiment build() { Experiment result = instance; instance = null; return result; } } /** * Encapsulates the ID for the experiment */ @JsonSerialize(using = Experiment.ID.Serializer.class) @JsonDeserialize(using = Experiment.ID.Deserializer.class) public static class ID { private UUID id; private ID(UUID id) { super(); this.id = Preconditions.checkNotNull(id); } /** * Creates a new, random ID * * @return ID */ public static ID newInstance() { return new ID(randomUUID()); } public static ID valueOf(UUID value) { return new ID(value); } public static ID valueOf(byte[] value) { if (value.length != 16) { throw new InvalidIdentifierException( "Argument \"value\" must " + "be a 16-byte array representing a UUID"); } try { LongBuffer buffer = ByteBuffer.wrap(value).asLongBuffer(); long msb = buffer.get(); long lsb = buffer.get(); return new ID(new UUID(msb, lsb)); } catch (IllegalArgumentException e) { throw new InvalidIdentifierException( "Invalid experiment " + "identifier \"" + Arrays.toString(value) + "\"", e); } } public static ID valueOf(String value) { try { return new ID(UUID.fromString(value)); } catch (IllegalArgumentException e) { throw new InvalidIdentifierException("Invalid experiment identifier \"" + value + "\"", e); } } @Override public int hashCode() { int hash = 3; hash = 59 * hash + Objects.hashCode(this.id); return hash; } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); } @Override public String toString() { return id.toString(); } /** * Returns the raw ID * * @return UUID */ public UUID getRawID() { return id; } public static class Serializer extends JsonSerializer<ID> { @Override public void serialize(ID value, JsonGenerator generator, SerializerProvider provider) throws IOException { generator.writeString(value.id.toString()); } } public static class Deserializer extends JsonDeserializer<ID> { @Override public ID deserialize(JsonParser parser, DeserializationContext context) throws IOException { return ID.valueOf(parser.getText()); } } } /** * Encapsulates the label for the experiment */ @JsonSerialize(using = Experiment.Label.Serializer.class) @JsonDeserialize(using = Experiment.Label.Deserializer.class) public static class Label { private String label; private Label(String label) { super(); this.label = Preconditions.checkNotNull(label); if (!label.matches("^[_\\-$A-Za-z][_\\-$A-Za-z0-9]*")) { throw new InvalidIdentifierException( "Experiment label \"" + label + "\" must begin with a letter, dollar sign, or " + "underscore, and must not contain any spaces"); } } public static Label valueOf(String value) { Label result = new Label(value); return result; } @Override public String toString() { return label; } @Override public int hashCode() { int hash = 5; hash = 59 * hash + Objects.hashCode(this.label); return hash; } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); } public static class Serializer extends JsonSerializer<Label> { @Override public void serialize(Label label, JsonGenerator generator, SerializerProvider provider) throws IOException { generator.writeString(label.toString()); } } public static class Deserializer extends JsonDeserializer<Label> { @Override public Label deserialize(JsonParser parser, DeserializationContext context) throws IOException { return Label.valueOf(parser.getText()); } } } public static class ExperimentAuditInfo { private String attributeName; private String oldValue; private String newValue; public ExperimentAuditInfo(String attributeName, String oldValue, String newValue) { this.attributeName = attributeName; this.oldValue = oldValue; this.newValue = newValue; } public String getAttributeName() { return attributeName; } public String getOldValue() { return oldValue; } public String getNewValue() { return newValue; } } }