From 4f6773b244e7056c0ac395ac9a897b7afade906e Mon Sep 17 00:00:00 2001 From: Sima Zhu <48036328+sima-zhu@users.noreply.github.com> Date: Fri, 1 May 2020 17:59:16 -0700 Subject: [PATCH] Field builder and Field annotation. (#10500) --- .../azure-search-documents/CHANGELOG.md | 3 + .../azure/search/documents/FieldBuilder.java | 262 ++++++++++++++++++ .../search/documents/indexes/FieldIgnore.java | 17 ++ .../indexes/SearchableFieldProperty.java | 82 ++++++ .../indexes/SimpleFieldProperty.java | 54 ++++ .../documents/indexes/package-info.java | 8 + .../search/documents/models/ComplexField.java | 61 ++++ .../search/documents/models/FieldBase.java | 49 ++++ .../documents/models/SearchableField.java | 146 ++++++++++ .../search/documents/models/SimpleField.java | 153 ++++++++++ .../search/documents/FieldBuilderTest.java | 155 +++++++++++ .../models/AddressCircularDependencies.java | 32 +++ .../test/environment/models/Hotel.java | 8 + .../test/environment/models/HotelAddress.java | 7 + .../models/HotelAnalyzerException.java | 33 +++ .../models/HotelCircularDependencies.java | 52 ++++ .../models/HotelSearchException.java | 35 +++ .../HotelSearchableExceptionOnList.java | 35 +++ .../models/HotelTwoDimensional.java | 34 +++ .../models/HotelWithEmptyInSynonymMaps.java | 36 +++ .../setup/AzureSearchResources.java | 4 + 21 files changed, 1266 insertions(+) create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/FieldBuilder.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/FieldIgnore.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SearchableFieldProperty.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SimpleFieldProperty.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/package-info.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/ComplexField.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/FieldBase.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SearchableField.java create mode 100644 sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SimpleField.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/FieldBuilderTest.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/AddressCircularDependencies.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAnalyzerException.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelCircularDependencies.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchException.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchableExceptionOnList.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelTwoDimensional.java create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelWithEmptyInSynonymMaps.java diff --git a/sdk/search/azure-search-documents/CHANGELOG.md b/sdk/search/azure-search-documents/CHANGELOG.md index a44db5e21aa05..27eaf4c465395 100644 --- a/sdk/search/azure-search-documents/CHANGELOG.md +++ b/sdk/search/azure-search-documents/CHANGELOG.md @@ -5,6 +5,9 @@ - Changed Azure Search service version from `2019-05-06` to `2019-05-06-Preview` - Changed `createOrUpdate` and `delete` APIs in `SearchServiceClient` to use boolean `onlyIfUnchanged` instead of `MatchConditions`. - Updated reactor core to `3.3.5.RELEASE`. +- Added helper class `FieldBuilder` which converts a strongly-typed model class to `List`. +- Added annotations `FieldIgnore`, `SimpleFieldProperty`, and `SearchableFieldProperty` to define the `Field` on model properties. +- Added fluent class `SimpleField`, `SearchableField`, and `ComplexField` to build `Field`. ## 1.0.0-beta.2 (2020-04-06) diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/FieldBuilder.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/FieldBuilder.java new file mode 100644 index 0000000000000..003c6f76388d7 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/FieldBuilder.java @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents; + +import com.azure.core.util.logging.ClientLogger; +import com.azure.search.documents.indexes.FieldIgnore; +import com.azure.search.documents.indexes.SearchableFieldProperty; +import com.azure.search.documents.indexes.SimpleFieldProperty; +import com.azure.search.documents.models.AnalyzerName; +import com.azure.search.documents.models.DataType; +import com.azure.search.documents.models.Field; +import com.azure.search.documents.models.GeoPoint; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.stream.Collectors; + +/** + * Helper to convert model class to Search {@link Field fields}. + */ +public final class FieldBuilder { + private static final int MAX_DEPTH = 10000; + private static final Map, DataType> SUPPORTED_NONE_PARAMETERIZED_TYPE = new HashMap<>(); + + static { + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(Integer.class, DataType.EDM_INT32); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(int.class, DataType.EDM_INT32); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(Long.class, DataType.EDM_INT64); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(long.class, DataType.EDM_INT64); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(Double.class, DataType.EDM_DOUBLE); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(double.class, DataType.EDM_DOUBLE); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(Boolean.class, DataType.EDM_BOOLEAN); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(boolean.class, DataType.EDM_BOOLEAN); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(String.class, DataType.EDM_STRING); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(Date.class, DataType.EDM_DATE_TIME_OFFSET); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(OffsetDateTime.class, DataType.EDM_DATE_TIME_OFFSET); + SUPPORTED_NONE_PARAMETERIZED_TYPE.put(GeoPoint.class, DataType.EDM_GEOGRAPHY_POINT); + } + + private static final List> UNSUPPORTED_TYPES = Arrays.asList(Byte.class, + CharSequence.class, + Character.class, + char.class, + Float.class, + float.class, + Short.class, + short.class); + + /** + * Creates a collection of {@link Field} objects corresponding to the properties of the type supplied. + * + * @param modelClass The class for which fields will be created, based on its properties. + * @param The generic type of the model class. + * @return A collection of fields. + */ + public static List build(Class modelClass) { + ClientLogger logger = new ClientLogger(FieldBuilder.class); + return build(modelClass, new Stack<>(), logger); + } + + /** + * Recursive class to build complex data type. + * + * @param currentClass Current class to be built. + * @param classChain A class chain from {@code modelClass} to prior of {@code currentClass}. + * @param logger {@link ClientLogger}. + * @return A list of {@link Field} that currentClass is built to. + */ + private static List build(Class currentClass, Stack> classChain, ClientLogger logger) { + if (classChain.contains(currentClass)) { + logger.warning(String.format("There is circular dependencies %s, %s", classChain, currentClass)); + return null; + } + if (classChain.size() > MAX_DEPTH) { + throw logger.logExceptionAsError(new RuntimeException( + "The dependency graph is too deep. Please review your schema.")); + } + classChain.push(currentClass); + List searchFields = Arrays.stream(currentClass.getDeclaredFields()) + .filter(classField -> !classField.isAnnotationPresent(FieldIgnore.class)) + .map(classField -> buildField(classField, classChain, logger)) + .collect(Collectors.toList()); + classChain.pop(); + return searchFields; + } + + private static Field buildField(java.lang.reflect.Field classField, Stack> classChain, + ClientLogger logger) { + Type type = classField.getGenericType(); + + if (SUPPORTED_NONE_PARAMETERIZED_TYPE.containsKey(type)) { + return buildNoneParameterizedType(classField, logger); + } + if (isArrayOrList(type)) { + return buildCollectionField(classField, classChain, logger); + } + List childFields = build((Class) type, classChain, logger); + Field searchField = convertToBasicSearchField(classField, logger); + searchField.setFields(childFields); + return searchField; + } + + private static Field buildNoneParameterizedType(java.lang.reflect.Field classField, + ClientLogger logger) { + Field searchField = convertToBasicSearchField(classField, logger); + return enrichWithAnnotation(searchField, classField, logger); + } + + + private static boolean isArrayOrList(Type type) { + return type.getClass().isArray() || isList(type); + } + + private static boolean isList(Type type) { + if (!(type instanceof ParameterizedType)) { + return false; + } + + Type rawType = ((ParameterizedType) type).getRawType(); + return List.class.isAssignableFrom((Class) rawType); + } + + private static Field buildCollectionField(java.lang.reflect.Field classField, + Stack> classChain, ClientLogger logger) { + Type componentOrElementType = getComponentOrElementType(classField.getGenericType(), logger); + validateType(componentOrElementType, true, logger); + if (SUPPORTED_NONE_PARAMETERIZED_TYPE.containsKey(componentOrElementType)) { + Field searchField = convertToBasicSearchField(classField, logger); + return enrichWithAnnotation(searchField, classField, logger); + } + List childFields = build((Class) componentOrElementType, classChain, logger); + Field searchField = convertToBasicSearchField(classField, logger); + searchField.setFields(childFields); + return searchField; + } + + private static Type getComponentOrElementType(Type arrayOrListType, ClientLogger logger) { + if (arrayOrListType.getClass().isArray()) { + return arrayOrListType.getClass().getComponentType(); + } + if (isList(arrayOrListType)) { + ParameterizedType pt = (ParameterizedType) arrayOrListType; + return pt.getActualTypeArguments()[0]; + } + throw logger.logExceptionAsError(new RuntimeException(String.format( + "Collection type %s is not supported.", arrayOrListType.getTypeName()))); + } + + private static Field convertToBasicSearchField(java.lang.reflect.Field classField, + ClientLogger logger) { + Field searchField = new Field(); + searchField.setName(classField.getName()); + DataType dataType = covertToDataType(classField.getGenericType(), false, logger); + searchField.setType(dataType) + .setKey(false) + .setSearchable(false) + .setFacetable(false) + .setHidden(false) + .setFilterable(false) + .setSortable(false); + return searchField; + } + + private static Field enrichWithAnnotation(Field searchField, java.lang.reflect.Field classField, + ClientLogger logger) { + if (classField.isAnnotationPresent(SimpleFieldProperty.class) + && classField.isAnnotationPresent(SearchableFieldProperty.class)) { + throw logger.logExceptionAsError(new IllegalArgumentException( + String.format("@SimpleFieldProperty and @SearchableFieldProperty cannot be present simultaneously " + + "for %s", classField.getName()))); + } + if (classField.isAnnotationPresent(SimpleFieldProperty.class)) { + SimpleFieldProperty simpleFieldPropertyAnnotation = + classField.getDeclaredAnnotation(SimpleFieldProperty.class); + searchField.setSearchable(false) + .setSortable(simpleFieldPropertyAnnotation.isSortable()) + .setFilterable(simpleFieldPropertyAnnotation.isFilterable()) + .setFacetable(simpleFieldPropertyAnnotation.isFacetable()) + .setKey(simpleFieldPropertyAnnotation.isKey()) + .setHidden(simpleFieldPropertyAnnotation.isHidden()); + } else if (classField.isAnnotationPresent(SearchableFieldProperty.class)) { + if (!searchField.getType().equals(DataType.EDM_STRING) + && !searchField.getType().equals(DataType.collection(DataType.EDM_STRING))) { + throw logger.logExceptionAsError(new RuntimeException(String.format("SearchFieldProperty can only" + + " be used on string properties. Property %s returns a %s value.", + classField.getName(), searchField.getType()))); + } + SearchableFieldProperty searchableFieldPropertyAnnotation = + classField.getDeclaredAnnotation(SearchableFieldProperty.class); + searchField.setSearchable(true) + .setSortable(searchableFieldPropertyAnnotation.isSortable()) + .setFilterable(searchableFieldPropertyAnnotation.isFilterable()) + .setFacetable(searchableFieldPropertyAnnotation.isFacetable()) + .setKey(searchableFieldPropertyAnnotation.isKey()) + .setHidden(searchableFieldPropertyAnnotation.isHidden()); + String analyzer = searchableFieldPropertyAnnotation.analyzer(); + String searchAnalyzer = searchableFieldPropertyAnnotation.searchAnalyzer(); + String indexAnalyzer = searchableFieldPropertyAnnotation.indexAnalyzer(); + if (!analyzer.isEmpty() && (!searchAnalyzer.isEmpty() || !indexAnalyzer.isEmpty())) { + throw logger.logExceptionAsError(new RuntimeException( + "Please specify either analyzer or both searchAnalyzer and indexAnalyzer.")); + } + if (!searchableFieldPropertyAnnotation.analyzer().isEmpty()) { + searchField.setAnalyzer(AnalyzerName.fromString((searchableFieldPropertyAnnotation.analyzer()))); + } + if (!searchableFieldPropertyAnnotation.searchAnalyzer().isEmpty()) { + searchField.setAnalyzer(AnalyzerName.fromString((searchableFieldPropertyAnnotation.searchAnalyzer()))); + } + if (!searchableFieldPropertyAnnotation.indexAnalyzer().isEmpty()) { + searchField.setAnalyzer(AnalyzerName.fromString((searchableFieldPropertyAnnotation.indexAnalyzer()))); + } + if (searchableFieldPropertyAnnotation.synonymMaps().length != 0) { + List synonymMaps = Arrays.stream(searchableFieldPropertyAnnotation.synonymMaps()) + .filter(synonym -> !synonym.trim().isEmpty()).collect(Collectors.toList()); + searchField.setSynonymMaps(synonymMaps); + } + } + return searchField; + } + + private static void validateType(Type type, boolean hasArrayOrCollectionWrapped, ClientLogger logger) { + if (!(type instanceof ParameterizedType)) { + if (UNSUPPORTED_TYPES.contains(type)) { + throw logger.logExceptionAsError(new IllegalArgumentException(String.format("%s is not supported", + type.getTypeName()))); + } + return; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (Map.class.isAssignableFrom((Class) parameterizedType.getRawType())) { + throw logger.logExceptionAsError(new IllegalArgumentException("Map and its subclasses are not supported")); + } + if (hasArrayOrCollectionWrapped) { + throw logger.logExceptionAsError(new IllegalArgumentException( + "Only single-dimensional array is supported.")); + } + if (!List.class.isAssignableFrom((Class) parameterizedType.getRawType())) { + throw logger.logExceptionAsError(new IllegalArgumentException( + String.format("Collection type %s is not supported", type.getTypeName()))); + } + } + + private static DataType covertToDataType(Type type, boolean hasArrayOrCollectionWrapped, ClientLogger logger) { + validateType(type, hasArrayOrCollectionWrapped, logger); + if (SUPPORTED_NONE_PARAMETERIZED_TYPE.containsKey(type)) { + return SUPPORTED_NONE_PARAMETERIZED_TYPE.get(type); + } + if (isArrayOrList(type)) { + Type componentOrElementType = getComponentOrElementType(type, logger); + return DataType.collection(covertToDataType(componentOrElementType, true, logger)); + } + return DataType.EDM_COMPLEX_TYPE; + } +} diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/FieldIgnore.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/FieldIgnore.java new file mode 100644 index 0000000000000..d70ca924d4706 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/FieldIgnore.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.indexes; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation that indicates the field is to be ignored by converting to SearchField. + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FieldIgnore { +} diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SearchableFieldProperty.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SearchableFieldProperty.java new file mode 100644 index 0000000000000..69a5546d14980 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SearchableFieldProperty.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.indexes; + +import com.azure.search.documents.models.AnalyzerName; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotation is to indicate whether the field is a searchable field. The boolean field of isSearchable + * defaults to true if use the annotation. + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SearchableFieldProperty { + /** + * Optional arguments defines whether the field is a key field or not. + * + * @return True if it is the key of SearchField, and false by default for non-key field. + */ + boolean isKey() default false; + + /** + * Optional arguments defines whether the field is hidden or not. + * + * @return True if it is not retrievable, and false by default for retrievable field. + */ + boolean isHidden() default false; + + /** + * Optional arguments defines whether the field is facetable or not. + * + * @return True if it is facetable, and false by default for non-facetable field. + */ + boolean isFacetable() default false; + + /** + * Optional arguments defines whether the field is sortable or not. + * + * @return True if it is sortable, and false by default for non-sortable field. + */ + boolean isSortable() default false; + + /** + * Optional arguments defines whether the field is filterable or not. + * + * @return True if it is filterable, and false by default for non-filterable field. + */ + boolean isFilterable() default false; + + /** + * Optional arguments defines the name of the analyzer used for the field. + * + * @return {@link AnalyzerName} String value. Or default to "null" String type. + */ + String analyzer() default ""; + + /** + * Optional arguments defines the name of the search analyzer used for the field. + * + * @return {@link AnalyzerName} String value. Or default to an empty String. + */ + String searchAnalyzer() default ""; + + /** + * Optional arguments defines the name of the analyzer used for the field. + * + * @return {@link AnalyzerName} String value. Or default to an empty String. + */ + String indexAnalyzer() default ""; + + /** + * Optional arguments defines the array of synonymMaps used for the field. + * + * @return An array of synonym map values. Or default to empty string array. + */ + String[] synonymMaps() default {}; +} diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SimpleFieldProperty.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SimpleFieldProperty.java new file mode 100644 index 0000000000000..c8d7db1a541a2 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/SimpleFieldProperty.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.indexes; + +import com.azure.search.documents.models.Field; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotation is to indicate whether the field is a simple field. This annotation can only set boolean field of + * {@link Field}. {@code isSearchable} will set to {@code false}. + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SimpleFieldProperty { + /** + * Optional arguments defines whether the field is a key field or not. + * + * @return True if it is the key of SearchField, and false by default for non-key field. + */ + boolean isKey() default false; + + /** + * Optional arguments defines whether the field is hidden or not. + * + * @return True if it is not retrievable, and false by default for retrievable field. + */ + boolean isHidden() default false; + + /** + * Optional arguments defines whether the field is facetable or not. + * + * @return True if it is facetable, and false by default for non-facetable field. + */ + boolean isFacetable() default false; + + /** + * Optional arguments defines whether the field is sortable or not. + * + * @return True if it is sortable, and false by default for non-sortable field. + */ + boolean isSortable() default false; + + /** + * Optional arguments defines whether the field is filterable or not. + * + * @return True if it is filterable, and false by default for non-filterable field. + */ + boolean isFilterable() default false; +} diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/package-info.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/package-info.java new file mode 100644 index 0000000000000..f3976eff94c23 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Package containing the annotations used by FieldBuilder which help convert + * model class to a list of Field classes for building Index. + */ +package com.azure.search.documents.indexes; diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/ComplexField.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/ComplexField.java new file mode 100644 index 0000000000000..1c9297785a179 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/ComplexField.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.models; + +import java.util.List; + +/** + * A helper Field model to build a complex field which uses {@code DataType.EDM_COMPLEX_TYPE} or collection of + * {@code DataType.EDM_COMPLEX_TYPE}. + */ +public class ComplexField extends FieldBase { + private List fields; + + /** + * Initializes a new instance of the {@link ComplexField} class. + * + * @param name The name of the field, which must be unique within the index or parent field. + * @param collection Whether the field is a collection of strings. + */ + public ComplexField(String name, boolean collection) { + super(name, collection ? DataType.collection(DataType.EDM_COMPLEX_TYPE) : DataType.EDM_COMPLEX_TYPE); + } + + /** + * Gets a collection of {@link SimpleField} or {@link ComplexField} child fields. + * + * @return The list of sub-fields. + */ + public List getFields() { + return fields; + } + + /** + * Sets a collection of {@link SimpleField} or {@link ComplexField} child fields. + * + * @param fields The list of sub-fields. + * @return The {@link ComplexField} object itself. + */ + public ComplexField setFields(List fields) { + this.fields = fields; + return this; + } + + /** + * Convert ComplexField to {@link Field}. + * + * @return The {@link Field} object. + */ + public Field build() { + return new Field().setName(super.getName()) + .setType(super.getDataType()) + .setFields(fields) + .setKey(false) + .setFilterable(false) + .setSortable(false) + .setHidden(false) + .setSearchable(false) + .setFacetable(false); + } +} diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/FieldBase.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/FieldBase.java new file mode 100644 index 0000000000000..9970a31ab33c7 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/FieldBase.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.models; + +import com.azure.core.util.CoreUtils; +import com.azure.core.util.logging.ClientLogger; + +import java.util.Objects; + +/** + * Base field type for helper classes to more easily create a {@link Index}. + */ +public abstract class FieldBase { + private final ClientLogger logger = new ClientLogger(FieldBase.class); + private final String name; + private final DataType dataType; + + /** + * Initializes a new instance of the {@link FieldBase} class. + * @param name The name of the field, which must be unique within the index or parent field. + * @param dataType The data type of the field. + */ + protected FieldBase(String name, DataType dataType) { + if (CoreUtils.isNullOrEmpty(name)) { + throw logger.logExceptionAsError(new IllegalArgumentException("The name of the field cannot be null")); + } + this.dataType = Objects.requireNonNull(dataType, "'dataType' cannot be null."); + this.name = name; + } + + /** + * Get the name of the field. + * + * @return The name of the field. + */ + public String getName() { + return name; + } + + /** + * Get the {@link DataType} of the field. + * + * @return The data type of the field. + */ + public DataType getDataType() { + return dataType; + } +} diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SearchableField.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SearchableField.java new file mode 100644 index 0000000000000..42fa06b254a12 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SearchableField.java @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.models; + +import java.util.List; + +/** + * A helper Field model to build a searchable {@link Field}. + */ +public class SearchableField extends SimpleField { + private AnalyzerName analyzer; + private AnalyzerName searchAnalyzer; + private AnalyzerName indexAnalyzer; + private List synonymMapNames; + + /** + * Initializes a new instance of the {@link SearchableField} class. + * + * @param name The name of the field, which must be unique within the index or parent field. + * @param collection Whether the field is a collection of strings. + * @throws NullPointerException when {@code name} is null. + */ + public SearchableField(String name, boolean collection) { + super(name, DataType.EDM_STRING, collection); + } + + /** + * Gets the name of the language analyzer. This property cannot be set when either {@code searchAnalyzer} or + * {@code indexAnalyzer} are set. Once the analyzer is chosen, it cannot be changed for the field in the index. + * + * @return The {@link AnalyzerName} used for analyzer. + */ + public AnalyzerName getAnalyzer() { + return analyzer; + } + + /** + * Sets the name of the language analyzer. This property cannot be set when either {@code searchAnalyzer} or + * {@code indexAnalyzer} are set. Once the analyzer is chosen, it cannot be changed for the field in the index. + * + * @param analyzer The {@link AnalyzerName} used for analyzer. + * @return The SearchableField object itself. + */ + public SearchableField setAnalyzer(AnalyzerName analyzer) { + this.analyzer = analyzer; + return this; + } + + /** + * Gets the name of the language analyzer for searching. This property must be set together with + * {@code indexAnalyzer}, and cannot be set when {@code analyzer} is set. Once the analyzer is chosen, it cannot be + * changed for the field in the index. + * + * @return The {@link AnalyzerName} used for search analyzer. + */ + public AnalyzerName getSearchAnalyzer() { + return searchAnalyzer; + } + + /** + * Sets the name of the language analyzer for searching. This property must be set together with + * {@code indexAnalyzer}, and cannot be set when {@code analyzer} is set. Once the analyzer is chosen, it cannot be + * changed for the field in the index. + * + * @param searchAnalyzer The {@link AnalyzerName} used for search analyzer. + * @return The SearchableField object itself. + */ + public SearchableField setSearchAnalyzer(AnalyzerName searchAnalyzer) { + this.searchAnalyzer = searchAnalyzer; + return this; + } + + /** + * Gets the name of the language analyzer for indexing. This property must be set together with + * {@code searchAnalyzer}, and cannot be set when {@code analyzer} is set. Once the analyzer is chosen, it cannot be + * changed for the field in the index. + * + * @return The {@link AnalyzerName} used for index analyzer. + */ + public AnalyzerName getIndexAnalyzer() { + return indexAnalyzer; + } + + /** + * Gets the name of the language analyzer for indexing. This property must be set together with + * {@code searchAnalyzer}, and cannot be set when {@code analyzer} is set. Once the analyzer is chosen, it cannot be + * changed for the field in the index. + * + * @param indexAnalyzer The {@link AnalyzerName} used for index analyzer. + * @return The SearchableField object itself. + */ + public SearchableField setIndexAnalyzer(AnalyzerName indexAnalyzer) { + this.indexAnalyzer = indexAnalyzer; + return this; + } + + /** + * Gets a list of names of synonym maps to associate with this field. + * Currently, only one synonym map per field is supported. + * + * Assigning a synonym map to a field ensures that query terms targeting that field are expanded at query-time using + * the rules in the synonym map. This attribute can be changed on existing fields. + * + * @return List of names of synonym maps to associate with this field. + */ + public List getSynonymMapNames() { + return synonymMapNames; + } + + /** + * Sets a list of names of synonym maps to associate with this field. + * Currently, only one synonym map per field is supported. + * + * Assigning a synonym map to a field ensures that query terms targeting that field are expanded at query-time using + * the rules in the synonym map. This attribute can be changed on existing fields. + * + * @param synonymMapNames list of names of synonym maps to associate with this field. + * @return The SearchableField object itself. + */ + public SearchableField setSynonymMapNames(List synonymMapNames) { + this.synonymMapNames = synonymMapNames; + return this; + } + + /** + * Convert SearchableField to {@link Field}. + * + * @return The {@link Field} object. + */ + public Field build() { + return new Field() + .setName(super.getName()) + .setType(super.getDataType()) + .setSearchable(true) + .setKey(super.isKey()) + .setSortable(super.isSortable()) + .setFilterable(super.isFilterable()) + .setHidden(super.isHidden()) + .setFacetable(super.isFacetable()) + .setAnalyzer(this.analyzer) + .setSearchAnalyzer(this.searchAnalyzer) + .setIndexAnalyzer(this.indexAnalyzer) + .setSynonymMaps(this.synonymMapNames); + } +} diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SimpleField.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SimpleField.java new file mode 100644 index 0000000000000..3250fb4854de7 --- /dev/null +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/SimpleField.java @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.models; + +/** + * A helper Field model to build a simple {@link Field}. + */ +public class SimpleField extends FieldBase { + private boolean key; + private boolean facetable; + private boolean sortable; + private boolean filterable; + private boolean hidden; + + /** + * Initializes a new instance of the {@link SimpleField} class. + * + * @param name The name of the field, which must be unique within the index or parent field. + * @param dataType The {@link DataType} of the {@link Field}. + * @param collection boolean field to indicate whether the dataType is collection. + * @throws NullPointerException when {@code name} is null. + */ + public SimpleField(String name, DataType dataType, boolean collection) { + super(name, collection ? DataType.collection(dataType) : dataType); + } + + /** + * Gets whether the field is the key field. + * + * @return An {@link Index} must have exactly one key field of type {@code DataType.EDM_STRING}. + */ + public boolean isKey() { + return key; + } + + /** + * Sets whether the field is the key field. The default is false. + * + * @param key boolean to indicate whether the field is key field or not. + * @return The SimpleField object itself. + */ + public SimpleField setKey(boolean key) { + this.key = key; + return this; + } + + /** + * Gets a value indicating whether to enable the field can be referenced in {@code $orderby} expressions. + * By default Azure Cognitive Search sorts results by score, but in many experiences users may want to sort by + * fields in the documents. + * + * @return The boolean to indicate whether the field is sortable or not. + */ + public boolean isSortable() { + return sortable; + } + + /** + * Sets a value indicating whether to enable the field can be referenced in {@code $orderby} expressions. + * The default is false. + * By default Azure Cognitive Search sorts results by score, but in many experiences users may want to sort by + * fields in the documents. + * + * @param sortable The boolean to indicate whether the field is sortable or not. + * @return The SimpleField object itself. + */ + public SimpleField setSortable(boolean sortable) { + this.sortable = sortable; + return this; + } + + /** + * Gets or sets a value indicating whether the field can be referenced in {@code $filter} queries. + * + * @return The boolean to indicate whether the field is filterable or not. + */ + public boolean isFilterable() { + return filterable; + } + + /** + * Gets or sets a value indicating whether the field can be referenced in {@code $filter} queries. The default is false. + * @param filterable The boolean to indicate whether the field is filterable or not. + * @return The SimpleField object itself. + */ + public SimpleField setFilterable(boolean filterable) { + this.filterable = filterable; + return this; + } + + /** + * Gets whether the field is returned in search results. + * + * @return The boolean to indicate whether the field is hidden or not. + */ + public boolean isHidden() { + return hidden; + } + + /** + * Sets whether the field is returned in search results. The default is false. + * A key field where {@code key} is true must have this property set to false. + * + * @param hidden The boolean to indicate whether the field is hidden or not. + * @return The SimpleField object itself. + */ + public SimpleField setHidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + /** + * Gets a value indicating whether the field can be retrieved in facet queries. + * Facets are used in presentation of search results that include hit counts by categories. + * For example, in a search for digital cameras, facets might include branch, megapixels, price, etc. + * + * @return The boolean to indicate whether the field is facetable or not. + */ + public boolean isFacetable() { + return facetable; + } + + /** + * Sets a value indicating whether the field can be retrieved in facet queries. The default is false. + * Facets are used in presentation of search results that include hit counts by categories. + * For example, in a search for digital cameras, facets might include branch, megapixels, price, etc. + * + * @param facetable The boolean to indicate whether the field is facetable or not. + * @return The SimpleField object itself. + */ + public SimpleField setFacetable(boolean facetable) { + this.facetable = facetable; + return this; + } + + /** + * Convert SimpleField to {@link Field}. + * + * @return The {@link Field} object. + */ + public Field build() { + return new Field().setName(super.getName()) + .setType(super.getDataType()) + .setKey(key) + .setSearchable(false) + .setSortable(sortable) + .setFilterable(filterable) + .setHidden(hidden) + .setFacetable(facetable) + .setHidden(hidden); + } +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/FieldBuilderTest.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/FieldBuilderTest.java new file mode 100644 index 0000000000000..6fadfc9cbf67e --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/FieldBuilderTest.java @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents; + +import com.azure.search.documents.models.AnalyzerName; +import com.azure.search.documents.models.ComplexField; +import com.azure.search.documents.models.DataType; +import com.azure.search.documents.models.Field; +import com.azure.search.documents.models.SearchableField; +import com.azure.search.documents.models.SimpleField; +import com.azure.search.documents.test.environment.models.Hotel; +import com.azure.search.documents.test.environment.models.HotelAnalyzerException; +import com.azure.search.documents.test.environment.models.HotelCircularDependencies; +import com.azure.search.documents.test.environment.models.HotelSearchException; +import com.azure.search.documents.test.environment.models.HotelSearchableExceptionOnList; +import com.azure.search.documents.test.environment.models.HotelTwoDimensional; +import com.azure.search.documents.test.environment.models.HotelWithEmptyInSynonymMaps; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FieldBuilderTest { + @Test + public void hotelComparison() { + List actualFields = sortByFieldName(FieldBuilder.build(Hotel.class)); + List expectedFields = sortByFieldName(buildHotelFields()); + assertListFieldEquals(expectedFields, actualFields); + } + + @Test + public void hotelSearchableThrowException() { + Exception exception = assertThrows(RuntimeException.class, () -> + FieldBuilder.build(HotelSearchException.class)); + assertExceptionMassageAndDataType(exception, DataType.EDM_INT32, "hotelId"); + } + + @Test + public void hotelListFieldSearchableThrowException() { + Exception exception = assertThrows(RuntimeException.class, () -> + FieldBuilder.build(HotelSearchableExceptionOnList.class)); + assertExceptionMassageAndDataType(exception, DataType.collection(DataType.EDM_INT32), "passcode"); + } + + @Test + public void hotelCircularDependencies() { + List actualFields = sortByFieldName(FieldBuilder.build(HotelCircularDependencies.class)); + List expectedFields = sortByFieldName(buildHotelCircularDependenciesModel()); + assertListFieldEquals(expectedFields, actualFields); + } + + @Test + public void hotelWithEmptySynonymMaps() { + // We cannot put null in the annotation. So no need to test null case. + List actualFields = FieldBuilder.build(HotelWithEmptyInSynonymMaps.class); + List expectedFields = Collections.singletonList(new SearchableField("tags", true) + .setSynonymMapNames(Arrays.asList("asynonymMaps", "maps")).build()); + assertListFieldEquals(expectedFields, actualFields); + } + + @Test + public void hotelWithTwoDimensionalType() { + Exception exception = assertThrows(RuntimeException.class, () -> FieldBuilder.build(HotelTwoDimensional.class)); + assertExceptionMassageAndDataType(exception, null, "single-dimensional"); + } + + @Test + public void hotelAnalyzerException() { + Exception exception = assertThrows(RuntimeException.class, () -> + FieldBuilder.build(HotelAnalyzerException.class)); + assertExceptionMassageAndDataType(exception, null, + "either analyzer or both searchAnalyzer and indexAnalyzer"); + } + + private void assertListFieldEquals(List expected, List actual) { + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) { + TestHelpers.assertObjectEquals(expected.get(i), actual.get(i)); + } + } + + private void assertExceptionMassageAndDataType(Exception exception, DataType dataType, String msg) { + assertTrue(exception.getMessage().contains(msg)); + if (dataType != null) { + assertTrue(exception.getMessage().contains(dataType.toString())); + } + } + + private List buildHotelCircularDependenciesModel() { + Field homeAddress = new ComplexField("homeAddress", false).setFields(buildHotelInAddress()).build(); + Field billingAddress = new ComplexField("billingAddress", false).setFields(buildHotelInAddress()).build(); + return Arrays.asList(homeAddress, billingAddress); + } + + private List buildHotelInAddress() { + Field hotel = new ComplexField("hotel", false).build(); + return Collections.singletonList(hotel); + } + + private List buildHotelFields() { + Field hotelId = new SimpleField("hotelId", DataType.EDM_STRING, false).setSortable(true) + .setKey(true).build(); + Field hotelName = new SearchableField("hotelName", false).setAnalyzer(AnalyzerName.fromString("en.lucene")) + .setSortable(true).build(); + Field description = new SimpleField("description", DataType.EDM_STRING, false).build(); + Field category = new SimpleField("category", DataType.EDM_STRING, false).build(); + Field tags = new SearchableField("tags", true).build(); + Field parkingIncluded = new SimpleField("parkingIncluded", DataType.EDM_BOOLEAN, false).build(); + Field smokingAllowed = new SimpleField("smokingAllowed", DataType.EDM_BOOLEAN, false).build(); + Field lastRenovationDate = new SimpleField("lastRenovationDate", DataType.EDM_DATE_TIME_OFFSET, false).build(); + Field rating = new SimpleField("rating", DataType.EDM_INT32, false).build(); + Field location = new SimpleField("location", DataType.EDM_GEOGRAPHY_POINT, false).build(); + Field address = new ComplexField("address", false) + .setFields(buildHotelAddressField()).build(); + Field rooms = new ComplexField("rooms", true).setFields(buildHotelRoomField()).build(); + + return Arrays.asList(hotelId, hotelName, description, category, tags, parkingIncluded, smokingAllowed, + lastRenovationDate, rating, location, address, rooms); + } + + private List buildHotelAddressField() { + Field streetAddress = new SimpleField("streetAddress", DataType.EDM_STRING, false).setFacetable(true) + .setKey(true).build(); + Field city = new SearchableField("city", false).setFilterable(true).build(); + Field stateProvince = new SearchableField("stateProvince", false).build(); + Field country = new SearchableField("country", false) + .setSynonymMapNames(Arrays.asList("America -> USA", "USA -> US")).build(); + Field postalCode = new SimpleField("postalCode", DataType.EDM_STRING, false).build(); + return Arrays.asList(streetAddress, city, stateProvince, country, postalCode); + } + + private List buildHotelRoomField() { + Field description = new SimpleField("description", DataType.EDM_STRING, false).build(); + Field descriptionFr = new SimpleField("descriptionFr", DataType.EDM_STRING, false).build(); + Field type = new SimpleField("type", DataType.EDM_STRING, false).build(); + Field baseRate = new SimpleField("baseRate", DataType.EDM_DOUBLE, false).build(); + Field bedOptions = new SimpleField("bedOptions", DataType.EDM_STRING, false).build(); + Field sleepsCount = new SimpleField("sleepsCount", DataType.EDM_INT32, false).build(); + Field smokingAllowed = new SimpleField("smokingAllowed", DataType.EDM_BOOLEAN, false).build(); + Field tags = new SimpleField("tags", DataType.EDM_STRING, true).build(); + return Arrays.asList(description, descriptionFr, type, baseRate, bedOptions, sleepsCount, smokingAllowed, tags); + } + + private List sortByFieldName(List fields) { + Collections.sort(fields, Comparator.comparing(Field::getName)); + return fields; + } +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/AddressCircularDependencies.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/AddressCircularDependencies.java new file mode 100644 index 0000000000000..4aa88cdb7851f --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/AddressCircularDependencies.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.test.environment.models; + +/** + * The address model class to test circular dependencies. + */ +public class AddressCircularDependencies { + + private HotelCircularDependencies hotel; + + /** + * Get hotel address. + * + * @return the hotel address. + */ + public HotelCircularDependencies getHotel() { + return hotel; + } + + /** + * Sets hotel address. + * + * @param hotel the hotel address. + * @return The {@link AddressCircularDependencies} object itself. + */ + public AddressCircularDependencies setHotel(HotelCircularDependencies hotel) { + this.hotel = hotel; + return this; + } +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/Hotel.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/Hotel.java index b3fb0d6ca1376..60b5ba5d973a0 100644 --- a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/Hotel.java +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/Hotel.java @@ -2,6 +2,9 @@ // Licensed under the MIT License. package com.azure.search.documents.test.environment.models; +import com.azure.search.documents.indexes.FieldIgnore; +import com.azure.search.documents.indexes.SearchableFieldProperty; +import com.azure.search.documents.indexes.SimpleFieldProperty; import com.azure.search.documents.models.GeoPoint; import com.fasterxml.jackson.annotation.JsonProperty; @@ -10,21 +13,25 @@ import java.util.List; public class Hotel { + @SimpleFieldProperty(isKey = true, isSortable = true) @JsonProperty(value = "HotelId") private String hotelId; + @SearchableFieldProperty(isSortable = true, analyzer = "en.lucene") @JsonProperty(value = "HotelName") private String hotelName; @JsonProperty(value = "Description") private String description; + @FieldIgnore @JsonProperty(value = "Description_fr") private String descriptionFr; @JsonProperty(value = "Category") private String category; + @SearchableFieldProperty @JsonProperty(value = "Tags") private List tags; @@ -40,6 +47,7 @@ public class Hotel { @JsonProperty(value = "Rating") private Integer rating; + @SimpleFieldProperty @JsonProperty(value = "Location") private GeoPoint location; diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAddress.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAddress.java index ac1462c44b893..da6d53009fe2b 100644 --- a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAddress.java +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAddress.java @@ -2,21 +2,28 @@ // Licensed under the MIT License. package com.azure.search.documents.test.environment.models; +import com.azure.search.documents.indexes.SearchableFieldProperty; +import com.azure.search.documents.indexes.SimpleFieldProperty; import com.fasterxml.jackson.annotation.JsonProperty; public class HotelAddress { + @SimpleFieldProperty(isKey = true, isFacetable = true) @JsonProperty(value = "StreetAddress") private String streetAddress; + @SearchableFieldProperty(isFilterable = true) @JsonProperty(value = "City") private String city; + @SearchableFieldProperty @JsonProperty(value = "StateProvince") private String stateProvince; + @SearchableFieldProperty(synonymMaps = {"America -> USA", "USA -> US"}) @JsonProperty(value = "Country") private String country; + @SimpleFieldProperty @JsonProperty(value = "PostalCode") private String postalCode; diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAnalyzerException.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAnalyzerException.java new file mode 100644 index 0000000000000..b666b74388894 --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelAnalyzerException.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.test.environment.models; + +import com.azure.search.documents.indexes.SearchableFieldProperty; + +public class HotelAnalyzerException { + @SearchableFieldProperty(analyzer = "en.microsoft", indexAnalyzer = "whitespce") + private String tag; + + /** + * Gets the tag. + * + * @return The tag of hotel. + */ + public String getTag() { + return tag; + } + + /** + * Sets the pattern. + * + * @param tag The tag of hotel. + * @return the {@link HotelAnalyzerException} object itself. + */ + public HotelAnalyzerException setTag(String tag) { + this.tag = tag; + return this; + } + + +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelCircularDependencies.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelCircularDependencies.java new file mode 100644 index 0000000000000..a8f7cb1b884cc --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelCircularDependencies.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.test.environment.models; + +/** + * The model class to test the behaviour of circular dependencies + */ +public class HotelCircularDependencies { + private AddressCircularDependencies homeAddress; + private AddressCircularDependencies billingAddress; + + /** + * Gets home address. + * + * @return home address. + */ + public AddressCircularDependencies getHomeAddress() { + return homeAddress; + } + + /** + * Sets home address. + * + * @param homeAddress Home address to set. + * @return The {@link HotelCircularDependencies} object itself. + */ + public HotelCircularDependencies setHomeAddress(AddressCircularDependencies homeAddress) { + this.homeAddress = homeAddress; + return this; + } + + /** + * Gets billing address. + * + * @return billing address. + */ + public AddressCircularDependencies getBillingAddress() { + return billingAddress; + } + + /** + * Sets billing address. + * + * @param billingAddress Billing address to set. + * @return The {@link HotelCircularDependencies} object itself. + */ + public HotelCircularDependencies setBillingAddress(AddressCircularDependencies billingAddress) { + this.billingAddress = billingAddress; + return this; + } +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchException.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchException.java new file mode 100644 index 0000000000000..c0749fd57c207 --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchException.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.test.environment.models; + +import com.azure.search.documents.indexes.SearchableFieldProperty; + +/** + * The data object model is to test exception case. + */ +public class HotelSearchException { + + @SearchableFieldProperty + private int hotelId; + + /** + * Gets hotel id. + * + * @return Get hotel id + */ + public int getHotelId() { + return hotelId; + } + + /** + * Sets hotel id. + * + * @param hotelId The hotel id. + * @return the {@link HotelSearchException} object itself. + */ + public HotelSearchException setHotelId(int hotelId) { + this.hotelId = hotelId; + return this; + } +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchableExceptionOnList.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchableExceptionOnList.java new file mode 100644 index 0000000000000..603c8990d8b6a --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelSearchableExceptionOnList.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.test.environment.models; + +import com.azure.search.documents.indexes.SearchableFieldProperty; + +import java.util.List; + +/** + * Helper class on test searchable exception field. + */ +public class HotelSearchableExceptionOnList { + @SearchableFieldProperty + private List passcode; + + /** + * Gets passcode. + * @return the passcode of hotel. + */ + public List getPasscode() { + return passcode; + } + + /** + * Sets passcode. + * + * @param passcode the passcode of hotel. + * @return The {@link HotelSearchableExceptionOnList} object itself. + */ + public HotelSearchableExceptionOnList setPasscode(List passcode) { + this.passcode = passcode; + return this; + } +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelTwoDimensional.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelTwoDimensional.java new file mode 100644 index 0000000000000..996af70646901 --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelTwoDimensional.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.test.environment.models; + +import java.util.List; + +/** + * The class is to test unsupported two-dimensional type. + */ +public class HotelTwoDimensional { + private List> matrix; + + /** + * Gets the matrix + * + * @return The matrix of hotel. + */ + public List> getMatrix() { + return matrix; + } + + /** + * Sets the matrix. + * + * @param matrix The matrix of hotel. + * @return The {@link HotelTwoDimensional} object itself. + */ + public HotelTwoDimensional setMatrix(List> matrix) { + this.matrix = matrix; + return this; + } + +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelWithEmptyInSynonymMaps.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelWithEmptyInSynonymMaps.java new file mode 100644 index 0000000000000..b677b79a77c7c --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/models/HotelWithEmptyInSynonymMaps.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.test.environment.models; + +import com.azure.search.documents.indexes.SearchableFieldProperty; + +import java.util.List; + +/** + * This is a class to test whether we filter out the empty String in synonymMaps. + */ +public class HotelWithEmptyInSynonymMaps { + @SearchableFieldProperty(synonymMaps = {"asynonymMaps", "", " ", "maps"}) + private List tags; + + /** + * Gets the tags. + * + * @return The tags of hotel. + */ + public List getTags() { + return tags; + } + + /** + * Set the tags + * + * @param tags The tags of hotel. + * @return The {@link HotelWithEmptyInSynonymMaps} object itself. + */ + public HotelWithEmptyInSynonymMaps setTags(List tags) { + this.tags = tags; + return this; + } +} diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/setup/AzureSearchResources.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/setup/AzureSearchResources.java index 58d39af1ca59a..686223e4627df 100644 --- a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/setup/AzureSearchResources.java +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/test/environment/setup/AzureSearchResources.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.net.InetAddress; import java.security.SecureRandom; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Locale; import java.util.Objects; @@ -190,10 +192,12 @@ public void createResourceGroup() { resourceGroup = azure.resourceGroups() .getByName(resourceGroupName); } else { + String deleteTime = OffsetDateTime.now().plusHours(12).format(DateTimeFormatter.ISO_INSTANT); System.out.println("Creating Resource Group: " + resourceGroupName); resourceGroup = azure.resourceGroups() .define(resourceGroupName) .withRegion(location) + .withTag("DeleteAfter", deleteTime) .create(); } }