diff --git a/prj/coherence-core/src/main/java/com/tangosol/util/Extractors.java b/prj/coherence-core/src/main/java/com/tangosol/util/Extractors.java index b526c690baa63..a39c2ea818653 100644 --- a/prj/coherence-core/src/main/java/com/tangosol/util/Extractors.java +++ b/prj/coherence-core/src/main/java/com/tangosol/util/Extractors.java @@ -16,6 +16,7 @@ import com.tangosol.util.extractor.ChainedExtractor; import com.tangosol.util.extractor.ChainedFragmentExtractor; +import com.tangosol.util.extractor.CollectionExtractor; import com.tangosol.util.extractor.FragmentExtractor; import com.tangosol.util.extractor.IdentityExtractor; import com.tangosol.util.extractor.KeyExtractor; @@ -38,6 +39,7 @@ * of {@code Extractor} classes. * * @author lh, hr, as, mf 2018.06.14 + * @author Gunnar Hillert 2024.09.19 */ @SuppressWarnings("rawtypes") public class Extractors @@ -247,6 +249,59 @@ public static ValueExtractor identityCast() return IdentityExtractor.INSTANCE; } + /** + * Returns a {@link CollectionExtractor} that extracts the specified fields + * where extraction occurs in a chain where the result of each + * field extraction is the input to the next extractor. The result + * returned is the result of the final extractor in the chain. + * + * @param fields the field names to extract (if any field name contains a dot '.' + * that field name is split into multiple field names delimiting on + * the dots. + * + * @param the type of the object to extract from + * @param the type of the extracted value + * + * @return a {@link CollectionExtractor} that extracts the value(s) of the specified field(s) + * + * @throws IllegalArgumentException if the fields parameter is {@code null} or an + * empty array + * + * @see CollectionExtractor + * @see ChainedExtractor + */ + public static CollectionExtractor fromCollection(String... fields) + { + return new CollectionExtractor(chained(fields)); + } + + /** + * Returns a {@link CollectionExtractor} that wraps the specified {@link ValueExtractor}s. + *

+ * If the {@code extractors} parameter is a single {@link ValueExtractor} then a + * {@link CollectionExtractor} is returned wrapping that extractor. If the {@code extractors} is + * multiple {@link ValueExtractor} instances in a chain, a {@link CollectionExtractor} is returned + * that wraps a {@link ChainedExtractor} that wraps the chain of {@link ValueExtractor} + * instances + * + * @param extractors the chain of {@link ValueExtractor}s to use to extract the value + * @param the type of the object to extract from + * @param the type of the extracted value + * + * @return a {@link CollectionExtractor} that extracts the value(s) of the specified field(s) + * + * @throws IllegalArgumentException if the fields parameter is {@code null} or an + * empty array + * + * @see CollectionExtractor + * @see ChainedExtractor + * + */ + public static CollectionExtractor fromCollection(ValueExtractor... extractors) + { + return new CollectionExtractor(chained(extractors)); + } + /** * Returns an extractor that extracts the value of the specified index(es) * from a POF encoded binary value. diff --git a/prj/coherence-core/src/main/java/com/tangosol/util/extractor/CollectionExtractor.java b/prj/coherence-core/src/main/java/com/tangosol/util/extractor/CollectionExtractor.java new file mode 100644 index 0000000000000..8176a373971c0 --- /dev/null +++ b/prj/coherence-core/src/main/java/com/tangosol/util/extractor/CollectionExtractor.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl. + */ +package com.tangosol.util.extractor; + +import com.tangosol.io.ExternalizableLite; +import com.tangosol.io.pof.PofReader; +import com.tangosol.io.pof.PofWriter; +import com.tangosol.io.pof.PortableObject; + +import com.tangosol.util.ValueExtractor; + +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Collection Extractor is used to extract values from Collections using the provided {@link ValueExtractor}. + * Important Note: + * + *

    + *
  • If the {@link ValueExtractor} is null, an {@link IllegalStateException} is raised. + *
  • If the provided {@link Collection} is null or empty, an empty {@link List} is returned. + *
+ * + * @author Gunnar Hillert 2024.08.28 + * @param – the type of the value to extract from + * @param - the type of value that will be extracted + */ +public class CollectionExtractor + extends AbstractExtractor, List> + implements ExternalizableLite, PortableObject + { + + // ----- constructors --------------------------------------------------- + + /** + * Default constructor (necessary for the {@link ExternalizableLite} interface). + */ + public CollectionExtractor() + { + } + + /** + * Construct a CollectionExtractor based on the specified {@link ValueExtractor}. + * + * @param extractor the ValueExtractor + */ + public CollectionExtractor(ValueExtractor extractor) + { + if (extractor == null) + { + throw new IllegalArgumentException("The CollectionExtractor requires a ValueExtractor to be specified"); + } + m_extractor = extractor; + } + + /** + * Construct a CollectionExtractor based on the specified {@link ValueExtractor}. + * + * @param extractor the ValueExtractor + * @param nTarget one of the {@link #VALUE} or {@link #KEY} values + */ + @JsonbCreator + public CollectionExtractor(@JsonbProperty("extractor") + ValueExtractor extractor, + @JsonbProperty("target") + int nTarget) + { + this(extractor); + + azzert(nTarget == VALUE || nTarget == KEY, String.format( + "nTarget must be either %s or %s", VALUE, KEY)); + m_nTarget = nTarget; + } + + // ----- CollectionExtractor methods --------------------------------------- + + /** + * Extract the value from the passed {@link Collection} using the underlying extractor. + * If the {@link ValueExtractor} is null, an {@link IllegalStateException} is raised. + * If the provided {@link Collection} is null or empty, an empty {@link List} is returned. + */ + @Override + public List extract(Collection target) + { + if (m_extractor == null) + { + throw new IllegalStateException("The CollectionExtractor requires a ValueExtractor to be specified"); + } + List results = new ArrayList<>(); + if (target == null || target.isEmpty()) + { + return results; + } + + for (T res : target) + { + results.add(m_extractor.extract(res)); + } + return results; + } + + @Override + public ValueExtractor, List> fromKey() + { + return new CollectionExtractor(m_extractor, KEY); + } + +// ----- CanonicallyNamed interface ------------------------------------- + + /** + * Compute a canonical name. + * + * @return canonical name. + */ + @Override + public String getCanonicalName() + { + if (m_sNameCanon == null) + { + m_sNameCanon = m_extractor.getCanonicalName(); + } + return m_sNameCanon; + } + + // ----- ExternalizableLite interface ----------------------------------- + + + + @Override + public void readExternal(DataInput in) + throws IOException + { + m_extractor = readObject(in); + m_nTarget = readInt(in); + } + + @Override + public void writeExternal(DataOutput out) + throws IOException + { + writeObject(out, m_extractor); + writeInt(out, m_nTarget); + } + + + // ----- PortableObject interface --------------------------------------- + + @Override + public void readExternal(PofReader in) + throws IOException + { + m_extractor = in.readObject(0); + m_nTarget = in.readInt(1); + } + + @Override + public void writeExternal(PofWriter out) + throws IOException + { + out.writeObject(0, m_extractor); + out.writeInt(1, m_nTarget); + } + + // ----- data fields ---------------------------------------------------- + + /** + * The underlying ValueExtractor. + */ + @JsonbProperty("extractor") + protected ValueExtractor m_extractor; + + } diff --git a/prj/coherence-core/src/main/resources/coherence-pof-config.xml b/prj/coherence-core/src/main/resources/coherence-pof-config.xml index 708926f117d99..9ce06b9890fdb 100644 --- a/prj/coherence-core/src/main/resources/coherence-pof-config.xml +++ b/prj/coherence-core/src/main/resources/coherence-pof-config.xml @@ -682,6 +682,11 @@ descriptor: pof-config.xsd. com.tangosol.util.UniversalManipulator + + 198 + com.tangosol.util.extractor.CollectionExtractor + + diff --git a/prj/coherence-testing-data/src/main/java/data/collectionExtractor/City.java b/prj/coherence-testing-data/src/main/java/data/collectionExtractor/City.java new file mode 100644 index 0000000000000..33f52eb19b4e6 --- /dev/null +++ b/prj/coherence-testing-data/src/main/java/data/collectionExtractor/City.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl. + */ +package data.collectionExtractor; + +import com.tangosol.io.ExternalizableLite; + +import com.tangosol.io.pof.PofReader; +import com.tangosol.io.pof.PofWriter; +import com.tangosol.io.pof.PortableObject; + +import java.io.IOException; +import java.io.Serializable; + +import java.util.Objects; + +/** + * A class representing a City. + * + * @author Gunnar Hillert 2024.09.10 + */ +public class City implements Serializable, PortableObject, ExternalizableLite + { + private String name; + private int population; + + public City() + { + } + + public City(String name, int population) + { + this.name = name; + this.population = population; + } + + public String getName() + { + return name; + } + public void setName(String name) + { + this.name = name; + } + + public int getPopulation() + { + return population; + } + + public void setPopulation(int population) + { + this.population = population; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof City city)) return false; + return population == city.population && Objects.equals(name, city.name); + } + + @Override + public int hashCode() + { + return Objects.hash(name, population); + } + + public String toString() + { + return "Country{" + + "name='" + name + '\'' + + ", population=" + population + + '}'; + } + + public void readExternal(PofReader in) throws IOException + { + name = in.readString(0); + population = in.readInt(1); + } + + public void writeExternal(PofWriter out) throws IOException + { + out.writeString(0, name); + out.writeInt( 1, population); + } + } diff --git a/prj/coherence-testing-data/src/main/java/data/collectionExtractor/Country.java b/prj/coherence-testing-data/src/main/java/data/collectionExtractor/Country.java new file mode 100644 index 0000000000000..33d8e632e2a6f --- /dev/null +++ b/prj/coherence-testing-data/src/main/java/data/collectionExtractor/Country.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl. + */ +package data.collectionExtractor; + +import com.tangosol.io.ExternalizableLite; +import com.tangosol.io.pof.PofReader; +import com.tangosol.io.pof.PofWriter; +import com.tangosol.io.pof.PortableObject; + +import java.io.IOException; +import java.io.Serializable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; + +/** + * A class representing a Country. + * + * @author Gunnar Hillert 2024.09.10 + */ +public class Country implements Serializable, PortableObject, ExternalizableLite + { + private String name; + + private int area; + private int population; + + private Collection cities = new ArrayList<>(); + + public Country() + { + } + + public Country(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + public void setName(String name) + { + this.name = name; + } + + public int getArea() + { + return area; + } + + public void setArea(int area) + { + this.area = area; + } + + public int getPopulation() + { + return population; + } + + public void setPopulation(int population) + { + this.population = population; + } + + public Collection getCities() + { + return cities; + } + + public void setCities(Collection cities) + { + this.cities = cities; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof Country country)) return false; + return area == country.area && population == country.population && Objects.equals(name, country.name) && Objects.equals(cities, country.cities); + } + + @Override + public int hashCode() + { + return Objects.hash(name, area, population, cities); + } + + public String toString() + { + return "Country{" + + "name='" + name + '\'' + + ", area='" + area + '\'' + + ", population=" + population + + ", cities=" + cities.size() + + '}'; + } + + public void readExternal(PofReader in) throws IOException + { + name = in.readString(0); + area = in.readInt(1); + population = in.readInt(2); + cities = in.readCollection(3, new ArrayList()); + } + + public void writeExternal(PofWriter out) throws IOException + { + out.writeString(0, name); + out.writeInt(1, area); + out.writeInt(2, population); + out.writeObject(3, cities); + } + + public Country addCity(City city) + { + getCities().add(city); + return this; + } + } diff --git a/prj/test/functional/extractor/src/main/java/extractor/ExtractorTests.java b/prj/test/functional/extractor/src/main/java/extractor/ExtractorTests.java index 83847e2a15aa3..ae912a91516a8 100644 --- a/prj/test/functional/extractor/src/main/java/extractor/ExtractorTests.java +++ b/prj/test/functional/extractor/src/main/java/extractor/ExtractorTests.java @@ -1,13 +1,11 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. + * Copyright (c) 2000, 2024, Oracle and/or its affiliates. * * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ - package extractor; - import com.tangosol.io.ExternalizableLite; import com.tangosol.io.pof.PofReader; @@ -16,17 +14,20 @@ import com.tangosol.net.NamedCache; +import com.tangosol.util.Extractors; import com.tangosol.util.Filter; +import com.tangosol.util.Filters; import com.tangosol.util.ValueExtractor; import com.tangosol.util.WrapperException; import com.tangosol.util.extractor.AbstractExtractor; +import com.tangosol.util.extractor.CollectionExtractor; import com.tangosol.util.extractor.DeserializationAccelerator; import com.tangosol.util.extractor.IdentityExtractor; import com.tangosol.util.extractor.MultiExtractor; import com.tangosol.util.extractor.ReflectionExtractor; - import com.tangosol.util.extractor.UniversalExtractor; + import com.tangosol.util.filter.EqualsFilter; import com.tangosol.util.filter.LessEqualsFilter; @@ -36,6 +37,8 @@ import data.Person; +import data.collectionExtractor.City; +import data.collectionExtractor.Country; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -46,16 +49,23 @@ import java.util.Calendar; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.hasItems; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * A collection of functional tests for the {@link ValueExtractor}. * * @author oew 01/22/2007 + * @author Gunnar Hillert 09/12/2024 */ public class ExtractorTests extends AbstractFunctionalTest @@ -109,6 +119,65 @@ public void testReflection() cache.destroy(); } + /** + * Test for {@link CollectionExtractor}. + */ + @Test + public void testCollection() + { + NamedCache cache = getNamedCache(); + + Country usa = new Country("USA"); + usa.setArea(3_796_742); + usa.setPopulation(334_914_895); + usa.addCity(new City("New York", 8_258_035)) + .addCity(new City("Los Angeles", 3_820_914)) + .addCity(new City("Chicago", 2_664_452)); + + Country germany = new Country("Germany"); + germany.setArea(357_569); + germany.setPopulation(82_719_540); + germany.addCity(new City("Berlin", 3_677_472)) + .addCity(new City("Hamburg", 1_906_411)) + .addCity(new City("Munich", 1_487_708)); + + Country taiwan = new Country("Taiwan"); + taiwan.setArea(36_197); + taiwan.setPopulation(23_894_394); + taiwan.addCity(new City("New Taipei", 3_974_911)) + .addCity(new City("Kaohsiung", 2_778_992)) + .addCity(new City("Taichung", 2_759_887)) + .addCity(new City("Taipei", 2_696_316)); + + cache.put("us", usa); + cache.put("de", germany); + cache.put("tw", taiwan); + + cache.values(Filters.in(Extractors.extract("name"), Set.of("USA", "Germany"))).size(); + + ValueExtractor countryNameExtractor = Extractors.extract("name"); + + Filter countryFilter = Filters.in(Extractors.extract("name"), "USA", "Germany"); + + ValueExtractor> listOfCitiesExtractor = Extractors.extract("cities"); + ValueExtractor cityNameExtractor = Extractors.extract("name"); + CollectionExtractor cityExtractor = new CollectionExtractor<>(cityNameExtractor); + + ValueExtractor> chainedExtractor = Extractors.chained(listOfCitiesExtractor, cityExtractor); + + List> cityNames = cache.stream(countryFilter, chainedExtractor).toList(); + + assertEquals("Expected 2 results (Countries) but got " + cityNames.size(), 2, cityNames.size()); + + List justCities = cityNames.stream().flatMap(list -> list.stream()).toList(); + + assertEquals("Expected 6 cities but got " + justCities.size(), 6, justCities.size()); + + assertThat(justCities, hasItems("Berlin", "Hamburg", "Munich", "Los Angeles", "New York", "Chicago")); + + cache.destroy(); + } + /** * Test for {@link DeserializationAccelerator}. */ diff --git a/prj/test/functional/extractor/src/main/resources/extractor/extractor-pof-config.xml b/prj/test/functional/extractor/src/main/resources/extractor/extractor-pof-config.xml index 21591f6057adb..3119d7c7cd108 100644 --- a/prj/test/functional/extractor/src/main/resources/extractor/extractor-pof-config.xml +++ b/prj/test/functional/extractor/src/main/resources/extractor/extractor-pof-config.xml @@ -1,6 +1,6 @@