LUCENENET-565: Porting of Lucene Replicator - Commit is for Review with comments about original Java Source for assistance.
Project: http://git-wip-us.apache.org/repos/asf/lucenenet/repo Commit: http://git-wip-us.apache.org/repos/asf/lucenenet/commit/6da4dd20 Tree: http://git-wip-us.apache.org/repos/asf/lucenenet/tree/6da4dd20 Diff: http://git-wip-us.apache.org/repos/asf/lucenenet/diff/6da4dd20 Branch: refs/heads/replicator Commit: 6da4dd20d45b152df0c0cf5f4e6c90a8024682e5 Parents: e67244a Author: jeme <[email protected]> Authored: Sat Jul 22 12:52:19 2017 +0200 Committer: jeme <[email protected]> Committed: Sat Jul 22 14:12:02 2017 +0200 ---------------------------------------------------------------------- Lucene.Net.sln | 78 +++ .../AspNetCoreReplicationRequest.cs | 49 ++ .../AspNetCoreReplicationResponse.cs | 56 ++ .../AspNetCoreReplicationServiceExtentions.cs | 17 + .../Lucene.Net.Replicator.AspNetCore.csproj | 82 +++ .../Properties/AssemblyInfo.cs | 21 + .../packages.config | 9 + .../ComponentWrapperInfoStream.cs | 54 ++ .../Http/Abstractions/IReplicationRequest.cs | 27 + .../Http/Abstractions/IReplicationResponse.cs | 29 + .../Http/EnumerableExtensions.cs | 32 + .../Http/HttpClientBase.cs | 516 ++++++++++++++ .../Http/HttpReplicator.cs | 145 ++++ .../Http/ReplicationService.cs | 211 ++++++ src/Lucene.Net.Replicator/Http/package.html | 28 + .../IReplicationHandler.cs | 32 + .../ISourceDirectoryFactory.cs | 27 + .../IndexAndTaxonomyReplicationHandler.cs | 276 ++++++++ .../IndexAndTaxonomyRevision.cs | 334 +++++++++ .../IndexInputInputStream.cs | 102 +++ .../IndexReplicationHandler.cs | 510 ++++++++++++++ src/Lucene.Net.Replicator/IndexRevision.cs | 200 ++++++ src/Lucene.Net.Replicator/LocalReplicator.cs | 416 ++++++++++++ .../Lucene.Net.Replicator.csproj | 108 +++ .../PerSessionDirectoryFactory.cs | 96 +++ .../Properties/AssemblyInfo.cs | 24 + src/Lucene.Net.Replicator/ReplicationClient.cs | 673 +++++++++++++++++++ src/Lucene.Net.Replicator/Replicator.cs | 91 +++ src/Lucene.Net.Replicator/Revision.cs | 81 +++ src/Lucene.Net.Replicator/RevisionFile.cs | 87 +++ .../SessionExpiredException.cs | 58 ++ src/Lucene.Net.Replicator/SessionToken.cs | 129 ++++ src/Lucene.Net.Replicator/packages.config | 4 + .../Http/HttpReplicatorTest.cs | 104 +++ .../Http/ReplicationServlet.cs | 22 + .../IndexAndTaxonomyReplicationClientTest.cs | 518 ++++++++++++++ .../IndexAndTaxonomyRevisionTest.cs | 188 ++++++ .../IndexReplicationClientTest.cs | 513 ++++++++++++++ .../IndexRevisionTest.cs | 177 +++++ .../LocalReplicatorTest.cs | 225 +++++++ .../Lucene.Net.Tests.Replicator.csproj | 219 ++++++ .../Properties/AssemblyInfo.cs | 19 + .../ReplicatorTestCase.cs | 192 ++++++ .../SessionTokenTest.cs | 67 ++ src/Lucene.Net.Tests.Replicator/packages.config | 52 ++ 45 files changed, 6898 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/Lucene.Net.sln ---------------------------------------------------------------------- diff --git a/Lucene.Net.sln b/Lucene.Net.sln index a187ccc..73e8562 100644 --- a/Lucene.Net.sln +++ b/Lucene.Net.sln @@ -106,6 +106,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Demo", "src\Luce EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Tests.Demo", "src\Lucene.Net.Tests.Demo\Lucene.Net.Tests.Demo.csproj", "{571B361E-B0D4-445E-A0BC-1A24AA184258}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Replicator", "src\Lucene.Net.Replicator\Lucene.Net.Replicator.csproj", "{1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Replicator.AspNetCore", "src\Lucene.Net.Replicator.AspNetCore\Lucene.Net.Replicator.AspNetCore.csproj", "{763CCB5A-E397-456A-AF47-7C6E228B1852}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Tests.Replicator", "src\Lucene.Net.Tests.Replicator\Lucene.Net.Tests.Replicator.csproj", "{418E9D8E-2369-4B52-8D2F-5A987213999B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1059,6 +1065,78 @@ Global {571B361E-B0D4-445E-A0BC-1A24AA184258}.Release35|Mixed Platforms.Build.0 = Release|Any CPU {571B361E-B0D4-445E-A0BC-1A24AA184258}.Release35|x86.ActiveCfg = Release|Any CPU {571B361E-B0D4-445E-A0BC-1A24AA184258}.Release35|x86.Build.0 = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug|x86.Build.0 = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug35|Any CPU.ActiveCfg = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug35|Any CPU.Build.0 = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug35|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug35|Mixed Platforms.Build.0 = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug35|x86.ActiveCfg = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Debug35|x86.Build.0 = Debug|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release|Any CPU.Build.0 = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release|x86.ActiveCfg = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release|x86.Build.0 = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release35|Any CPU.ActiveCfg = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release35|Any CPU.Build.0 = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release35|Mixed Platforms.ActiveCfg = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release35|Mixed Platforms.Build.0 = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release35|x86.ActiveCfg = Release|Any CPU + {1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}.Release35|x86.Build.0 = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug|x86.ActiveCfg = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug|x86.Build.0 = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug35|Any CPU.ActiveCfg = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug35|Any CPU.Build.0 = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug35|Mixed Platforms.ActiveCfg = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug35|Mixed Platforms.Build.0 = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug35|x86.ActiveCfg = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Debug35|x86.Build.0 = Debug|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release|Any CPU.Build.0 = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release|x86.ActiveCfg = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release|x86.Build.0 = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release35|Any CPU.ActiveCfg = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release35|Any CPU.Build.0 = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release35|Mixed Platforms.ActiveCfg = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release35|Mixed Platforms.Build.0 = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release35|x86.ActiveCfg = Release|Any CPU + {763CCB5A-E397-456A-AF47-7C6E228B1852}.Release35|x86.Build.0 = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug|x86.ActiveCfg = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug|x86.Build.0 = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug35|Any CPU.ActiveCfg = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug35|Any CPU.Build.0 = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug35|Mixed Platforms.ActiveCfg = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug35|Mixed Platforms.Build.0 = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug35|x86.ActiveCfg = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Debug35|x86.Build.0 = Debug|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release|Any CPU.Build.0 = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release|x86.ActiveCfg = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release|x86.Build.0 = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release35|Any CPU.ActiveCfg = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release35|Any CPU.Build.0 = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release35|Mixed Platforms.ActiveCfg = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release35|Mixed Platforms.Build.0 = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release35|x86.ActiveCfg = Release|Any CPU + {418E9D8E-2369-4B52-8D2F-5A987213999B}.Release35|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationRequest.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationRequest.cs b/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationRequest.cs new file mode 100644 index 0000000..7a9fba2 --- /dev/null +++ b/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationRequest.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Lucene.Net.Replicator.Http; +using Lucene.Net.Replicator.Http.Abstractions; +using Microsoft.AspNetCore.Http; + +namespace Lucene.Net.Replicator.AspNetCore +{ + /// <summary> + /// Abstraction for remote replication requests, allows easy integration into any hosting frameworks. + /// </summary> + /// <remarks> + /// .NET Specific Implementation of the Lucene Replicator using AspNetCore + /// </remarks> + //Note: LUCENENET specific + public class AspNetCoreReplicationRequest : IReplicationRequest + { + private readonly HttpRequest request; + + /// <summary> + /// Creates a <see cref="IReplicationRequest"/> wrapper around the provided <see cref="HttpRequest"/> + /// </summary> + /// <param name="request">the request to wrap</param> + public AspNetCoreReplicationRequest(HttpRequest request) + { + this.request = request; + } + + /// <summary> + /// Provides the requested path which mapps to a replication operation. + /// </summary> + public string Path { get { return request.PathBase + request.Path; } } + + /// <summary> + /// Returns the requested query parameter or null if not present. + /// Throws an exception if the same parameter is provided multiple times. + /// </summary> + /// <param name="name">the name of the requested parameter</param> + /// <returns>the value of the requested parameter or null if not present</returns> + /// <exception cref="InvalidOperationException">More than one parameter with the name was given.</exception> + public string QueryParam(string name) + { + return request.Query[name].SingleOrDefault(); + } + } +} http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs b/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs new file mode 100644 index 0000000..e671101 --- /dev/null +++ b/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs @@ -0,0 +1,56 @@ +using System.IO; +using Lucene.Net.Replicator.Http; +using Lucene.Net.Replicator.Http.Abstractions; +using Microsoft.AspNetCore.Http; + +namespace Lucene.Net.Replicator.AspNetCore +{ + /// <summary> + /// Implementation of the <see cref="IReplicationResponse"/> abstraction for the AspNetCore framework. + /// </summary> + /// <remarks> + /// .NET Specific Implementation of the Lucene Replicator using AspNetCore + /// </remarks> + //Note: LUCENENET specific + public class AspNetCoreReplicationResponse : IReplicationResponse + { + private readonly HttpResponse response; + + /// <summary> + /// Creates a <see cref="IReplicationResponse"/> wrapper around the provided <see cref="HttpResponse"/> + /// </summary> + /// <param name="response">the response to wrap</param> + public AspNetCoreReplicationResponse(HttpResponse response) + { + this.response = response; + } + + /// <summary> + /// Gets or sets the http status code of the response. + /// </summary> + public int StatusCode + { + get { return response.StatusCode; } + set { response.StatusCode = value; } + } + + /// <summary> + /// The response content. + /// </summary> + /// <remarks> + /// This simply returns the <see cref="HttpResponse.Body"/>. + /// </remarks> + public Stream Body { get { return response.Body; } } + + /// <summary> + /// Flushes the reponse to the underlying response stream. + /// </summary> + /// <remarks> + /// This simply calls <see cref="Stream.Flush"/> on the <see cref="HttpResponse.Body"/>. + /// </remarks> + public void Flush() + { + response.Body.Flush(); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs b/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs new file mode 100644 index 0000000..f772bfd --- /dev/null +++ b/src/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs @@ -0,0 +1,17 @@ +using Lucene.Net.Replicator.Http; +using Microsoft.AspNetCore.Http; + +namespace Lucene.Net.Replicator.AspNetCore +{ + //Note: LUCENENET specific + public static class AspNetCoreReplicationServiceExtentions + { + /// <summary> + /// Extensiont method that mirrors the signature of <see cref="ReplicationService.Perform"/> using AspNetCore as implementation. + /// </summary> + public static void Perform(this ReplicationService self, HttpRequest request, HttpResponse response) + { + self.Perform(new AspNetCoreReplicationRequest(request), new AspNetCoreReplicationResponse(response)); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj b/src/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj new file mode 100644 index 0000000..cdb84b6 --- /dev/null +++ b/src/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{763CCB5A-E397-456A-AF47-7C6E228B1852}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Lucene.Net.Replicator.AspNetCore</RootNamespace> + <AssemblyName>Lucene.Net.Replicator.AspNetCore</AssemblyName> + <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Http.Abstractions, Version=1.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.AspNetCore.Http.Abstractions.1.0.3\lib\net451\Microsoft.AspNetCore.Http.Abstractions.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Microsoft.AspNetCore.Http.Features, Version=1.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.AspNetCore.Http.Features.1.0.3\lib\net451\Microsoft.AspNetCore.Http.Features.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Microsoft.Extensions.Primitives, Version=1.0.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.Extensions.Primitives.1.0.1\lib\netstandard1.0\Microsoft.Extensions.Primitives.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="System" /> + <Reference Include="System.ComponentModel.Composition" /> + <Reference Include="System.Core" /> + <Reference Include="System.Text.Encodings.Web, Version=4.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> + <HintPath>..\..\packages\System.Text.Encodings.Web.4.0.1\lib\netstandard1.0\System.Text.Encodings.Web.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Net.Http" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="AspNetCoreReplicationRequest.cs" /> + <Compile Include="AspNetCoreReplicationResponse.cs" /> + <Compile Include="AspNetCoreReplicationServiceExtentions.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="packages.config" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\Lucene.Net.Replicator\Lucene.Net.Replicator.csproj"> + <Project>{1F70D2DB-C1B3-4F78-9598-3E04E0C7EB06}</Project> + <Name>Lucene.Net.Replicator</Name> + </ProjectReference> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator.AspNetCore/Properties/AssemblyInfo.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator.AspNetCore/Properties/AssemblyInfo.cs b/src/Lucene.Net.Replicator.AspNetCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c0e4fd2 --- /dev/null +++ b/src/Lucene.Net.Replicator.AspNetCore/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Lucene.Net.Replicator.AspNetCore")] +[assembly: AssemblyDescription("AspNetCore implementation of request and response abstractions for the Lucene.Net.Replicator " + + "for the Lucene.Net full - text search engine library from The Apache Software Foundation.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyDefaultAlias("Lucene.Net.Replicator.AspNetCore")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("763ccb5a-e397-456a-af47-7c6e228b1852")] http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator.AspNetCore/packages.config ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator.AspNetCore/packages.config b/src/Lucene.Net.Replicator.AspNetCore/packages.config new file mode 100644 index 0000000..3ffed90 --- /dev/null +++ b/src/Lucene.Net.Replicator.AspNetCore/packages.config @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="Microsoft.AspNetCore.Http.Abstractions" version="1.0.3" targetFramework="net451" /> + <package id="Microsoft.AspNetCore.Http.Features" version="1.0.3" targetFramework="net451" /> + <package id="Microsoft.Extensions.Primitives" version="1.0.1" targetFramework="net451" /> + <package id="System.Resources.ResourceManager" version="4.0.1" targetFramework="net451" /> + <package id="System.Runtime" version="4.1.0" targetFramework="net451" /> + <package id="System.Text.Encodings.Web" version="4.0.1" targetFramework="net451" /> +</packages> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/ComponentWrapperInfoStream.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/ComponentWrapperInfoStream.cs b/src/Lucene.Net.Replicator/ComponentWrapperInfoStream.cs new file mode 100644 index 0000000..20d1e0e --- /dev/null +++ b/src/Lucene.Net.Replicator/ComponentWrapperInfoStream.cs @@ -0,0 +1,54 @@ +using Lucene.Net.Util; + +namespace Lucene.Net.Replicator +{ + /// <summary> + /// Wraps a InfoStream for a specific component. + /// This is intented to make it a little easier to work with the InfoStreams. + /// </summary> + /// <remarks> + /// .NET Specific + /// </remarks> + public sealed class ComponentWrapperInfoStream : InfoStream + { + private readonly string component; + private readonly InfoStream innerStream; + + public ComponentWrapperInfoStream(string component, InfoStream innerStream) + { + this.component = component; + this.innerStream = innerStream; + } + + public override void Message(string component, string message) + { + if (IsEnabled(component)) + innerStream.Message(component, message); + } + + public bool IsEnabled() + { + return IsEnabled(component); + } + + public override bool IsEnabled(string component) + { + return innerStream.IsEnabled(component); + } + + public override object Clone() + { + return new ComponentWrapperInfoStream(component, (InfoStream)innerStream.Clone()); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + innerStream.Dispose(); + } + base.Dispose(disposing); + } + + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationRequest.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationRequest.cs b/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationRequest.cs new file mode 100644 index 0000000..ad869af --- /dev/null +++ b/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationRequest.cs @@ -0,0 +1,27 @@ +namespace Lucene.Net.Replicator.Http.Abstractions +{ + /// <summary> + /// Abstraction for remote replication requests, allows easy integration into any hosting frameworks. + /// </summary> + /// <remarks> + /// .NET Specific Abstraction + /// </remarks> + //Note: LUCENENET specific + public interface IReplicationRequest + { + /// <summary> + /// Provides the requested path which mapps to a replication operation. + /// </summary> + string Path { get; } + + /// <summary> + /// Returns the requested query parameter or null if not present. + /// </summary> + /// <remarks> + /// May though execeptions if the same parameter is provided multiple times, consult the documentation for the specific implementation. + /// </remarks> + /// <param name="name">the name of the requested parameter</param> + /// <returns>the value of the requested parameter or null if not present</returns> + string QueryParam(string name); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationResponse.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationResponse.cs b/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationResponse.cs new file mode 100644 index 0000000..e46f358 --- /dev/null +++ b/src/Lucene.Net.Replicator/Http/Abstractions/IReplicationResponse.cs @@ -0,0 +1,29 @@ +using System.IO; + +namespace Lucene.Net.Replicator.Http.Abstractions +{ + /// <summary> + /// Abstraction for remote replication response, allows easy integration into any hosting frameworks. + /// </summary> + /// <remarks> + /// .NET Specific Abstraction + /// </remarks> + //Note: LUCENENET specific + public interface IReplicationResponse + { + /// <summary> + /// Gets or sets the http status code of the response. + /// </summary> + int StatusCode { get; set; } + + /// <summary> + /// The response content. + /// </summary> + Stream Body { get; } + + /// <summary> + /// Flushes the reponse to the underlying response stream. + /// </summary> + void Flush(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/Http/EnumerableExtensions.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/Http/EnumerableExtensions.cs b/src/Lucene.Net.Replicator/Http/EnumerableExtensions.cs new file mode 100644 index 0000000..5247f40 --- /dev/null +++ b/src/Lucene.Net.Replicator/Http/EnumerableExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Lucene.Net.Replicator.Http +{ + /// <summary> + /// + /// </summary> + /// <remarks> + /// .NET Specific Helper Extensions for IEnumerable + /// </remarks> + //Note: LUCENENET specific + public static class EnumerableExtensions + { + public static IEnumerable<TOut> InPairs<T, TOut>(this IEnumerable<T> list, Func<T, T, TOut> join) + { + using (var enumerator = list.GetEnumerator()) + { + while (true) + { + if (!enumerator.MoveNext()) + yield break; + + T x = enumerator.Current; + if (!enumerator.MoveNext()) + yield return join(x, default(T)); + yield return join(x, enumerator.Current); + } + } + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/Http/HttpClientBase.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/Http/HttpClientBase.cs b/src/Lucene.Net.Replicator/Http/HttpClientBase.cs new file mode 100644 index 0000000..17f1c3a --- /dev/null +++ b/src/Lucene.Net.Replicator/Http/HttpClientBase.cs @@ -0,0 +1,516 @@ +//STATUS: DRAFT - 4.8.0 + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Lucene.Net.Replicator.Http +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// <summary> + /// Base class for Http clients. + /// </summary> + /// <remarks> + /// Lucene.Experimental + /// </remarks> + public abstract class HttpClientBase : IDisposable + { + /// <summary> + /// Default connection timeout for this client, in milliseconds. + /// <see cref="ConnectionTimeout"/> + /// </summary> + public const int DEFAULT_CONNECTION_TIMEOUT = 1000; + + /** + * Default socket timeout for this client, in milliseconds. + * + * @see #setSoTimeout(int) + */ + //TODO: This goes to the Read and Write timeouts in the request (Closest we can get to a socket timeout in .NET?), those should be controlled in the messageHandler + // if the choosen messagehandler provides such mechanishm (e.g. the WebRequestHandler) so this doesn't seem to make sense in .NET. + //public const int DEFAULT_SO_TIMEOUT = 60000; + + // TODO compression? + + /// <summary> + /// The URL to execute requests against. + /// </summary> + protected string Url { get; private set; } + + private readonly HttpClient httpc; + + //JAVA: /** + //JAVA: * Set the connection timeout for this client, in milliseconds. This setting + //JAVA: * is used to modify {@link HttpConnectionParams#setConnectionTimeout}. + //JAVA: * + //JAVA: * @param timeout timeout to set, in millisecopnds + //JAVA: */ + //JAVA: public void setConnectionTimeout(int timeout) { + //JAVA: HttpConnectionParams.setConnectionTimeout(httpc.getParams(), timeout); + //JAVA: } + /// <summary> + /// Gets or Sets the connection timeout for this client, in milliseconds. This setting + /// is used to modify <see cref="HttpClient.Timeout"/>. + /// </summary> + public int ConnectionTimeout + { + get { return (int) httpc.Timeout.TotalMilliseconds; } + set { httpc.Timeout = TimeSpan.FromMilliseconds(value); } + } + + //JAVA: /** + //JAVA: * Set the socket timeout for this client, in milliseconds. This setting + //JAVA: * is used to modify {@link HttpConnectionParams#setSoTimeout}. + //JAVA: * + //JAVA: * @param timeout timeout to set, in millisecopnds + //JAVA: */ + //JAVA: public void setSoTimeout(int timeout) { + //JAVA: HttpConnectionParams.setSoTimeout(httpc.getParams(), timeout); + //JAVA: } + //TODO: This goes to the Read and Write timeouts in the request (Closest we can get to a socket timeout in .NET?), those should be controlled in the messageHandler + // if the choosen messagehandler provides such mechanishm (e.g. the WebRequestHandler) so this doesn't seem to make sense in .NET. + //public int SoTimeout { get; set; } + + /// <summary> + /// Returns true if this instance was <see cref="Dispose(bool)"/>ed, otherwise + /// returns false. Note that if you override <see cref="Dispose(bool)"/>, you must call + /// <see cref="Dispose(bool)"/> on the base class, in order for this instance to be properly disposed. + /// </summary> + public bool IsDisposed { get; private set; } + + //TODO: HttpMessageHandler is not really a replacement for the ClientConnectionManager, allowing for custom message handlers will + // provide flexibility, this is AFAIK also where users would be able to controll the equivalent of the SO timeout. + protected HttpClientBase(string host, int port, string path, HttpMessageHandler messageHandler) + { + IsDisposed = false; + + #region Java + //JAVA: /** + //JAVA: * @param conMgr connection manager to use for this http client. + //JAVA: * <b>NOTE:</b>The provided {@link ClientConnectionManager} will not be + //JAVA: * {@link ClientConnectionManager#shutdown()} by this class. + //JAVA: */ + //JAVA: protected HttpClientBase(String host, int port, String path, ClientConnectionManager conMgr) { + //JAVA: url = normalizedURL(host, port, path); + //JAVA: httpc = new DefaultHttpClient(conMgr); + //JAVA: setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT); + //JAVA: setSoTimeout(DEFAULT_SO_TIMEOUT); + //JAVA: } + #endregion + + Url = NormalizedUrl(host, port, path); + httpc = new HttpClient(messageHandler ?? new HttpClientHandler()); + httpc.Timeout = TimeSpan.FromMilliseconds(DEFAULT_CONNECTION_TIMEOUT); + } + + /// <summary> + /// Throws <see cref="ObjectDisposedException"/> if this client is already closed. + /// </summary> + /// <exception cref="ObjectDisposedException">client is already closed.</exception> + protected void EnsureOpen() + { + #region Java + //JAVA: protected final void ensureOpen() throws AlreadyClosedException { + //JAVA: if (closed) { + //JAVA: throw new AlreadyClosedException("HttpClient already closed"); + //JAVA: } + //JAVA: } + #endregion + + if (IsDisposed) + { + throw new ObjectDisposedException("HttpClient already closed"); + } + } + + private static string NormalizedUrl(string host, int port, string path) + { + #region Java + //JAVA: /** + //JAVA: * Create a URL out of the given parameters, translate an empty/null path to '/' + //JAVA: */ + //JAVA: private static String normalizedURL(String host, int port, String path) { + //JAVA: if (path == null || path.length() == 0) { + //JAVA: path = "/"; + //JAVA: } + //JAVA: return "http://" + host + ":" + port + path; + //JAVA: } + #endregion + + if (string.IsNullOrEmpty(path)) + path = "/"; + return string.Format("http://{0}:{1}{2}", host, port, path); + } + + /// <summary> + /// Verifies the response status and if not successfull throws an exception. + /// </summary> + /// <exception cref="IOException">IO Error happened at the server, check inner exception for details.</exception> + /// <exception cref="HttpRequestException">Unknown error received from the server.</exception> + protected void VerifyStatus(HttpResponseMessage response) + { + #region Java + //JAVA: + //JAVA: /** + //JAVA: * <b>Internal:</b> response status after invocation, and in case or error attempt to read the + //JAVA: * exception sent by the server. + //JAVA: */ + //JAVA: protected void verifyStatus(HttpResponse response) throws IOException { + //JAVA: StatusLine statusLine = response.getStatusLine(); + //JAVA: if (statusLine.getStatusCode() != HttpStatus.SC_OK) { + //JAVA: throwKnownError(response, statusLine); + //JAVA: } + //JAVA: } + #endregion + + if (!response.IsSuccessStatusCode) + { + ThrowKnownError(response); + } + } + + /// <summary> + /// Throws an exception for any errors. + /// </summary> + /// <exception cref="IOException">IO Error happened at the server, check inner exception for details.</exception> + /// <exception cref="HttpRequestException">Unknown error received from the server.</exception> + protected void ThrowKnownError(HttpResponseMessage response) + { + #region Java + //JAVA: protected void throwKnownError(HttpResponse response, StatusLine statusLine) throws IOException { + //JAVA: ObjectInputStream in = null; + //JAVA: try { + //JAVA: in = new ObjectInputStream(response.getEntity().getContent()); + //JAVA: } catch (Exception e) { + //JAVA: // the response stream is not an exception - could be an error in servlet.init(). + //JAVA: throw new RuntimeException("Uknown error: " + statusLine); + //JAVA: } + //JAVA: + //JAVA: Throwable t; + //JAVA: try { + //JAVA: t = (Throwable) in.readObject(); + //JAVA: } catch (Exception e) { + //JAVA: //not likely + //JAVA: throw new RuntimeException("Failed to read exception object: " + statusLine, e); + //JAVA: } finally { + //JAVA: in.close(); + //JAVA: } + //JAVA: if (t instanceof IOException) { + //JAVA: throw (IOException) t; + //JAVA: } + //JAVA: if (t instanceof RuntimeException) { + //JAVA: throw (RuntimeException) t; + //JAVA: } + //JAVA: throw new RuntimeException("unknown exception "+statusLine,t); + //JAVA: } + #endregion + + Stream input; + try + { + //.NET Note: Bridging from Async to Sync, this is not ideal and we could consider changing the interface to be Async or provide Async overloads + // and have these Sync methods with their caveats. + input = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception) + { + // the response stream is not an exception - could be an error in servlet.init(). + //JAVA: throw new RuntimeException("Uknown error: " + statusLine); + response.EnsureSuccessStatusCode(); + //Note: This is unreachable, but the compiler and resharper cant see that EnsureSuccessStatusCode always + // throws an exception in this scenario. So it complains later on in the method. + throw; + } + + Exception exception; + try + { + TextReader reader = new StreamReader(input); + JsonSerializer serializer = JsonSerializer.Create(ReplicationService.JSON_SERIALIZER_SETTINGS); + exception = (Exception)serializer.Deserialize(new JsonTextReader(reader)); + } + catch (Exception e) + { + //not likely + throw new HttpRequestException(string.Format("Failed to read exception object: {0} {1}", response.StatusCode, response.ReasonPhrase), e); + } + finally + { + input.Dispose(); + } + + if (exception is IOException) + { + //NOTE: Preserve server stacktrace, but there are probably better options. + throw new IOException(exception.Message, exception); + } + throw new HttpRequestException(string.Format("unknown exception: {0} {1}", response.StatusCode, response.ReasonPhrase), exception); + } + + protected HttpResponseMessage ExecutePost(string request, object entity, params string[] parameters) + { + #region Java + //JAVA: /** + //JAVA: * <b>internal:</b> execute a request and return its result + //JAVA: * The <code>params</code> argument is treated as: name1,value1,name2,value2,... + //JAVA: */ + //JAVA: protected HttpResponse executePOST(String request, HttpEntity entity, String... params) throws IOException { + //JAVA: ensureOpen(); + //JAVA: HttpPost m = new HttpPost(queryString(request, params)); + //JAVA: m.setEntity(entity); + //JAVA: HttpResponse response = httpc.execute(m); + //JAVA: verifyStatus(response); + //JAVA: return response; + //JAVA: } + #endregion + + EnsureOpen(); + //.NET Note: No headers? No ContentType?... Bad use of Http? + HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters)); + + req.Content = new StringContent(JToken.FromObject(entity, JsonSerializer.Create(ReplicationService.JSON_SERIALIZER_SETTINGS)) + .ToString(Formatting.None), Encoding.UTF8, "application/json"); + + //.NET Note: Bridging from Async to Sync, this is not ideal and we could consider changing the interface to be Async or provide Async overloads + // and have these Sync methods with their caveats. + HttpResponseMessage response = httpc.SendAsync(req).ConfigureAwait(false).GetAwaiter().GetResult(); + VerifyStatus(response); + return response; + } + + protected HttpResponseMessage ExecuteGet(string request, params string[] parameters) + { + #region Java + //JAVA: /** + //JAVA: * <b>internal:</b> execute a request and return its result + //JAVA: * The <code>params</code> argument is treated as: name1,value1,name2,value2,... + //JAVA: */ + //JAVA: protected HttpResponse executeGET(String request, String... params) throws IOException { + //JAVA: ensureOpen(); + //JAVA: HttpGet m = new HttpGet(queryString(request, params)); + //JAVA: HttpResponse response = httpc.execute(m); + //JAVA: verifyStatus(response); + //JAVA: return response; + //JAVA: } + #endregion + + EnsureOpen(); + //Note: No headers? No ContentType?... Bad use of Http? + HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, QueryString(request, parameters)); + //.NET Note: Bridging from Async to Sync, this is not ideal and we could consider changing the interface to be Async or provide Async overloads + // and have these Sync methods with their caveats. + HttpResponseMessage response = httpc.SendAsync(req).ConfigureAwait(false).GetAwaiter().GetResult(); + VerifyStatus(response); + return response; + } + + private string QueryString(string request, params string[] parameters) + { + #region Java + //JAVA: private String queryString(String request, String... params) throws UnsupportedEncodingException { + //JAVA: StringBuilder query = new StringBuilder(url).append('/').append(request).append('?'); + //JAVA: if (params != null) { + //JAVA: for (int i = 0; i < params.length; i += 2) { + //JAVA: query.append(params[i]).append('=').append(URLEncoder.encode(params[i+1], "UTF8")).append('&'); + //JAVA: } + //JAVA: } + //JAVA: return query.substring(0, query.length() - 1); + //JAVA: } + #endregion + + return parameters == null + ? string.Format("{0}/{1}", Url, request) + : string.Format("{0}/{1}?{2}", Url, request, string + .Join("&", parameters.Select(WebUtility.UrlEncode).InPairs((key, val) => string.Format("{0}={1}", key, val)))); + } + + /// <summary> + /// Internal utility: input stream of the provided response + /// </summary> + /// <exception cref="IOException"></exception> + public Stream ResponseInputStream(HttpResponseMessage response)// throws IOException + { + #region Java + //JAVA: /** Internal utility: input stream of the provided response */ + //JAVA: public InputStream responseInputStream(HttpResponse response) throws IOException { + //JAVA: return responseInputStream(response, false); + //JAVA: } + #endregion + + return ResponseInputStream(response, false); + } + + /// <summary> + /// Internal utility: input stream of the provided response + /// </summary> + /// <exception cref="IOException"></exception> + public Stream ResponseInputStream(HttpResponseMessage response, bool consume)// throws IOException + { + #region Java + //JAVA: TODO: can we simplify this Consuming !?!?!? + //JAVA: /** + //JAVA: * Internal utility: input stream of the provided response, which optionally + //JAVA: * consumes the response's resources when the input stream is exhausted. + //JAVA: */ + //JAVA: public InputStream responseInputStream(HttpResponse response, boolean consume) throws IOException { + //JAVA: final HttpEntity entity = response.getEntity(); + //JAVA: final InputStream in = entity.getContent(); + //JAVA: if (!consume) { + //JAVA: return in; + //JAVA: } + //JAVA: return new InputStream() { + //JAVA: private boolean consumed = false; + //JAVA: @Override + //JAVA: public int read() throws IOException { + //JAVA: final int res = in.read(); + //JAVA: consume(res); + //JAVA: return res; + //JAVA: } + //JAVA: @Override + //JAVA: public void close() throws IOException { + //JAVA: super.close(); + //JAVA: consume(-1); + //JAVA: } + //JAVA: @Override + //JAVA: public int read(byte[] b) throws IOException { + //JAVA: final int res = super.read(b); + //JAVA: consume(res); + //JAVA: return res; + //JAVA: } + //JAVA: @Override + //JAVA: public int read(byte[] b, int off, int len) throws IOException { + //JAVA: final int res = super.read(b, off, len); + //JAVA: consume(res); + //JAVA: return res; + //JAVA: } + //JAVA: private void consume(int minusOne) { + //JAVA: if (!consumed && minusOne==-1) { + //JAVA: try { + //JAVA: EntityUtils.consume(entity); + //JAVA: } catch (Exception e) { + //JAVA: // ignored on purpose + //JAVA: } + //JAVA: consumed = true; + //JAVA: } + //JAVA: } + //JAVA: }; + //JAVA: } + #endregion + + return response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + protected T DoAction<T>(HttpResponseMessage response, Func<T> call) + { + #region Java + //JAVA: /** + //JAVA: * Same as {@link #doAction(HttpResponse, boolean, Callable)} but always do consume at the end. + //JAVA: */ + //JAVA: protected <T> T doAction(HttpResponse response, Callable<T> call) throws IOException { + //JAVA: return doAction(response, true, call); + //JAVA: } + #endregion + + return DoAction(response, true, call); + } + + protected T DoAction<T>(HttpResponseMessage response, bool consume, Func<T> call) + { + #region Java + //JAVA: /** + //JAVA: * Do a specific action and validate after the action that the status is still OK, + //JAVA: * and if not, attempt to extract the actual server side exception. Optionally + //JAVA: * release the response at exit, depending on <code>consume</code> parameter. + //JAVA: */ + //JAVA: protected <T> T doAction(HttpResponse response, boolean consume, Callable<T> call) throws IOException { + //JAVA: IOException error = null; + //JAVA: try { + //JAVA: return call.call(); + //JAVA: } catch (IOException e) { + //JAVA: error = e; + //JAVA: } catch (Exception e) { + //JAVA: error = new IOException(e); + //JAVA: } finally { + //JAVA: try { + //JAVA: verifyStatus(response); + //JAVA: } finally { + //JAVA: if (consume) { + //JAVA: try { + //JAVA: EntityUtils.consume(response.getEntity()); + //JAVA: } catch (Exception e) { + //JAVA: // ignoring on purpose + //JAVA: } + //JAVA: } + //JAVA: } + //JAVA: } + //JAVA: throw error; // should not get here + //JAVA: } + #endregion + + Exception error = new NotImplementedException(); + try + { + return call(); + } + catch (IOException e) + { + error = e; + } + catch (Exception e) + { + error = new IOException(e.Message, e); + } + finally + { + try + { + VerifyStatus(response); + } + finally + { + //TODO: Is there any reason for this on .NET?... What are they trying to achieve? + //JAVA: if (consume) { + //JAVA: try { + //JAVA: EntityUtils.consume(response.getEntity()); + //JAVA: } catch (Exception e) { + //JAVA: // ignoring on purpose + //JAVA: } + //JAVA: } + } + } + throw error; // should not get here + } + + protected virtual void Dispose(bool disposing) + { + IsDisposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/Http/HttpReplicator.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/Http/HttpReplicator.cs b/src/Lucene.Net.Replicator/Http/HttpReplicator.cs new file mode 100644 index 0000000..90df85b --- /dev/null +++ b/src/Lucene.Net.Replicator/Http/HttpReplicator.cs @@ -0,0 +1,145 @@ +//STATUS: DRAFT - 4.8.0 + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Lucene.Net.Support.IO; + +namespace Lucene.Net.Replicator.Http +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// <summary> + /// An HTTP implementation of <see cref="IReplicator"/>. Assumes the API supported by <see cref="ReplicationService"/>. + /// </summary> + /// <remarks> + /// Lucene.Experimental + /// </remarks> + public class HttpReplicator : HttpClientBase, IReplicator + { + public HttpReplicator(string host, int port, string path, HttpMessageHandler messageHandler) + : base(host, port, path, messageHandler) + { + #region Java + //JAVA: /** Construct with specified connection manager. */ + //JAVA: public HttpReplicator(String host, int port, String path, ClientConnectionManager conMgr) { + //JAVA: super(host, port, path, conMgr); + //JAVA: } + #endregion + } + + /// <summary> + /// + /// </summary> + /// <param name="revision"></param> + /// <exception cref="NotSupportedException">this replicator implementation does not support remote publishing of revisions</exception> + public void Publish(IRevision revision) + { + throw new NotSupportedException("this replicator implementation does not support remote publishing of revisions"); + } + + public SessionToken CheckForUpdate(string currentVersion) + { + #region Java + //JAVA: public SessionToken checkForUpdate(String currVersion) throws IOException { + //JAVA: String[] params = null; + //JAVA: if (currVersion != null) { + //JAVA: params = new String[] { ReplicationService.REPLICATE_VERSION_PARAM, currVersion }; + //JAVA: } + //JAVA: final HttpResponse response = executeGET(ReplicationAction.UPDATE.name(), params); + //JAVA: return doAction(response, new Callable<SessionToken>() { + //JAVA: @Override + //JAVA: public SessionToken call() throws Exception { + //JAVA: final DataInputStream dis = new DataInputStream(responseInputStream(response)); + //JAVA: try { + //JAVA: if (dis.readByte() == 0) { + //JAVA: return null; + //JAVA: } else { + //JAVA: return new SessionToken(dis); + //JAVA: } + //JAVA: } finally { + //JAVA: dis.close(); + //JAVA: } + //JAVA: } + //JAVA: }); + //JAVA: } + #endregion + + string[] parameters = null; + if (currentVersion != null) + parameters = new [] { ReplicationService.REPLICATE_VERSION_PARAM, currentVersion }; + + HttpResponseMessage response = base.ExecuteGet( ReplicationService.ReplicationAction.UPDATE.ToString(), parameters); + return DoAction(response, () => + { + using (DataInputStream inputStream = new DataInputStream(ResponseInputStream(response))) + { + return inputStream.ReadByte() == 0 ? null : new SessionToken(inputStream); + } + }); + } + + public void Release(string sessionId) + { + #region Java + //JAVA: public void release(String sessionID) throws IOException { + //JAVA: String[] params = new String[] { + //JAVA: ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionID + //JAVA: }; + //JAVA: final HttpResponse response = executeGET(ReplicationAction.RELEASE.name(), params); + //JAVA: doAction(response, new Callable<Object>() { + //JAVA: @Override + //JAVA: public Object call() throws Exception { + //JAVA: return null; // do not remove this call: as it is still validating for us! + //JAVA: } + //JAVA: }); + //JAVA: } + #endregion + + HttpResponseMessage response = ExecuteGet(ReplicationService.ReplicationAction.RELEASE.ToString(), ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionId); + // do not remove this call: as it is still validating for us! + DoAction<object>(response, () => null); + } + + public Stream ObtainFile(string sessionId, string source, string fileName) + { + #region Java + //JAVA: public InputStream obtainFile(String sessionID, String source, String fileName) throws IOException { + //JAVA: String[] params = new String[] { + //JAVA: ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionID, + //JAVA: ReplicationService.REPLICATE_SOURCE_PARAM, source, + //JAVA: ReplicationService.REPLICATE_FILENAME_PARAM, fileName, + //JAVA: }; + //JAVA: final HttpResponse response = executeGET(ReplicationAction.OBTAIN.name(), params); + //JAVA: return doAction(response, false, new Callable<InputStream>() { + //JAVA: @Override + //JAVA: public InputStream call() throws Exception { + //JAVA: return responseInputStream(response,true); + //JAVA: } + //JAVA: }); + //JAVA: } + #endregion + HttpResponseMessage response = ExecuteGet(ReplicationService.ReplicationAction.OBTAIN.ToString(), + ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionId, + ReplicationService.REPLICATE_SOURCE_PARAM, source, + ReplicationService.REPLICATE_FILENAME_PARAM, fileName); + return DoAction(response, false, () => ResponseInputStream(response)); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/Http/ReplicationService.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/Http/ReplicationService.cs b/src/Lucene.Net.Replicator/Http/ReplicationService.cs new file mode 100644 index 0000000..d004fc7 --- /dev/null +++ b/src/Lucene.Net.Replicator/Http/ReplicationService.cs @@ -0,0 +1,211 @@ +//STATUS: DRAFT - 4.8.0 + +using System; +using System.Collections.Generic; +using System.IO; +using Lucene.Net.Replicator.Http.Abstractions; +using Lucene.Net.Support.IO; +using Newtonsoft.Json; + +namespace Lucene.Net.Replicator.Http +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// <summary> + /// A server-side service for handling replication requests. The service assumes + /// requests are sent in the format <code>/<context>/<shard>/<action></code> where + /// <ul> + /// <li><code>context</code> is the servlet context, e.g. <see cref="REPLICATION_CONTEXT"/></li> + /// <li><code>shard</code> is the ID of the shard, e.g. "s1"</li> + /// <li><code>action</code> is one of <see cref="ReplicationAction"/> values</li> + /// </ul> + /// For example, to check whether there are revision updates for shard "s1" you + /// should send the request: <code>http://host:port/replicate/s1/update</code>. + /// </summary> + /// <remarks> + /// This service is written using abstractions over requests and responses which makes it easy + /// to integrate into any hosting framework. + /// <p> + /// See the Lucene.Net.Replicator.AspNetCore for an example of an implementation for the AspNetCore Framework. + /// </p> + /// </remarks> + /// <remarks> + /// Lucene.Experimental + /// </remarks> + public class ReplicationService + { + /// <summary> + /// Actions supported by the <see cref="ReplicationService"/>. + /// </summary> + public enum ReplicationAction + { + OBTAIN, RELEASE, UPDATE + } + + /// <summary> + /// The default context path for the <see cref="ReplicationService"/>. + /// </summary> + public const string REPLICATION_CONTEXT = "/replicate"; + + /// <summary> + /// Request parameter name for providing the revision version. + /// </summary> + public const string REPLICATE_VERSION_PARAM = "version"; + + /// <summary> + /// Request parameter name for providing a session ID. + /// </summary> + public const string REPLICATE_SESSION_ID_PARAM = "sessionid"; + + /// <summary> + /// Request parameter name for providing the file's source. + /// </summary> + public const string REPLICATE_SOURCE_PARAM = "source"; + + /// <summary> + /// Request parameter name for providing the file's name. + /// </summary> + public const string REPLICATE_FILENAME_PARAM = "filename"; + + /// <summary> + /// Json Serializer Settings to use when serializing and deserializing errors. + /// </summary> + public static readonly JsonSerializerSettings JSON_SERIALIZER_SETTINGS = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + + private const int SHARD_IDX = 0, ACTION_IDX = 1; + + private readonly string context; + private readonly IDictionary<string, IReplicator> replicators; + + public ReplicationService(IDictionary<string, IReplicator> replicators, string context = REPLICATION_CONTEXT) + { + this.context = context; + this.replicators = replicators; + } + + /// <summary> + /// Returns the path elements that were given in the servlet request, excluding the servlet's action context. + /// </summary> + private string[] GetPathElements(IReplicationRequest request) + { + string path = request.Path; + + int actionLength = context.Length; + int startIndex = actionLength; + + if (path.Length > actionLength && path[actionLength] == '/') + ++startIndex; + + return path.Substring(startIndex).Split('/'); + } + + private static string ExtractRequestParam(IReplicationRequest request, string paramName) + { + string param = request.QueryParam(paramName); + if (param == null) + { + //JAVA: throw new ServletException("Missing mandatory parameter: " + paramName); + throw new InvalidOperationException("Missing mandatory parameter: " + paramName); + } + return param; + } + + + /// <summary> + /// Executes the replication task. + /// </summary> + /// <exception cref="InvalidOperationException">required parameters are missing</exception> + public void Perform(IReplicationRequest request, IReplicationResponse response) + { + string[] pathElements = GetPathElements(request); + if (pathElements.Length != 2) + { + throw new InvalidOperationException("invalid path, must contain shard ID and action, e.g. */s1/update"); + } + + ReplicationAction action; + if (!Enum.TryParse(pathElements[ACTION_IDX], true, out action)) + { + throw new InvalidOperationException("Unsupported action provided: " + pathElements[ACTION_IDX]); + } + + IReplicator replicator; + if (!replicators.TryGetValue(pathElements[SHARD_IDX], out replicator)) + { + throw new InvalidOperationException("unrecognized shard ID " + pathElements[SHARD_IDX]); + } + + // SOLR-8933 Don't close this stream. + try + { + switch (action) + { + case ReplicationAction.OBTAIN: + string sessionId = ExtractRequestParam(request, REPLICATE_SESSION_ID_PARAM); + string fileName = ExtractRequestParam(request, REPLICATE_FILENAME_PARAM); + string source = ExtractRequestParam(request, REPLICATE_SOURCE_PARAM); + using (Stream stream = replicator.ObtainFile(sessionId, source, fileName)) + stream.CopyTo(response.Body); + break; + + case ReplicationAction.RELEASE: + replicator.Release(ExtractRequestParam(request, REPLICATE_SESSION_ID_PARAM)); + break; + + case ReplicationAction.UPDATE: + string currentVersion = request.QueryParam(REPLICATE_VERSION_PARAM); + SessionToken token = replicator.CheckForUpdate(currentVersion); + if (token == null) + { + response.Body.Write(new byte[] { 0 }, 0, 1); // marker for null token + } + else + { + response.Body.Write(new byte[] { 1 }, 0, 1); + token.Serialize(new DataOutputStream(response.Body)); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + catch (Exception e) + { + response.StatusCode = 500; + try + { + TextWriter writer = new StreamWriter(response.Body); + JsonSerializer serializer = JsonSerializer.Create(JSON_SERIALIZER_SETTINGS); + serializer.Serialize(writer, e, e.GetType()); + } + catch (Exception exception) + { + throw new IOException("Could not serialize", exception); + } + } + finally + { + response.Flush(); + } + } + + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/Http/package.html ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/Http/package.html b/src/Lucene.Net.Replicator/Http/package.html new file mode 100644 index 0000000..fce050b --- /dev/null +++ b/src/Lucene.Net.Replicator/Http/package.html @@ -0,0 +1,28 @@ +<html> + +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<head> +<title>HTTP replication implementation</title> +</head> + +<body> +<h1>HTTP replication implementation</h1> +</body> + +</html> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/IReplicationHandler.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/IReplicationHandler.cs b/src/Lucene.Net.Replicator/IReplicationHandler.cs new file mode 100644 index 0000000..1976435 --- /dev/null +++ b/src/Lucene.Net.Replicator/IReplicationHandler.cs @@ -0,0 +1,32 @@ +//STATUS: DRAFT - 4.8.0 +using System; +using System.Collections.Generic; +using System.IO; +using Directory = Lucene.Net.Store.Directory; + +namespace Lucene.Net.Replicator +{ + /// <summary>Handler for revisions obtained by the client.</summary> + //Note: LUCENENET specific denesting of interface + public interface IReplicationHandler + { + /// <summary>Returns the current revision files held by the handler.</summary> + string CurrentVersion { get; } + + /// <summary>Returns the current revision version held by the handler.</summary> + IDictionary<string, IList<RevisionFile>> CurrentRevisionFiles { get; } + + /// <summary> + /// Called when a new revision was obtained and is available (i.e. all needed files were successfully copied). + /// </summary> + /// <param name="version">The version of the <see cref="IRevision"/> that was copied</param> + /// <param name="revisionFiles"> the files contained by this <see cref="IRevision"/></param> + /// <param name="copiedFiles">the files that were actually copied</param> + /// <param name="sourceDirectory">a mapping from a source of files to the <see cref="Directory"/> they were copied into</param> + /// <see cref="IOException"/> + void RevisionReady(string version, + IDictionary<string, IList<RevisionFile>> revisionFiles, + IDictionary<string, IList<string>> copiedFiles, + IDictionary<string, Directory> sourceDirectory); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/6da4dd20/src/Lucene.Net.Replicator/ISourceDirectoryFactory.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Replicator/ISourceDirectoryFactory.cs b/src/Lucene.Net.Replicator/ISourceDirectoryFactory.cs new file mode 100644 index 0000000..7942b91 --- /dev/null +++ b/src/Lucene.Net.Replicator/ISourceDirectoryFactory.cs @@ -0,0 +1,27 @@ +using Lucene.Net.Store; + +namespace Lucene.Net.Replicator +{ + /// <summary> + /// Resolves a session and source into a <see cref="Directory"/> to use for copying + /// the session files to. + /// </summary> + //Note: LUCENENET specific denesting of interface + public interface ISourceDirectoryFactory + { + /// <summary> + /// Returns the <see cref="Directory"/> to use for the given session and source. + /// Implementations may e.g. return different directories for different + /// sessions, or the same directory for all sessions. In that case, it is + /// advised to clean the directory before it is used for a new session. + /// </summary> + /// <seealso cref="CleanupSession"/> + Directory GetDirectory(string sessionId, string source); //throws IOException; + + /// <summary> + /// Called to denote that the replication actions for this session were finished and the directory is no longer needed. + /// </summary> + /// <exception cref="System.IO.IOException"></exception> + void CleanupSession(string sessionId); + } +} \ No newline at end of file
