/* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;
using OpenSearch.Net;
namespace OpenSearch.Client
{
///
public class ConnectionSettings : ConnectionSettingsBase
{
/// The default user agent for OpenSearch.Client
public static readonly string DefaultUserAgent =
$"opensearch-net/{typeof(IConnectionSettingsValues).Assembly.GetCustomAttribute().InformationalVersion} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; OpenSearch.Client)";
///
/// A delegate used to construct a serializer to serialize CLR types representing documents and other types related to
/// documents.
/// By default, the internal serializer will be used to serializer all types.
///
public delegate IOpenSearchSerializer SourceSerializerFactory(IOpenSearchSerializer builtIn, IConnectionSettingsValues values);
///
/// Creates a new instance of connection settings, if is not specified will default to connecting to http://localhost:9200
///
///
///
public ConnectionSettings(Uri uri = null, IConnection connection = null) : this(new SingleNodeConnectionPool(uri ?? new Uri("http://localhost:9200")), connection) { }
///
/// Sets up the client to communicate to OpenSearch Cloud using ,
/// documentation for more information on how to obtain your Cloud Id
///
public ConnectionSettings(string cloudId, BasicAuthenticationCredentials credentials) : this(new CloudConnectionPool(cloudId, credentials)) { }
///
/// Sets up the client to communicate to OpenSearch Cloud using ,
/// documentation for more information on how to obtain your Cloud Id
///
public ConnectionSettings(string cloudId, ApiKeyAuthenticationCredentials credentials) : this(new CloudConnectionPool(cloudId, credentials)) { }
///
/// Instantiate connection settings using a using the provided
/// that never uses any IO.
///
public ConnectionSettings(InMemoryConnection connection)
: this(new SingleNodeConnectionPool(new Uri("http://localhost:9200")), connection) { }
public ConnectionSettings(IConnectionPool connectionPool) : this(connectionPool, null, null) { }
public ConnectionSettings(IConnectionPool connectionPool, SourceSerializerFactory sourceSerializer)
: this(connectionPool, null, sourceSerializer) { }
public ConnectionSettings(IConnectionPool connectionPool, IConnection connection) : this(connectionPool, connection, null) { }
public ConnectionSettings(IConnectionPool connectionPool, IConnection connection, SourceSerializerFactory sourceSerializer)
: this(connectionPool, connection, sourceSerializer, null) { }
public ConnectionSettings(
IConnectionPool connectionPool,
IConnection connection,
SourceSerializerFactory sourceSerializer,
IPropertyMappingProvider propertyMappingProvider
) : base(connectionPool, connection, sourceSerializer, propertyMappingProvider) { }
}
///
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class ConnectionSettingsBase : ConnectionConfiguration, IConnectionSettingsValues
where TConnectionSettings : ConnectionSettingsBase, IConnectionSettingsValues
{
private readonly FluentDictionary _defaultIndices;
private readonly FluentDictionary _defaultRelationNames;
private readonly HashSet _disableIdInference = new HashSet();
private readonly FluentDictionary _idProperties = new FluentDictionary();
private readonly Inferrer _inferrer;
private readonly IPropertyMappingProvider _propertyMappingProvider;
private readonly FluentDictionary _propertyMappings = new FluentDictionary();
private readonly FluentDictionary _routeProperties = new FluentDictionary();
private readonly IOpenSearchSerializer _sourceSerializer;
private bool _defaultDisableAllInference;
private Func _defaultFieldNameInferrer;
private string _defaultIndex;
protected ConnectionSettingsBase(
IConnectionPool connectionPool,
IConnection connection,
ConnectionSettings.SourceSerializerFactory sourceSerializerFactory,
IPropertyMappingProvider propertyMappingProvider
)
: base(connectionPool, connection, null)
{
var formatterResolver = new OpenSearchClientFormatterResolver(this);
var defaultSerializer = new DefaultHighLevelSerializer(formatterResolver);
var sourceSerializer = sourceSerializerFactory?.Invoke(defaultSerializer, this) ?? defaultSerializer;
var serializerAsMappingProvider = sourceSerializer as IPropertyMappingProvider;
_propertyMappingProvider = propertyMappingProvider ?? serializerAsMappingProvider ?? new PropertyMappingProvider();
//We wrap these in an internal proxy to facilitate serialization diagnostics
_sourceSerializer = new DiagnosticsSerializerProxy(sourceSerializer, "source");
UseThisRequestResponseSerializer = new DiagnosticsSerializerProxy(defaultSerializer);
_defaultFieldNameInferrer = p => p.ToCamelCase();
_defaultIndices = new FluentDictionary();
_defaultRelationNames = new FluentDictionary();
_inferrer = new Inferrer(this);
UserAgent(ConnectionSettings.DefaultUserAgent);
}
bool IConnectionSettingsValues.DefaultDisableIdInference => _defaultDisableAllInference;
Func IConnectionSettingsValues.DefaultFieldNameInferrer => _defaultFieldNameInferrer;
string IConnectionSettingsValues.DefaultIndex => _defaultIndex;
FluentDictionary IConnectionSettingsValues.DefaultIndices => _defaultIndices;
HashSet IConnectionSettingsValues.DisableIdInference => _disableIdInference;
FluentDictionary IConnectionSettingsValues.DefaultRelationNames => _defaultRelationNames;
FluentDictionary IConnectionSettingsValues.IdProperties => _idProperties;
Inferrer IConnectionSettingsValues.Inferrer => _inferrer;
IPropertyMappingProvider IConnectionSettingsValues.PropertyMappingProvider => _propertyMappingProvider;
FluentDictionary IConnectionSettingsValues.PropertyMappings => _propertyMappings;
FluentDictionary IConnectionSettingsValues.RouteProperties => _routeProperties;
IOpenSearchSerializer IConnectionSettingsValues.SourceSerializer => _sourceSerializer;
///
/// The default index to use for a request when no index has been explicitly specified
/// and no default indices are specified for the given CLR type specified for the request.
///
public TConnectionSettings DefaultIndex(string defaultIndex) => Assign(defaultIndex, (a, v) => a._defaultIndex = v);
///
/// Specifies how field names are inferred from CLR property names.
///
/// By default, OpenSearch.Client camel cases property names.
///
///
/// CLR property EmailAddress will be inferred as "emailAddress" OpenSearch document field name
///
public TConnectionSettings DefaultFieldNameInferrer(Func fieldNameInferrer) =>
Assign(fieldNameInferrer, (a, v) => a._defaultFieldNameInferrer = v);
///
/// Disables automatic Id inference for given CLR types.
///
/// OpenSearch.Client by default will use the value of a property named Id on a CLR type as the _id to send to OpenSearch. Adding a type
/// will disable this behaviour for that CLR type. If Id inference should be disabled for all CLR types, use
///
///
public TConnectionSettings DefaultDisableIdInference(bool disable = true) => Assign(disable, (a, v) => a._defaultDisableAllInference = v);
private void MapIdPropertyFor(Expression> objectPath)
{
objectPath.ThrowIfNull(nameof(objectPath));
var memberInfo = new MemberInfoResolver(objectPath);
var fieldName = memberInfo.Members.Single().Name;
if (_idProperties.TryGetValue(typeof(TDocument), out var idPropertyFieldName))
{
if (idPropertyFieldName.Equals(fieldName)) return;
throw new ArgumentException(
$"Cannot map '{fieldName}' as the id property for type '{typeof(TDocument).Name}': it already has '{_idProperties[typeof(TDocument)]}' mapped.");
}
_idProperties.Add(typeof(TDocument), fieldName);
}
///
private void MapRoutePropertyFor(Expression> objectPath)
{
objectPath.ThrowIfNull(nameof(objectPath));
var memberInfo = new MemberInfoResolver(objectPath);
var fieldName = memberInfo.Members.Single().Name;
if (_routeProperties.TryGetValue(typeof(TDocument), out var routePropertyFieldName))
{
if (routePropertyFieldName.Equals(fieldName)) return;
throw new ArgumentException(
$"Cannot map '{fieldName}' as the route property for type '{typeof(TDocument).Name}': it already has '{_routeProperties[typeof(TDocument)]}' mapped.");
}
_routeProperties.Add(typeof(TDocument), fieldName);
}
private void ApplyPropertyMappings(IList> mappings)
where TDocument : class
{
foreach (var mapping in mappings)
{
var e = mapping.Property;
var memberInfoResolver = new MemberInfoResolver(e);
if (memberInfoResolver.Members.Count > 1)
throw new ArgumentException($"{nameof(ApplyPropertyMappings)} can only map direct properties");
if (memberInfoResolver.Members.Count == 0)
throw new ArgumentException($"Expression {e} does contain any member access");
var memberInfo = memberInfoResolver.Members[0];
if (_propertyMappings.TryGetValue(memberInfo, out var propertyMapping))
{
var newName = mapping.NewName;
var mappedAs = propertyMapping.Name;
var typeName = typeof(TDocument).Name;
if (mappedAs.IsNullOrEmpty() && newName.IsNullOrEmpty())
throw new ArgumentException($"Property mapping '{e}' on type is already ignored");
if (mappedAs.IsNullOrEmpty())
throw new ArgumentException(
$"Property mapping '{e}' on type {typeName} can not be mapped to '{newName}' it already has an ignore mapping");
if (newName.IsNullOrEmpty())
throw new ArgumentException(
$"Property mapping '{e}' on type {typeName} can not be ignored it already has a mapping to '{mappedAs}'");
throw new ArgumentException(
$"Property mapping '{e}' on type {typeName} can not be mapped to '{newName}' already mapped as '{mappedAs}'");
}
_propertyMappings[memberInfo] = mapping.ToPropertyMapping();
}
}
///
/// Specify how the mapping is inferred for a given CLR type.
/// The mapping can infer the index, id and relation name for a given CLR type, as well as control
/// serialization behaviour for CLR properties.
///
public TConnectionSettings DefaultMappingFor(Func, IClrTypeMapping> selector)
where TDocument : class
{
var inferMapping = selector(new ClrTypeMappingDescriptor());
if (!inferMapping.IndexName.IsNullOrEmpty())
_defaultIndices[inferMapping.ClrType] = inferMapping.IndexName;
if (!inferMapping.RelationName.IsNullOrEmpty())
_defaultRelationNames[inferMapping.ClrType] = inferMapping.RelationName;
if (!string.IsNullOrWhiteSpace(inferMapping.IdPropertyName))
_idProperties[inferMapping.ClrType] = inferMapping.IdPropertyName;
if (inferMapping.IdProperty != null)
MapIdPropertyFor(inferMapping.IdProperty);
if (inferMapping.RoutingProperty != null)
MapRoutePropertyFor(inferMapping.RoutingProperty);
if (inferMapping.Properties != null)
ApplyPropertyMappings(inferMapping.Properties);
if (inferMapping.DisableIdInference) _disableIdInference.Add(inferMapping.ClrType);
else _disableIdInference.Remove(inferMapping.ClrType);
return (TConnectionSettings)this;
}
///
/// Specify how the mapping is inferred for a given CLR type.
/// The mapping can infer the index and relation name for a given CLR type.
///
public TConnectionSettings DefaultMappingFor(Type documentType, Func selector)
{
var inferMapping = selector(new ClrTypeMappingDescriptor(documentType));
if (!inferMapping.IndexName.IsNullOrEmpty())
_defaultIndices[inferMapping.ClrType] = inferMapping.IndexName;
if (!inferMapping.RelationName.IsNullOrEmpty())
_defaultRelationNames[inferMapping.ClrType] = inferMapping.RelationName;
if (!string.IsNullOrWhiteSpace(inferMapping.IdPropertyName))
_idProperties[inferMapping.ClrType] = inferMapping.IdPropertyName;
return (TConnectionSettings)this;
}
///
/// Specify how the mapping is inferred for a given CLR type.
/// The mapping can infer the index and relation name for a given CLR type.
///
public TConnectionSettings DefaultMappingFor(IEnumerable typeMappings)
{
if (typeMappings == null) return (TConnectionSettings)this;
foreach (var inferMapping in typeMappings)
{
if (!inferMapping.IndexName.IsNullOrEmpty())
_defaultIndices[inferMapping.ClrType] = inferMapping.IndexName;
if (!inferMapping.RelationName.IsNullOrEmpty())
_defaultRelationNames[inferMapping.ClrType] = inferMapping.RelationName;
}
return (TConnectionSettings)this;
}
///
/// OpenSearch.Client handles 404 in its , we do not want the low level client throwing exceptions
/// when is enabled for 404's. The client is in charge of composing paths
/// so a 404 never signals a wrong url but a missing entity.
///
protected override bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) =>
statusCode >= 200 && statusCode < 300
|| statusCode == 404;
}
}