From fc6d93565b72f00942b826fe71eb89f63c02d6a4 Mon Sep 17 00:00:00 2001 From: Ashley Heath Date: Mon, 1 Jul 2019 17:56:24 +0100 Subject: [PATCH] First pass at add and put methods for building collections and maps within beans (#100). --- pom.xml | 4 + src/main/java/org/joda/beans/gen/BeanGen.java | 18 +++ .../org/joda/beans/gen/BeanGenConfig.java | 44 ++++- .../org/joda/beans/gen/CollectionGen.java | 61 +++++++ .../java/org/joda/beans/gen/PropertyData.java | 72 ++++++++- .../java/org/joda/beans/gen/PropertyGen.java | 150 +++++++++++++++++- .../org/joda/beans/gen/PropertyParser.java | 2 + .../java/org/joda/beans/utils/NameUtils.java | 48 ++++++ .../resources/org/joda/beans/gen/guava.ini | 30 ++++ 9 files changed, 423 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/joda/beans/gen/CollectionGen.java create mode 100644 src/main/java/org/joda/beans/utils/NameUtils.java diff --git a/pom.xml b/pom.xml index f94510d8..b8cae776 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,10 @@ Martynas Sateika https://github.com/martynassateika + + Ashley Heath + https://github.com/ashleyheath + diff --git a/src/main/java/org/joda/beans/gen/BeanGen.java b/src/main/java/org/joda/beans/gen/BeanGen.java index 0bfe1e0a..47f7c1d5 100644 --- a/src/main/java/org/joda/beans/gen/BeanGen.java +++ b/src/main/java/org/joda/beans/gen/BeanGen.java @@ -1249,6 +1249,8 @@ private void generateBuilderClass() { } generateIndentedSeparator(); generateBuilderPropertySetMethods(); + generateBuilderPropertyAddMethods(); + generateBuilderPropertyPutMethods(); generateIndentedSeparator(); generateBuilderToString(); addLine(1, "}"); @@ -1395,6 +1397,22 @@ private void generateBuilderPropertySetMethods() { } } + private void generateBuilderPropertyAddMethods() { + if (data.isEffectiveBuilderScopeVisible()) { + for (PropertyGen prop : nonDerivedProperties()) { + addLines(prop.generateBuilderAddMethod()); + } + } + } + + private void generateBuilderPropertyPutMethods() { + if (data.isEffectiveBuilderScopeVisible()) { + for (PropertyGen prop : nonDerivedProperties()) { + addLines(prop.generateBuilderPutMethod()); + } + } + } + private void generateBuilderToString() { List nonDerived = toStringProperties(); if (data.isImmutable() && data.isTypeFinal()) { diff --git a/src/main/java/org/joda/beans/gen/BeanGenConfig.java b/src/main/java/org/joda/beans/gen/BeanGenConfig.java index 0567c263..f1ba47dd 100644 --- a/src/main/java/org/joda/beans/gen/BeanGenConfig.java +++ b/src/main/java/org/joda/beans/gen/BeanGenConfig.java @@ -50,6 +50,10 @@ public final class BeanGenConfig { * The builder generators. */ private final Map builderGenerators; + /** + * The collection generators. + */ + private final Map collectionGenerators; /** * The invalid immutable types. */ @@ -96,7 +100,7 @@ public static BeanGenConfig parse(String resourceLocator) { if (loader == null) { throw new IllegalArgumentException("ClassLoader was null: " + fullFile); } - URL url = loader.getResource(fullFile); + URL url = BeanGenConfig.class.getResource("guava.ini"); if (url == null) { throw new IllegalArgumentException("Configuration file not found: " + fullFile); } @@ -131,6 +135,7 @@ private static BeanGenConfig parse(List lines) { Map immutableGetClones = new HashMap<>(); Map immutableVarArgs = new HashMap<>(); Map builderInits = new HashMap<>(); + Map builderAdds = new HashMap<>(); Map builderTypes = new HashMap<>(); Set invalidImmutableTypes = new HashSet<>(); for (ListIterator iterator = lines.listIterator(); iterator.hasNext(); ) { @@ -238,6 +243,21 @@ private static BeanGenConfig parse(List lines) { String value = line.substring(pos + 1).trim(); builderInits.put(key, value); } + } else if (line.equals("[immutable.builder.add]")){ + while (iterator.hasNext()) { + line = iterator.next().trim(); + if (line.startsWith("[")) { + iterator.previous(); + break; + } + int pos = line.indexOf('='); + if (pos <= 0) { + throw new IllegalArgumentException("Invalid ini file line: " + line); + } + String key = line.substring(0, pos).trim(); + String value = line.substring(pos + 1).trim(); + builderAdds.put(key, value); + } } else { throw new IllegalArgumentException("Invalid ini file section: " + line); } @@ -261,7 +281,13 @@ private static BeanGenConfig parse(List lines) { } copyGenerators.put(fieldType, new CopyGen.PatternCopyGen(immutableCopier, mutableCopier)); } - return new BeanGenConfig(copyGenerators, builderGenerators, builderTypes, invalidImmutableTypes, immutableVarArgs, immutableGetClones); + Map collectionGenerators = new HashMap<>(); + for (Entry entry : builderAdds.entrySet()) { + String fieldType = entry.getKey(); + String adder = entry.getValue(); + collectionGenerators.put(fieldType, new CollectionGen.PatternCollectionGen(adder)); + } + return new BeanGenConfig(copyGenerators, builderGenerators, collectionGenerators, builderTypes, invalidImmutableTypes, immutableVarArgs, immutableGetClones); } //----------------------------------------------------------------------- @@ -270,6 +296,7 @@ private static BeanGenConfig parse(List lines) { * * @param copyGenerators the copy generators, not null * @param builderGenerators the builder generators, not null + * @param collectionGenerators the collection generators, not null * @param builderTypes the builder types, not null * @param invalidImmutableTypes the invalid immutable types, not null * @param immutableVarArgs the varargs code @@ -278,12 +305,14 @@ private static BeanGenConfig parse(List lines) { private BeanGenConfig( Map copyGenerators, Map builderGenerators, + Map collectionGenerators, Map builderTypes, Set invalidImmutableTypes, Map immutableVarArgs, Map immutableGetClones) { this.copyGenerators = copyGenerators; this.builderGenerators = builderGenerators; + this.collectionGenerators = collectionGenerators; this.builderTypes = builderTypes; this.invalidImmutableTypes = invalidImmutableTypes; this.immutableVarArgs = immutableVarArgs; @@ -302,13 +331,22 @@ public Map getCopyGenerators() { /** * The builder generators. - * + * * @return the generators, not null */ public Map getBuilderGenerators() { return builderGenerators; } + /** + * The collection generators. + * + * @return the generators, not null + */ + public Map getCollectionGenerators() { + return collectionGenerators; + } + /** * The builder types. * diff --git a/src/main/java/org/joda/beans/gen/CollectionGen.java b/src/main/java/org/joda/beans/gen/CollectionGen.java new file mode 100644 index 00000000..c96606af --- /dev/null +++ b/src/main/java/org/joda/beans/gen/CollectionGen.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019-present Stephen Colebourne + * + * 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 org.joda.beans.gen; + +/** + * A generator of collection code. + */ +abstract class CollectionGen { + + /** + * Generates code to add an element to a collection. + * + * @param existing the name of an existing collection or an expression evaluating to an instance of a collection, not null + * @param collectionTypeParams the type params of the collection, may be empty + * @param value the name of the value to add to the collection or an expression evaluating to an instance of a collection, not null + * @return the generated code, not null + */ + abstract String generateAddToCollection(String existing, String collectionTypeParams, String value); + + /** + * Generates code to add a new key/value pair to a map. + * + * @param existing the name of an existing map or an expression evaluating to an instance of a map, not null + * @param mapTypeParams the type params of the map, may be empty + * @param key the name of the key to add to the map or an expression evaluating to an instance of the key type, not null + * @param value the name of the value to add to the map or an expression evaluating to an instance of the value type, not null + * @return the generated code, not null + */ + abstract String generateAddToMap(String existing, String mapTypeParams, String key, String value); + + static class PatternCollectionGen extends CollectionGen { + private final String pattern; + + PatternCollectionGen(String pattern) { + this.pattern = pattern; + } + + @Override + String generateAddToCollection(String existing, String collectionTypeParams, String value) { + return generateAddToMap(existing, collectionTypeParams, "", value); + } + + @Override + String generateAddToMap(String existing, String mapTypeParams, String key, String value) { + return pattern.replaceAll("\\$existing", existing).replaceAll("\\$params", mapTypeParams).replaceAll("\\$value", value).replaceAll("\\$key", key); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/joda/beans/gen/PropertyData.java b/src/main/java/org/joda/beans/gen/PropertyData.java index 6d54b90f..8a0e141e 100644 --- a/src/main/java/org/joda/beans/gen/PropertyData.java +++ b/src/main/java/org/joda/beans/gen/PropertyData.java @@ -20,6 +20,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -119,6 +120,8 @@ class PropertyData { private CopyGen copyGen; /** The builder generator. */ private BuilderGen builderGen; + /** The collection generator. */ + private CollectionGen collectionGen; /** The config. */ private BeanGenConfig config; @@ -658,6 +661,19 @@ public String getGenericParamType() { return type.substring(pos + 1, type.length() - 1); } + /** + * Gets the parameterisations of the property. + * {@code Map} will return a list of {@code String} and {@code Object}. + * @return the generic types, or an empty list if not generic, not null + */ + public List getGenericParamTypes() { + String params = getGenericParamType(); + if (params.isEmpty()) { + return Collections.emptyList(); + } + return separateGenericParamTypes(params); + } + /** * Checks if the type is the generic type of the bean. * For example, if the property is of type T or T[] in a bean of Foo[T]. @@ -984,7 +1000,7 @@ public CopyGen getCopyGen() { //----------------------------------------------------------------------- /** - * Resolves the copy generator. + * Resolves the builder generator. */ public void resolveBuilderGen() { if (getBean().isMutable()) { @@ -1012,6 +1028,29 @@ public BuilderGen getBuilderGen() { return builderGen; } + //----------------------------------------------------------------------- + /** + * Resolves the collection generator. + */ + public void resolveCollectionGen() { + if (getBean().isMutable()) { + if (!getBean().isBuilderScopeVisible() && !getBean().isBeanStyleLightOrMinimal()) { + return; // no builder + } + } + if (!isDerived()) { + collectionGen = config.getCollectionGenerators().get(getFieldTypeRaw()); + } + } + + /** + * Gets the collection generator. + * @return the collection generator + */ + public CollectionGen getCollectionGen() { + return collectionGen; + } + //----------------------------------------------------------------------- /** * Checks if this property is an array type. @@ -1157,4 +1196,35 @@ public String getVarArgsCode() { return config.getImmutableVarArgs().get(getTypeRaw()); } + /** + * Take a string of type parameters (such as (@code String, List} and separate it out + * into the top-level types it contains. + * + * @param typesString the param type string + * @return a list of the top-level types contained within the string (including their own parameter types) + */ + private static List separateGenericParamTypes(String typesString) { + List types = new ArrayList<>(); + + StringBuilder typeBuilder = new StringBuilder(); + int nestingDepth = 0; + for (Character character : typesString.toCharArray()) { + if (character.equals('<')) { + nestingDepth += 1; + typeBuilder.append(character); + } else if (character.equals('>')) { + nestingDepth -= 1; + typeBuilder.append(character); + } else if (character.equals(',') && nestingDepth == 0) { + types.add(typeBuilder.toString().trim()); + typeBuilder = new StringBuilder(); + } else { + typeBuilder.append(character); + } + } + + types.add(typeBuilder.toString().trim()); + + return types; + } } diff --git a/src/main/java/org/joda/beans/gen/PropertyGen.java b/src/main/java/org/joda/beans/gen/PropertyGen.java index 9cfc22b2..2958d52a 100644 --- a/src/main/java/org/joda/beans/gen/PropertyGen.java +++ b/src/main/java/org/joda/beans/gen/PropertyGen.java @@ -22,6 +22,7 @@ import org.joda.beans.MetaProperty; import org.joda.beans.Property; import org.joda.beans.impl.direct.DirectMetaProperty; +import org.joda.beans.utils.NameUtils; /** * A property parsed from the source file. @@ -242,10 +243,10 @@ List generateBuilderSetMethod() { String builderType = getBuilderType(); if (builderType.endsWith("[]") && !builderType.endsWith("[][]") && !builderType.equals("byte[]")) { list.add("\t\tpublic Builder" + data.getBean().getTypeGenericName(true) + " " + data.getPropertyName() + - "(" + builderType.substring(0, builderType.length() - 2) + "... " + data.getPropertyName() + ") {"); + "(" + builderType.substring(0, builderType.length() - 2) + "... " + data.getPropertyName() + ") {"); } else { list.add("\t\tpublic Builder" + data.getBean().getTypeGenericName(true) + " " + data.getPropertyName() + - "(" + builderType + " " + data.getPropertyName() + ") {"); + "(" + builderType + " " + data.getPropertyName() + ") {"); } if (data.isValidated()) { list.add("\t\t\t" + data.getValidationMethodName() + "(" + data.getPropertyName() + ", \"" + data.getPropertyName() + "\");"); @@ -258,6 +259,96 @@ List generateBuilderSetMethod() { return list; } + List generateBuilderAddMethod() { + List list = new ArrayList<>(); + if (!(data.isCollectionType() || data.isArrayType())) { + return list; + } + + if (data.isArrayType() && data.getFieldType().contains("[][]")) { + // Only 1 dimensional arrays are supported. + return list; + } + + String valueParamName = "value"; + String adderName = "add" + NameUtils.capitalize(data.getPropertyName()); + String collectionType = getCollectionOrArrayParamType().replaceAll("\\? extends ", ""); + + list.add("\t\t/**"); + list.add("\t\t * Adds an element to the {@code " + data.getPropertyName() + "} property"); + for (String comment : data.getComments()) { + list.add("\t\t * " + comment); + } + list.add("\t\t * @param " + valueParamName + " the new element" + data.getNotNullJavadoc()); + list.add("\t\t * @return this, for chaining, not null"); + if (data.getDeprecatedComment() != null) { + list.add("\t\t * " + data.getDeprecatedComment()); + } + list.add("\t\t */"); + if (data.isDeprecated()) { + list.add("\t\t@Deprecated"); + } + list.add("\t\tpublic Builder" + data.getBean().getTypeGenericName(true) + " " + adderName + "(" + collectionType + " " + valueParamName + ") {"); + if (data.isArrayType()) { + data.getBean().ensureImport(Arrays.class); + String newArray = "new " + data.getFieldType().replaceAll("\\[]", "") + "[1]"; + list.add("\t\t\tthis." + generateBuilderFieldName() + " = " + generateBuilderFieldName() + " == null ? " + newArray + " : Arrays.copyOf(this." + data.getFieldName() + ", this." + data.getFieldName() + ".length + 1);"); + list.add("\t\t\tthis." + generateBuilderFieldName() + "[this." + generateBuilderFieldName() + ".length - 1] = " + valueParamName + ";"); + } else { + String existing = "this." + generateBuilderFieldName() + " == null ? " + data.getBuilderGen().generateInit(data) + " : this." + generateBuilderFieldName(); + String typeParam = (data.isGenericParamType() && !data.isGenericWildcardParamType() ? "<" + data.getGenericParamType() + ">" : "").replaceAll("\\? extends ", ""); + list.add("\t\t\tthis." + generateBuilderFieldName() + " = " + data.getCollectionGen().generateAddToCollection(existing, typeParam, valueParamName) + ";"); + } + if (data.isValidated()) { + list.add("\t\t\t" + data.getValidationMethodName() + "(this." + data.getPropertyName() + ", \"" + data.getPropertyName() + "\");"); + } + list.add("\t\t\treturn this;"); + list.add("\t\t}"); + list.add(""); + return list; + } + + List generateBuilderPutMethod() { + List list = new ArrayList<>(); + if (!data.isMapType()) { + return list; + } + + String putterName = "put" + NameUtils.capitalize(data.getPropertyName()); + String keyType = parseMapKeyType().replaceAll("\\? extends ", ""); + String valueType = parseMapValueType().replaceAll("\\? extends ", ""); + String keyParamName = "key"; + String valueParamName = "value"; + + list.add("\t\t/**"); + list.add("\t\t * Adds an entry to the {@code " + data.getPropertyName() + "} property"); + for (String comment : data.getComments()) { + list.add("\t\t * " + comment); + } + list.add("\t\t * @param " + keyParamName + " the key of the new entry" + data.getNotNullJavadoc()); + list.add("\t\t * @param " + valueParamName + " the value of the new entry"); + list.add("\t\t * @return this, for chaining, not null"); + if (data.getDeprecatedComment() != null) { + list.add("\t\t * " + data.getDeprecatedComment()); + } + list.add("\t\t */"); + if (data.isDeprecated()) { + list.add("\t\t@Deprecated"); + } + list.add("\t\tpublic Builder" + data.getBean().getTypeGenericName(true) + " " + putterName + + "(" + keyType + " " + keyParamName + ", " + valueType + " " + valueParamName + ") {"); + String existing = "this." + generateBuilderFieldName() + " == null ? " + data.getBuilderGen().generateInit(data) + " : this." + generateBuilderFieldName(); + String typeParam = (data.isGenericParamType() && !data.isGenericWildcardParamType() ? "<" + data.getGenericParamType() + ">" : "").replaceAll("\\? extends ", ""); + list.add("\t\t\tthis." + generateBuilderFieldName() + " = " + data.getCollectionGen().generateAddToMap(existing, typeParam, keyParamName, valueParamName) + ";"); + if (data.isValidated()) { + list.add("\t\t\t" + data.getValidationMethodName() + "(this." + data.getPropertyName() + ", \"" + data.getPropertyName() + "\");"); + } + list.add("\t\t\treturn this;"); + list.add("\t\t}"); + list.add(""); + return list; + } + String getBuilderType() { return data.getBuilderGen().generateType(data); } @@ -380,6 +471,61 @@ private String propertyType(String type) { return type; } + + + private String getCollectionOrArrayParamType() { + if (!(data.isCollectionType() || data.isArrayType())) { + throw new IllegalStateException("Property is not a Collection or array."); + } + + if (data.isArrayType()) { + return data.getFieldType().substring(0, data.getFieldType().lastIndexOf('[')); + } + + String paramType = data.getGenericParamType(); + if (data.isGenericWildcardParamType() || paramType.isEmpty()) { + return "Object"; + } + + if (paramType.startsWith("< extends ")) { + return paramType.replaceFirst("< extends ", ""); + } + + return paramType; + } + + private String parseMapKeyType() { + if (!data.isMapType()) { + throw new IllegalStateException("Property is not a Map."); + } + + List mapParamTypes = data.getGenericParamTypes(); + if (mapParamTypes.isEmpty()) { + return "Object"; + } + if (mapParamTypes.size() != 2) { + throw new IllegalStateException("Map should have 2 parameter types"); + } + + return mapParamTypes.get(0); + } + + private String parseMapValueType() { + if (!data.isMapType()) { + throw new IllegalStateException("Property is not a Map."); + } + + List mapParamTypes = data.getGenericParamTypes(); + if (mapParamTypes.isEmpty()) { + return "Object"; + } + if (mapParamTypes.size() != 2) { + throw new IllegalStateException("Map should have 2 parameter types"); + } + + return mapParamTypes.get(1); + } + PropertyData getData() { return data; } diff --git a/src/main/java/org/joda/beans/gen/PropertyParser.java b/src/main/java/org/joda/beans/gen/PropertyParser.java index b85e118a..ea1aa2e1 100644 --- a/src/main/java/org/joda/beans/gen/PropertyParser.java +++ b/src/main/java/org/joda/beans/gen/PropertyParser.java @@ -95,6 +95,7 @@ PropertyGen parse(BeanData beanData, List content, int lineIndex) { data.resolveSetterGen(beanParser.getFile(), lineIndex); data.resolveCopyGen(beanParser.getFile(), lineIndex); data.resolveBuilderGen(); + data.resolveCollectionGen(); data.resolveEqualsHashCodeStyle(beanParser.getFile(), lineIndex); data.resolveToStringStyle(beanParser.getFile(), lineIndex); data.setMetaFieldName(beanParser.getFieldPrefix() + data.getPropertyName()); @@ -126,6 +127,7 @@ PropertyGen parseDerived(BeanData beanData, List content, int lineIndex) data.resolveSetterGen(beanParser.getFile(), lineIndex); data.resolveCopyGen(beanParser.getFile(), lineIndex); data.resolveBuilderGen(); + data.resolveCollectionGen(); data.setMetaFieldName(beanParser.getFieldPrefix() + data.getPropertyName()); parseComments(content, data); return new PropertyGen(data); diff --git a/src/main/java/org/joda/beans/utils/NameUtils.java b/src/main/java/org/joda/beans/utils/NameUtils.java new file mode 100644 index 00000000..379e447d --- /dev/null +++ b/src/main/java/org/joda/beans/utils/NameUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019-present Stephen Colebourne + * + * 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 org.joda.beans.utils; + +import java.util.Locale; + +/** + * Utility methods for working with names of things. + */ +public final class NameUtils { + + private NameUtils() { + + } + + /** + * Capitalize (i.e. upper-case) the first character of a string. + * + * @param string the string + * @return the string with the first character capitalized + */ + public static String capitalize(String string) { + return string.substring(0, 1).toUpperCase(Locale.ENGLISH) + string.substring(1); + } + + /** + * Lower-case the first character of a string. + * + * @param string the string + * @return the string with the first character lower-cased + */ + public static String decapitalize(String string) { + return string.substring(0, 1).toLowerCase(Locale.ENGLISH) + string.substring(1); + } +} diff --git a/src/main/resources/org/joda/beans/gen/guava.ini b/src/main/resources/org/joda/beans/gen/guava.ini index 93f124af..9f32c7f7 100644 --- a/src/main/resources/org/joda/beans/gen/guava.ini +++ b/src/main/resources/org/joda/beans/gen/guava.ini @@ -137,6 +137,36 @@ ImmutableList = ImmutableList.copyOf($value) ImmutableSet = ImmutableSet.copyOf($value) ImmutableSortedSet = ImmutableSortedSet.copyOf($value) +# how to add a new $value (by $key where appropriate) to an $existing collection +[immutable.builder.add] +Collection = ImmutableList.$paramsbuilder().addAll($existing).add($value).build() +List = ImmutableList.$paramsbuilder().addAll($existing).add($value).build() +Set = ImmutableSet.$paramsbuilder().addAll($existing).add($value).build() +SortedSet = ImmutableSortedSet.$paramsbuilder().addAll($existing).add($value).build() +Map = ImmutableMap.$paramsbuilder().putAll($existing).put($key, $value).build() +SortedMap = ImmutableSortedMap.$paramsnaturalOrder().putAll($existing).put($key, $value).build() +EnumSet = ImmutableSet.$paramsbuilder().addAll($existing).add($value).build() +BiMap = ImmutableBiMap.$paramsbuilder().putAll($existing).put($key, $value).build() +Multimap = ImmutableMultimap.$paramsbuilder().putAll($existing).put($key, $value).build() +ListMultimap = ImmutableListMultimap.$paramsbuilder().putAll($existing).put($key, $value).build() +SetMultimap = ImmutableSetMultimap.$paramsbuilder().putAll($existing).put($key, $value).build() +Multiset = ImmutableMultiset.$paramsbuilder().addAll($existing).add($value).build() +SortedMultiset = ImmutableSortedMultiset.$paramsnaturalOrder().addAll($existing).add($value).build() +Table = ImmutableTable.$paramsbuilder().putAll($existing).put($key, $value).build() +ImmutableCollection = ImmutableList.$paramsbuilder().addAll($existing).add($value).build() +ImmutableList = ImmutableList.$paramsbuilder().addAll($existing).add($value).build() +ImmutableSet = ImmutableSet.$paramsbuilder().addAll($existing).add($value).build() +ImmutableSortedSet = ImmutableSortedSet.$paramsbuilder().addAll($existing).add($value).build() +ImmutableMap = ImmutableMap.$paramsbuilder().putAll($existing).put($key, $value).build() +ImmutableSortedMap = ImmutableSortedMap.$paramsnaturalOrder().putAll($existing).put($key, $value).build() +ImmutableBiMap = ImmutableBiMap.$paramsbuilder().putAll($existing).put($key, $value).build() +ImmutableMultimap = ImmutableMultimap.$paramsbuilder().putAll($existing).put($key, $value).build() +ImmutableListMultimap = ImmutableListMultimap.$paramsbuilder().putAll($existing).put($key, $value).build() +ImmutableSetMultimap = ImmutableSetMultimap.$paramsbuilder().putAll($existing).put($key, $value).build() +ImmutableMultiset = ImmutableMultiset.$paramsbuilder().addAll($existing).add($value).build() +ImmutableSortedMultiset = ImmutableSortedMultiset.$paramsnaturalOrder().putAll($existing).put($key, $value).build() +ImmutableTable = ImmutableTable.$paramsbuilder().putAll($existing).put($key, $value).build() + # provide the ability to handle clone-on-get or immutable classes [immutable.get.clone] Date = cloneCast