CUnion.java
/*
* Copyright 2016 Providence Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 net.morimekta.providence.reflect.contained;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageBuilder;
import net.morimekta.providence.PType;
import net.morimekta.providence.PUnion;
import net.morimekta.providence.descriptor.PAnnotation;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PUnionDescriptor;
import net.morimekta.util.collect.UnmodifiableList;
import net.morimekta.util.collect.UnmodifiableMap;
import net.morimekta.util.collect.UnmodifiableSet;
import net.morimekta.util.collect.UnmodifiableSortedMap;
import net.morimekta.util.collect.UnmodifiableSortedSet;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* @author Stein Eldar Johnsen
* @since 07.09.15
*/
public class CUnion implements PUnion<CUnion> {
private final CUnionDescriptor descriptor;
private final CField<CUnion> unionField;
private final Object unionValue;
@SuppressWarnings("unchecked")
private CUnion(Builder builder) {
this.unionField = builder.unionField;
this.descriptor = builder.descriptor;
if (builder.currentValue instanceof PMessageBuilder) {
this.unionValue = ((PMessageBuilder<?>) builder.currentValue).build();
} else if (builder.currentValue instanceof List) {
this.unionValue = UnmodifiableList.copyOf((List<?>) builder.currentValue);
} else if (builder.currentValue instanceof Map) {
if (PContainer.typeForName(unionField.getAnnotationValue(PAnnotation.CONTAINER)) ==
PContainer.Type.SORTED) {
this.unionValue = UnmodifiableSortedMap.copyOf((Map<?,?>) builder.currentValue);
} else {
this.unionValue = UnmodifiableMap.copyOf((Map<?,?>) builder.currentValue);
}
} else if (builder.currentValue instanceof Set) {
if (PContainer.typeForName(unionField.getAnnotationValue(PAnnotation.CONTAINER)) ==
PContainer.Type.SORTED) {
this.unionValue = UnmodifiableSortedSet.copyOf((Set<?>) builder.currentValue);
} else {
this.unionValue = UnmodifiableSet.copyOf((Set<?>) builder.currentValue);
}
} else {
this.unionValue = builder.currentValue;
}
}
@Override
public boolean has(int key) {
return unionField != null && unionField.getId() == key && unionValue != null;
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(int key) {
return has(key) ? (T) unionValue : null;
}
@Nonnull
@Override
public PMessageBuilder<CUnion> mutate() {
return new Builder(this);
}
@Nonnull
@Override
public String asString() {
return CStruct.asString(this);
}
@Override
public String toString() {
return descriptor.getQualifiedName() + asString();
}
@Nonnull
@Override
public CUnionDescriptor descriptor() {
return descriptor;
}
@Override
public boolean unionFieldIsSet() {
return unionField != null;
}
@Nonnull
@Override
public CField<CUnion> unionField() {
if (unionField == null) throw new IllegalStateException("No union field set in " + descriptor.getQualifiedName());
return unionField;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CUnion)) {
return false;
}
CUnion other = (CUnion) o;
return Objects.equals(descriptor, other.descriptor) &&
Objects.equals(unionField, other.unionField) &&
Objects.equals(unionValue, other.unionValue);
}
@Override
public int hashCode() {
return Objects.hash(descriptor().getQualifiedName(), unionField, unionValue);
}
@Override
public int compareTo(@Nonnull CUnion other) {
return CStruct.compareMessages(this, other);
}
public static class Builder extends PMessageBuilder<CUnion> {
private final CUnionDescriptor descriptor;
private CField<CUnion> originalField;
private boolean updated;
private CField<CUnion> unionField;
private Object currentValue;
public Builder(CUnionDescriptor descriptor) {
this.descriptor = descriptor;
this.originalField = null;
this.updated = false;
}
public Builder(CUnion union) {
this.descriptor = union.descriptor;
if (union.unionField != null) {
this.set(union.unionField, union.unionValue);
}
this.originalField = union.unionField;
this.updated = false;
}
@Nonnull
@Override
public PMessageBuilder<?> mutator(int key) {
CField<CUnion> field = descriptor.findFieldById(key);
if (field == null) {
throw new IllegalArgumentException("No such field ID " + key);
} else if (field.getType() != PType.MESSAGE) {
throw new IllegalArgumentException("Not a message field ID " + key + ": " + field.getName());
}
if (unionField != field) {
unionField = field;
currentValue = null;
}
updated = true;
if (currentValue == null) {
currentValue = ((PMessageDescriptor<?>) field.getDescriptor()).builder();
} else if (currentValue instanceof PMessage) {
currentValue = ((PMessage<?>) currentValue).mutate();
} else if (!(currentValue instanceof PMessageBuilder)) {
// This should in theory not be possible. This is just a safe-guard.
throw new IllegalArgumentException("Invalid currentValue in map on message type: " + currentValue.getClass().getSimpleName());
}
return (PMessageBuilder<?>) currentValue;
}
@Nonnull
@Override
@SuppressWarnings("unchecked,rawtypes")
public Builder merge(@Nonnull CUnion from) {
if (unionField == null || unionField != from.unionField) {
if (from.unionField != null) {
set(from.unionField.getId(), from.unionValue);
}
} else {
this.updated = true;
int key = unionField.getId();
switch (unionField.getType()) {
case MESSAGE: {
PMessageBuilder src;
if (currentValue instanceof PMessageBuilder) {
src = (PMessageBuilder<?>) currentValue;
} else {
src = ((PMessage<?>) currentValue).mutate();
}
PMessage toMerge = from.get(key);
currentValue = src.merge(toMerge);
break;
}
case SET:
((Set<Object>) currentValue).addAll(from.get(key));
break;
case MAP:
((Map<Object, Object>) currentValue).putAll(from.get(key));
break;
default:
// Lists replace, not add.
set(key, from.get(key));
break;
}
}
return this;
}
@Override
public boolean has(int key) {
CField<?> field = descriptor.findFieldById(key);
return field != null && unionField == field;
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(int key) {
CField<?> field = descriptor.findFieldById(key);
if (unionField == field) {
return (T) currentValue;
}
return null;
}
@Nonnull
@Override
public PUnionDescriptor<CUnion> descriptor() {
return descriptor;
}
@Nonnull
@Override
public CUnion build() {
return new CUnion(this);
}
@Override
public boolean valid() {
return unionField != null && currentValue != null;
}
@Override
public Builder validate() {
if (!valid()) {
throw new IllegalStateException("No union field set in " +
descriptor().getQualifiedName());
}
return this;
}
@Nonnull
@Override
public Builder set(int key, Object value) {
CField<CUnion> field = descriptor.findFieldById(key);
if (field == null) {
return this; // soft ignoring unsupported fields.
}
if (value == null) {
return clear(key);
}
this.updated = true;
this.unionField = field;
switch (field.getType()) {
case SET:
this.currentValue = new LinkedHashSet<>((Collection<?>) value);
break;
case LIST:
this.currentValue = new ArrayList<>((Collection<?>) value);
break;
case MAP:
this.currentValue = new LinkedHashMap<>((Map<?,?>) value);
break;
default:
this.currentValue = value;
break;
}
return this;
}
@Override
public boolean isSet(int key) {
return unionField != null && unionField.getId() == key;
}
@Override
public boolean isModified(int key) {
if (updated) {
if (unionField != null && unionField.getId() == key) return true;
return originalField != null && originalField.getId() == key;
}
return false;
}
@Nonnull
@Override
@SuppressWarnings("unchecked")
public Builder addTo(int key, Object value) {
CField<CUnion> field = descriptor.findFieldById(key);
if (field == null) {
return this; // soft ignoring unsupported fields.
}
if (field.getType() != PType.LIST &&
field.getType() != PType.SET) {
throw new IllegalArgumentException("Unable to accept addTo on non-list field " + field.getName());
}
if (value == null) {
throw new IllegalArgumentException("Adding null item to collection " + field.getName());
}
this.updated = true;
if (this.unionField != field || this.currentValue == null) {
this.unionField = field;
switch (field.getType()) {
case LIST: {
this.currentValue = new ArrayList<>();
break;
}
case SET: {
this.currentValue = new LinkedHashSet<>();
break;
}
default:
break;
}
}
((Collection<Object>) this.currentValue).add(value);
return this;
}
@Nonnull
@Override
public Builder clear(int key) {
if (isSet(key)) {
this.updated = true;
if (originalField == null) {
originalField = unionField;
}
this.unionField = null;
this.currentValue = null;
}
return this;
}
}
}