From 9dd2d3e020bc4298ba8550f844a113261b4a3af4 Mon Sep 17 00:00:00 2001 From: Ogoun Date: Fri, 22 Mar 2019 17:34:52 +0300 Subject: [PATCH] First --- ZeroLevel.Discovery/App.config | 18 + .../Controllers/BaseController.cs | 95 + .../HttpRequestMessagesExtensions.cs | 72 + .../Controllers/RoutesController.cs | 55 + ZeroLevel.Discovery/DiscoveryService.cs | 32 + .../ExchangeTransportFactory.cs | 110 + ZeroLevel.Discovery/Program.cs | 10 + .../Properties/AssemblyInfo.cs | 36 + ZeroLevel.Discovery/RouteTable.cs | 246 ++ ZeroLevel.Discovery/Startup.cs | 79 + .../ZeroLevel.Discovery.csproj | 97 + ...le_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs | 0 ...le_5937a670-0e60-4077-877b-f7221da3dda1.cs | 0 ...le_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs | 0 ...l.Discovery.csproj.CoreCompileInputs.cache | 1 + ...el.Discovery.csprojAssemblyReference.cache | Bin 0 -> 27737 bytes ZeroLevel.Discovery/packages.config | 12 + ZeroLevel.Microservices/BaseProxy.cs | 158 + .../Contracts/IDiscoveryClient.cs | 15 + .../Contracts/IExchangeService.cs | 13 + ZeroLevel.Microservices/ExServiceHost.cs | 519 ++++ ZeroLevel.Microservices/Exchange.cs | 640 ++++ .../ExchangeTransportFactory.cs | 110 + ZeroLevel.Microservices/Model/Checkpoint.cs | 101 + .../Model/CheckpointArc.cs | 21 + .../Model/CheckpointType.cs | 10 + .../Properties/AssemblyInfo.cs | 36 + .../WebApiDiscoveryClient.cs | 179 ++ .../ZeroLevel.Microservices.csproj | 71 + ...le_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs | 0 ...le_5937a670-0e60-4077-877b-f7221da3dda1.cs | 0 ...le_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs | 0 ...croservices.csproj.CoreCompileInputs.cache | 1 + ...icroservices.csprojAssemblyReference.cache | Bin 0 -> 27363 bytes ZeroLevel.Microservices/packages.config | 4 + ZeroLevel.sln | 65 + ZeroLevel/Models/BaseModel.cs | 39 + ZeroLevel/Models/BinaryDocument.cs | 121 + ZeroLevel/Models/IEntity.cs | 16 + ZeroLevel/Models/InvokeResult.cs | 111 + ZeroLevel/Properties/AssemblyInfo.cs | 36 + .../Application/BaseWindowsService.cs | 121 + .../Application/BasicServiceInstaller.cs | 114 + .../Application/BusinessApplication.cs | 161 + .../Services/Application/IZeroService.cs | 14 + .../Services/Application/ZeroServiceState.cs | 22 + .../Services/Async/AsyncConditionVariable.cs | 184 ++ ZeroLevel/Services/Async/AsyncHelper.cs | 38 + ZeroLevel/Services/Async/AsyncLock.cs | 191 ++ .../Services/Async/AsyncManualResetEvent.cs | 159 + ZeroLevel/Services/Async/AsyncMonitor.cs | 135 + .../Async/AsyncProducerConsumerQueue.cs | 737 +++++ .../Services/Async/AsyncReaderWriterLock.cs | 769 +++++ ZeroLevel/Services/Async/AsyncSemaphore.cs | 177 ++ ZeroLevel/Services/Async/AsyncWaitQueue.cs | 208 ++ .../Async/CancellationTokenHelpers.cs | 103 + .../Async/CancellationTokenTaskSource.cs | 47 + ZeroLevel/Services/Async/Deque.cs | 836 ++++++ ZeroLevel/Services/Async/ExceptionHelpers.cs | 23 + ZeroLevel/Services/Async/IdManager.cs | 48 + .../Async/NormalizedCancellationToken.cs | 61 + .../Services/Async/TaskCompletionSource.cs | 137 + .../Async/TaskCompletionSourceExtensions.cs | 278 ++ ZeroLevel/Services/Async/TaskConstants.cs | 143 + ZeroLevel/Services/Async/TaskExtensions.cs | 155 + ZeroLevel/Services/Async/TaskShim.cs | 54 + .../Services/Collections/EverythingStorage.cs | 141 + .../Services/Collections/FixSizeQueue.cs | 145 + .../Collections/IEverythingStorage.cs | 13 + .../Services/Collections/IFixSizeQueue.cs | 14 + .../Collections/RoundRobinCollection.cs | 132 + .../Services/Collections/SparseIterator.cs | 82 + .../Services/Config/BaseConfiguration.cs | 411 +++ .../Services/Config/BaseConfigurationSet.cs | 225 ++ ZeroLevel/Services/Config/Configuration.cs | 177 ++ ZeroLevel/Services/Config/IConfiguration.cs | 116 + .../Services/Config/IConfigurationReader.cs | 8 + .../Services/Config/IConfigurationSet.cs | 85 + .../Services/Config/IConfigurationWriter.cs | 8 + .../Implementation/AppWebConfigReader.cs | 105 + .../Implementation/ApplicationConfigReader.cs | 39 + .../Implementation/CommandLineReader.cs | 54 + .../Config/Implementation/IniFileReader.cs | 125 + .../Config/Implementation/IniFileWriter.cs | 79 + .../Services/DOM/Contracts/IContentReader.cs | 51 + .../Services/DOM/Contracts/IMetadataReader.cs | 61 + .../DOM/DSL/Contexts/TBlockContext.cs | 129 + .../Services/DOM/DSL/Contexts/TContext.cs | 27 + .../DOM/DSL/Contexts/TElementContext.cs | 78 + .../DOM/DSL/Contexts/TFunctionContext.cs | 187 ++ .../DOM/DSL/Contexts/TPropertyContext.cs | 185 ++ .../Services/DOM/DSL/Contexts/TRootContext.cs | 146 + .../DOM/DSL/Contracts/ISpecialTableBuilder.cs | 20 + .../Services/DOM/DSL/Contracts/TCloneable.cs | 9 + .../DOM/DSL/Model/DOMRenderElementCounter.cs | 47 + ZeroLevel/Services/DOM/DSL/Model/TChar.cs | 17 + .../Services/DOM/DSL/Model/TContentElement.cs | 144 + .../Services/DOM/DSL/Model/TEnvironment.cs | 27 + .../Services/DOM/DSL/Model/TFlowRules.cs | 426 +++ .../Services/DOM/DSL/Model/TRenderOptions.cs | 89 + .../DOM/DSL/Services/PlainTextTableBuilder.cs | 110 + .../Services/SpecialTableBuilderFactory.cs | 64 + .../Services/DOM/DSL/Services/TContainer.cs | 2643 +++++++++++++++++ .../DOM/DSL/Services/TContainerFactory.cs | 45 + .../DSL/Services/TContentToStringConverter.cs | 409 +++ .../Services/DOM/DSL/Services/TRender.cs | 435 +++ .../DOM/DSL/Services/TStringReader.cs | 104 + ZeroLevel/Services/DOM/DSL/TEngine.cs | 81 + .../Services/DOM/DSL/Tokens/TBlockToken.cs | 39 + .../Services/DOM/DSL/Tokens/TElementToken.cs | 36 + .../Services/DOM/DSL/Tokens/TFunctionToken.cs | 33 + .../Services/DOM/DSL/Tokens/TPropertyToken.cs | 30 + .../Services/DOM/DSL/Tokens/TSystemToken.cs | 23 + .../Services/DOM/DSL/Tokens/TTextToken.cs | 24 + ZeroLevel/Services/DOM/DSL/Tokens/TToken.cs | 23 + ZeroLevel/Services/DOM/Model/Agency.cs | 42 + ZeroLevel/Services/DOM/Model/AsideContent.cs | 55 + ZeroLevel/Services/DOM/Model/Assotiation.cs | 46 + .../Services/DOM/Model/AssotiationRelation.cs | 28 + ZeroLevel/Services/DOM/Model/Category.cs | 137 + ZeroLevel/Services/DOM/Model/ContentType.cs | 17 + .../Services/DOM/Model/DescriptiveMetadata.cs | 94 + ZeroLevel/Services/DOM/Model/Document.cs | 119 + ZeroLevel/Services/DOM/Model/Flow/Audio.cs | 43 + .../Services/DOM/Model/Flow/Audioplayer.cs | 35 + ZeroLevel/Services/DOM/Model/Flow/Column.cs | 33 + .../Services/DOM/Model/Flow/ContentElement.cs | 25 + .../DOM/Model/Flow/ContentElementType.cs | 29 + .../Services/DOM/Model/Flow/FlowAlign.cs | 27 + .../Services/DOM/Model/Flow/FormContent.cs | 38 + ZeroLevel/Services/DOM/Model/Flow/Gallery.cs | 42 + .../DOM/Model/Flow/IContentElement.cs | 10 + ZeroLevel/Services/DOM/Model/Flow/Image.cs | 46 + ZeroLevel/Services/DOM/Model/Flow/Link.cs | 41 + ZeroLevel/Services/DOM/Model/Flow/List.cs | 33 + .../Services/DOM/Model/Flow/Paragraph.cs | 49 + ZeroLevel/Services/DOM/Model/Flow/Quote.cs | 25 + ZeroLevel/Services/DOM/Model/Flow/Row.cs | 32 + ZeroLevel/Services/DOM/Model/Flow/Section.cs | 49 + .../Services/DOM/Model/Flow/SourceType.cs | 10 + ZeroLevel/Services/DOM/Model/Flow/Table.cs | 40 + ZeroLevel/Services/DOM/Model/Flow/Text.cs | 37 + .../Services/DOM/Model/Flow/TextFormatting.cs | 13 + ZeroLevel/Services/DOM/Model/Flow/TextSize.cs | 12 + .../Services/DOM/Model/Flow/TextStyle.cs | 29 + ZeroLevel/Services/DOM/Model/Flow/Video.cs | 42 + .../Services/DOM/Model/Flow/Videoplayer.cs | 34 + ZeroLevel/Services/DOM/Model/FlowContent.cs | 34 + ZeroLevel/Services/DOM/Model/Header.cs | 97 + ZeroLevel/Services/DOM/Model/Identifier.cs | 44 + ZeroLevel/Services/DOM/Model/Priority.cs | 24 + ZeroLevel/Services/DOM/Model/Tag.cs | 33 + ZeroLevel/Services/DOM/Model/TagMetadata.cs | 57 + .../Services/DOM/Services/ContentBuilder.cs | 550 ++++ .../DOM/Services/ContentElementSerializer.cs | 77 + .../DOM/Services/DocumentContentReader.cs | 230 ++ .../Services/DOM/Services/LanguageParser.cs | 32 + .../Services/DependencyInjection/Container.cs | 1266 ++++++++ .../DependencyInjection/ContainerFactory.cs | 125 + .../Contracts/ICompositionProvider.cs | 22 + .../Contracts/IContainer.cs | 18 + .../Contracts/IContainerFactory.cs | 21 + .../Contracts/IContainerInstanceRegister.cs | 40 + .../Contracts/IContainerRegister.cs | 296 ++ .../Contracts/IParameterStorage.cs | 72 + .../Contracts/IPoolContainer.cs | 91 + .../Contracts/IResolver.cs | 121 + .../Services/DependencyInjection/Injector.cs | 67 + .../Internal/ConstructorMetadata.cs | 112 + .../Internal/ConstructorParameter.cs | 50 + .../DependencyInjection/Internal/IPoolable.cs | 9 + .../Internal/ResolveTypeInfo.cs | 44 + .../DependencyInjection/ParameterAttribute.cs | 34 + .../DependencyInjection/ResolveAttribute.cs | 15 + .../Services/Diagnostics/StackTraceReader.cs | 36 + ZeroLevel/Services/Drawing/TextPainter.cs | 73 + .../Services/Encryption/FastObfuscator.cs | 51 + .../Services/Encryption/RijndaelEncryptor.cs | 186 ++ .../Services/Extensions/ArrayExtensions.cs | 88 + .../Extensions/BitConverterExtensions.cs | 29 + .../CollectionComparsionExtensions.cs | 101 + .../Services/Extensions/DateTimeExtensions.cs | 19 + .../Services/Extensions/EndpointExtensions.cs | 54 + .../Services/Extensions/EnumExtensions.cs | 53 + .../Extensions/EnumerableExtensions.cs | 35 + ZeroLevel/Services/Extensions/FPCommon.cs | 155 + .../Services/Extensions/LinqExtension.cs | 24 + ZeroLevel/Services/Extensions/Monades.cs | 76 + .../Services/Extensions/StringExtensions.cs | 26 + .../Services/Extensions/TypeExtensions.cs | 59 + ZeroLevel/Services/FileSystem/FSUtils.cs | 277 ++ ZeroLevel/Services/FileSystem/FileArchive.cs | 505 ++++ ZeroLevel/Services/FileSystem/FileMeta.cs | 14 + .../FileSystem/PeriodicFileSystemWatcher.cs | 172 ++ ZeroLevel/Services/IdGenerator.cs | 67 + .../Impersonation/IImpersonationExecutor.cs | 13 + .../Services/Impersonation/Impersonation.cs | 141 + .../ImpersonationNativeMethods.cs | 178 ++ .../Impersonation/MySafeTokenHandle.cs | 26 + .../ProcessImpersonationExecutor.cs | 73 + .../UserImpersonationExecutor.cs | 52 + .../Services/Invokation/IInvokeWrapper.cs | 31 + .../Services/Invokation/InvokeWrapper.cs | 361 +++ ZeroLevel/Services/Invokation/Invoker.cs | 10 + .../Logging/FixSizeLogMessageBuffer.cs | 46 + ZeroLevel/Services/Logging/ILog.cs | 44 + ZeroLevel/Services/Logging/ILogComposer.cs | 8 + .../Services/Logging/ILogMessageBuffer.cs | 24 + ZeroLevel/Services/Logging/ILogger.cs | 9 + .../Logging/Implementation/ConsoleLogger.cs | 25 + .../Logging/Implementation/DelegateLogger.cs | 33 + .../Implementation/EncryptedFileLog.cs | 211 ++ .../Logging/Implementation/TextFileLogger.cs | 480 +++ ZeroLevel/Services/Logging/Log.cs | 235 ++ ZeroLevel/Services/Logging/LogLevel.cs | 66 + .../Services/Logging/LogLevelNameMapping.cs | 41 + ZeroLevel/Services/Logging/LogRouterr.cs | 141 + .../Logging/NoLimitedLogMessageBuffer.cs | 42 + .../Services/Network/Contract/IExClient.cs | 18 + .../Services/Network/Contract/IExService.cs | 17 + .../Services/Network/Contract/IZBackward.cs | 10 + .../Network/Contract/IZObservableServer.cs | 17 + .../Services/Network/Contract/IZTransport.cs | 18 + .../Exceptions/NoConnectionException.cs | 9 + .../Network/Models/ExchangeAttributes.cs | 75 + ZeroLevel/Services/Network/Models/Frame.cs | 130 + .../Network/Models/MicroserviceInfo.cs | 78 + .../Services/Network/Models/RequestInfo.cs | 38 + .../Network/Models/ServiceEndpointInfo.cs | 32 + .../Network/Models/ServiceEndpointsInfo.cs | 40 + .../Network/Models/ZTransportStatus.cs | 11 + ZeroLevel/Services/Network/NetworkStats.cs | 157 + .../Network/NetworkStreamDataObfuscator.cs | 50 + .../Services/Network/Services/ExClient.cs | 109 + .../Services/Network/Services/ExRouter.cs | 194 ++ .../Services/Network/Services/ExService.cs | 100 + .../Services/Network/Services/FrameBuilder.cs | 72 + .../Network/Services/FrameExchange.cs | 90 + .../Services/Network/Services/FrameParser.cs | 185 ++ .../Services/Network/Services/IPFinder.cs | 45 + .../Services/ZExSocketObservableServer.cs | 30 + .../Services/Network/SocketExtensions.cs | 44 + ZeroLevel/Services/Network/ZBaseNetwork.cs | 48 + ZeroLevel/Services/Network/ZSocketClient.cs | 411 +++ ZeroLevel/Services/Network/ZSocketServer.cs | 156 + .../Services/Network/ZSocketServerClient.cs | 211 ++ ZeroLevel/Services/ObjectMapping/IMapper.cs | 23 + .../Services/ObjectMapping/IMemberInfo.cs | 16 + .../Services/ObjectMapping/MapFieldInfo.cs | 82 + .../Services/ObjectMapping/TypeMapper.cs | 257 ++ ZeroLevel/Services/Pools/ObjectPool.cs | 163 + ZeroLevel/Services/Queries/AndQuery.cs | 14 + ZeroLevel/Services/Queries/BaseQuery.cs | 21 + .../Services/Queries/Builder/IQueryBuilder.cs | 12 + .../Builder/MemoryStorageQueryBuilder.cs | 136 + ZeroLevel/Services/Queries/IQuery.cs | 9 + ZeroLevel/Services/Queries/NotQuery.cs | 12 + ZeroLevel/Services/Queries/OrQuery.cs | 14 + ZeroLevel/Services/Queries/Query.cs | 45 + ZeroLevel/Services/Queries/QueryOp.cs | 17 + ZeroLevel/Services/Queries/QueryOperation.cs | 15 + .../Services/Queries/Storage/IRealQuery.cs | 7 + .../Services/Queries/Storage/IStorage.cs | 17 + .../Services/Queries/Storage/MemoryStorage.cs | 143 + .../Services/Queries/Storage/QueryResult.cs | 27 + .../ITypeFastAccessMethodBuilder.cs | 13 + .../Services/Reflection/ReferenceHelper.cs | 25 + .../Reflection/StringToTypeConverter.cs | 47 + .../Services/Reflection/TypeActivator.cs | 126 + .../Reflection/TypeFastAccessMethodBuilder.cs | 34 + .../TypeFastAccessMethodBuilderImpl.cs | 77 + ZeroLevel/Services/Reflection/TypeHelpers.cs | 197 ++ .../Semantic/Contracts/ILexProvider.cs | 37 + .../Services/Semantic/Contracts/ILexer.cs | 10 + .../Services/Semantic/Contracts/Languages.cs | 20 + .../Services/Semantic/Contracts/LexToken.cs | 53 + .../Services/Semantic/Contracts/README.txt | 22 + .../Services/Semantic/Contracts/WordToken.cs | 21 + .../Services/Semantic/Helpers/StopWords.cs | 13 + .../Services/Semantic/Helpers/TextAnalizer.cs | 85 + ZeroLevel/Services/Semantic/LexProvider.cs | 123 + .../Services/Semantic/LexProviderFactory.cs | 71 + ZeroLevel/Services/Semantic/Snowball/Among.cs | 21 + .../Semantic/Snowball/CzechStemmer.cs | 34 + .../Semantic/Snowball/DanishStemmer.cs | 465 +++ .../Semantic/Snowball/DutchStemmer.cs | 965 ++++++ .../Semantic/Snowball/EnglishStemmer.cs | 1484 +++++++++ .../Semantic/Snowball/FinnishStemmer.cs | 1148 +++++++ .../Semantic/Snowball/FrenchStemmer.cs | 1744 +++++++++++ .../Semantic/Snowball/GermanStemmer.cs | Bin 0 -> 23280 bytes .../Semantic/Snowball/HungarianStemmer.cs | 1238 ++++++++ .../Semantic/Snowball/ItalianStemmer.cs | 1359 +++++++++ .../Semantic/Snowball/NorwegianStemmer.cs | 401 +++ .../Semantic/Snowball/PortugalStemmer.cs | 1270 ++++++++ .../Semantic/Snowball/RomanianStemmer.cs | 1169 ++++++++ .../Semantic/Snowball/RussianStemmer.cs | 868 ++++++ .../Semantic/Snowball/SpanishStemmer.cs | 1326 +++++++++ .../Semantic/Snowball/StemmerOperations.cs | Bin 0 -> 86906 bytes ZeroLevel/Services/Semantic/WordLexer.cs | 12 + .../Services/Serialization/IBinaryReader.cs | 41 + .../Serialization/IBinarySerializable.cs | 8 + .../Services/Serialization/IBinaryWriter.cs | 47 + .../Services/Serialization/JsonEscaper.cs | 62 + .../Serialization/MemoryStreamReader.cs | 344 +++ .../Serialization/MemoryStreamWriter.cs | 312 ++ .../Serialization/MessageSerializer.cs | 261 ++ .../Serialization/SerializedObjectWrapper.cs | 224 ++ .../SerializedObjectWrapperExtension.cs | 38 + .../Services/Shedulling/AsyncShedullerImpl.cs | 299 ++ .../Shedulling/DateTimeAsyncSheduller.cs | 290 ++ .../Services/Shedulling/DateTimeSheduller.cs | 284 ++ .../Services/Shedulling/ExpiredAsyncObject.cs | 48 + .../Services/Shedulling/ExpiredObject.cs | 53 + .../Services/Shedulling/IAsyncSheduller.cs | 84 + .../Shedulling/IExpirationAsyncSheduller.cs | 34 + .../Shedulling/IExpirationSheduller.cs | 34 + ZeroLevel/Services/Shedulling/ISheduller.cs | 82 + ZeroLevel/Services/Shedulling/Sheduller.cs | 328 ++ .../Services/Shedulling/ShedullerImpl.cs | 298 ++ .../Specification/AndSpecification.cs | 27 + .../Specification/BaseNamedSpecification.cs | 24 + .../Specification/BaseSpecification.cs | 27 + .../Building/ISpecificationBuilder.cs | 16 + .../Building/ISpecificationConstructor.cs | 11 + .../SpecificationConstructorParameterKind.cs | 9 + ...cificationConstructorParametersResolver.cs | 125 + .../Building/SpecificationParameter.cs | 27 + .../Specification/CurrySpecification.cs | 21 + .../Specification/ExpressionSpecification.cs | 26 + .../Services/Specification/ISpecification.cs | 10 + .../Specification/ISpecificationFinder.cs | 12 + .../Specification/NotSpecification.cs | 21 + .../Services/Specification/OrSpecification.cs | 25 + .../Specification/PredicateBuilder.cs | 170 ++ .../Services/AssemblySpecificationFactory.cs | 55 + .../Services/SpecificationBuilder.cs | 133 + .../Services/SpecificationConstructor.cs | 70 + .../Services/Specification/Specification.cs | 39 + .../Specification/SpecificationReader.cs | 20 + .../Specification/TrueSpecification.cs | 13 + .../Text/PlainTextTables/TextTableData.cs | 118 + .../Text/PlainTextTables/TextTableRender.cs | 299 ++ .../PlainTextTables/TextTableRenderOptions.cs | 176 ++ .../Text/PlainTextTables/TextTableStyle.cs | 36 + ZeroLevel/Services/Text/TStringReader.cs | 104 + ZeroLevel/Services/Trees/Contracts/ITree.cs | 10 + .../Services/Trees/Contracts/ITreeNode.cs | 14 + ZeroLevel/Services/Trees/Generic.cs | 85 + .../Trees/ItemFieldToTreeConverter.cs | 304 ++ ZeroLevel/Services/Trees/Tree.cs | 45 + ZeroLevel/Services/Trees/TreeNode.cs | 45 + ZeroLevel/Services/Trees/TreesVisitor.cs | 103 + ZeroLevel/Services/Web/HtmlUtility.cs | 366 +++ ZeroLevel/Services/Web/UrlUtility.cs | 447 +++ ZeroLevel/Startup.cs | 56 + ZeroLevel/ZeroLevel.csproj | 379 +++ ...le_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs | 0 ...le_5937a670-0e60-4077-877b-f7221da3dda1.cs | 0 ...le_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs | 0 .../ZeroLevel.csproj.CoreCompileInputs.cache | 1 + .../ZeroLevel.csprojAssemblyReference.cache | Bin 0 -> 12917 bytes 361 files changed, 49571 insertions(+) create mode 100644 ZeroLevel.Discovery/App.config create mode 100644 ZeroLevel.Discovery/Controllers/BaseController.cs create mode 100644 ZeroLevel.Discovery/Controllers/HttpRequestMessagesExtensions.cs create mode 100644 ZeroLevel.Discovery/Controllers/RoutesController.cs create mode 100644 ZeroLevel.Discovery/DiscoveryService.cs create mode 100644 ZeroLevel.Discovery/ExchangeTransportFactory.cs create mode 100644 ZeroLevel.Discovery/Program.cs create mode 100644 ZeroLevel.Discovery/Properties/AssemblyInfo.cs create mode 100644 ZeroLevel.Discovery/RouteTable.cs create mode 100644 ZeroLevel.Discovery/Startup.cs create mode 100644 ZeroLevel.Discovery/ZeroLevel.Discovery.csproj create mode 100644 ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs create mode 100644 ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs create mode 100644 ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs create mode 100644 ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csproj.CoreCompileInputs.cache create mode 100644 ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csprojAssemblyReference.cache create mode 100644 ZeroLevel.Discovery/packages.config create mode 100644 ZeroLevel.Microservices/BaseProxy.cs create mode 100644 ZeroLevel.Microservices/Contracts/IDiscoveryClient.cs create mode 100644 ZeroLevel.Microservices/Contracts/IExchangeService.cs create mode 100644 ZeroLevel.Microservices/ExServiceHost.cs create mode 100644 ZeroLevel.Microservices/Exchange.cs create mode 100644 ZeroLevel.Microservices/ExchangeTransportFactory.cs create mode 100644 ZeroLevel.Microservices/Model/Checkpoint.cs create mode 100644 ZeroLevel.Microservices/Model/CheckpointArc.cs create mode 100644 ZeroLevel.Microservices/Model/CheckpointType.cs create mode 100644 ZeroLevel.Microservices/Properties/AssemblyInfo.cs create mode 100644 ZeroLevel.Microservices/WebApiDiscoveryClient.cs create mode 100644 ZeroLevel.Microservices/ZeroLevel.Microservices.csproj create mode 100644 ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs create mode 100644 ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs create mode 100644 ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs create mode 100644 ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csproj.CoreCompileInputs.cache create mode 100644 ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csprojAssemblyReference.cache create mode 100644 ZeroLevel.Microservices/packages.config create mode 100644 ZeroLevel.sln create mode 100644 ZeroLevel/Models/BaseModel.cs create mode 100644 ZeroLevel/Models/BinaryDocument.cs create mode 100644 ZeroLevel/Models/IEntity.cs create mode 100644 ZeroLevel/Models/InvokeResult.cs create mode 100644 ZeroLevel/Properties/AssemblyInfo.cs create mode 100644 ZeroLevel/Services/Application/BaseWindowsService.cs create mode 100644 ZeroLevel/Services/Application/BasicServiceInstaller.cs create mode 100644 ZeroLevel/Services/Application/BusinessApplication.cs create mode 100644 ZeroLevel/Services/Application/IZeroService.cs create mode 100644 ZeroLevel/Services/Application/ZeroServiceState.cs create mode 100644 ZeroLevel/Services/Async/AsyncConditionVariable.cs create mode 100644 ZeroLevel/Services/Async/AsyncHelper.cs create mode 100644 ZeroLevel/Services/Async/AsyncLock.cs create mode 100644 ZeroLevel/Services/Async/AsyncManualResetEvent.cs create mode 100644 ZeroLevel/Services/Async/AsyncMonitor.cs create mode 100644 ZeroLevel/Services/Async/AsyncProducerConsumerQueue.cs create mode 100644 ZeroLevel/Services/Async/AsyncReaderWriterLock.cs create mode 100644 ZeroLevel/Services/Async/AsyncSemaphore.cs create mode 100644 ZeroLevel/Services/Async/AsyncWaitQueue.cs create mode 100644 ZeroLevel/Services/Async/CancellationTokenHelpers.cs create mode 100644 ZeroLevel/Services/Async/CancellationTokenTaskSource.cs create mode 100644 ZeroLevel/Services/Async/Deque.cs create mode 100644 ZeroLevel/Services/Async/ExceptionHelpers.cs create mode 100644 ZeroLevel/Services/Async/IdManager.cs create mode 100644 ZeroLevel/Services/Async/NormalizedCancellationToken.cs create mode 100644 ZeroLevel/Services/Async/TaskCompletionSource.cs create mode 100644 ZeroLevel/Services/Async/TaskCompletionSourceExtensions.cs create mode 100644 ZeroLevel/Services/Async/TaskConstants.cs create mode 100644 ZeroLevel/Services/Async/TaskExtensions.cs create mode 100644 ZeroLevel/Services/Async/TaskShim.cs create mode 100644 ZeroLevel/Services/Collections/EverythingStorage.cs create mode 100644 ZeroLevel/Services/Collections/FixSizeQueue.cs create mode 100644 ZeroLevel/Services/Collections/IEverythingStorage.cs create mode 100644 ZeroLevel/Services/Collections/IFixSizeQueue.cs create mode 100644 ZeroLevel/Services/Collections/RoundRobinCollection.cs create mode 100644 ZeroLevel/Services/Collections/SparseIterator.cs create mode 100644 ZeroLevel/Services/Config/BaseConfiguration.cs create mode 100644 ZeroLevel/Services/Config/BaseConfigurationSet.cs create mode 100644 ZeroLevel/Services/Config/Configuration.cs create mode 100644 ZeroLevel/Services/Config/IConfiguration.cs create mode 100644 ZeroLevel/Services/Config/IConfigurationReader.cs create mode 100644 ZeroLevel/Services/Config/IConfigurationSet.cs create mode 100644 ZeroLevel/Services/Config/IConfigurationWriter.cs create mode 100644 ZeroLevel/Services/Config/Implementation/AppWebConfigReader.cs create mode 100644 ZeroLevel/Services/Config/Implementation/ApplicationConfigReader.cs create mode 100644 ZeroLevel/Services/Config/Implementation/CommandLineReader.cs create mode 100644 ZeroLevel/Services/Config/Implementation/IniFileReader.cs create mode 100644 ZeroLevel/Services/Config/Implementation/IniFileWriter.cs create mode 100644 ZeroLevel/Services/DOM/Contracts/IContentReader.cs create mode 100644 ZeroLevel/Services/DOM/Contracts/IMetadataReader.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contexts/TBlockContext.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contexts/TContext.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contexts/TElementContext.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contexts/TFunctionContext.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contexts/TPropertyContext.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contexts/TRootContext.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contracts/ISpecialTableBuilder.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Contracts/TCloneable.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Model/DOMRenderElementCounter.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Model/TChar.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Model/TContentElement.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Model/TEnvironment.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Model/TFlowRules.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Model/TRenderOptions.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Services/PlainTextTableBuilder.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Services/SpecialTableBuilderFactory.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Services/TContainer.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Services/TContainerFactory.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Services/TContentToStringConverter.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Services/TRender.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Services/TStringReader.cs create mode 100644 ZeroLevel/Services/DOM/DSL/TEngine.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Tokens/TBlockToken.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Tokens/TElementToken.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Tokens/TFunctionToken.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Tokens/TPropertyToken.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Tokens/TSystemToken.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Tokens/TTextToken.cs create mode 100644 ZeroLevel/Services/DOM/DSL/Tokens/TToken.cs create mode 100644 ZeroLevel/Services/DOM/Model/Agency.cs create mode 100644 ZeroLevel/Services/DOM/Model/AsideContent.cs create mode 100644 ZeroLevel/Services/DOM/Model/Assotiation.cs create mode 100644 ZeroLevel/Services/DOM/Model/AssotiationRelation.cs create mode 100644 ZeroLevel/Services/DOM/Model/Category.cs create mode 100644 ZeroLevel/Services/DOM/Model/ContentType.cs create mode 100644 ZeroLevel/Services/DOM/Model/DescriptiveMetadata.cs create mode 100644 ZeroLevel/Services/DOM/Model/Document.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Audio.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Audioplayer.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Column.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/ContentElement.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/ContentElementType.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/FlowAlign.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/FormContent.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Gallery.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/IContentElement.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Image.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Link.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/List.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Paragraph.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Quote.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Row.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Section.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/SourceType.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Table.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Text.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/TextFormatting.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/TextSize.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/TextStyle.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Video.cs create mode 100644 ZeroLevel/Services/DOM/Model/Flow/Videoplayer.cs create mode 100644 ZeroLevel/Services/DOM/Model/FlowContent.cs create mode 100644 ZeroLevel/Services/DOM/Model/Header.cs create mode 100644 ZeroLevel/Services/DOM/Model/Identifier.cs create mode 100644 ZeroLevel/Services/DOM/Model/Priority.cs create mode 100644 ZeroLevel/Services/DOM/Model/Tag.cs create mode 100644 ZeroLevel/Services/DOM/Model/TagMetadata.cs create mode 100644 ZeroLevel/Services/DOM/Services/ContentBuilder.cs create mode 100644 ZeroLevel/Services/DOM/Services/ContentElementSerializer.cs create mode 100644 ZeroLevel/Services/DOM/Services/DocumentContentReader.cs create mode 100644 ZeroLevel/Services/DOM/Services/LanguageParser.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Container.cs create mode 100644 ZeroLevel/Services/DependencyInjection/ContainerFactory.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/ICompositionProvider.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/IContainer.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/IContainerFactory.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/IContainerInstanceRegister.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/IContainerRegister.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/IParameterStorage.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/IPoolContainer.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Contracts/IResolver.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Injector.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Internal/ConstructorMetadata.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Internal/ConstructorParameter.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Internal/IPoolable.cs create mode 100644 ZeroLevel/Services/DependencyInjection/Internal/ResolveTypeInfo.cs create mode 100644 ZeroLevel/Services/DependencyInjection/ParameterAttribute.cs create mode 100644 ZeroLevel/Services/DependencyInjection/ResolveAttribute.cs create mode 100644 ZeroLevel/Services/Diagnostics/StackTraceReader.cs create mode 100644 ZeroLevel/Services/Drawing/TextPainter.cs create mode 100644 ZeroLevel/Services/Encryption/FastObfuscator.cs create mode 100644 ZeroLevel/Services/Encryption/RijndaelEncryptor.cs create mode 100644 ZeroLevel/Services/Extensions/ArrayExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/BitConverterExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/CollectionComparsionExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/DateTimeExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/EndpointExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/EnumExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/EnumerableExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/FPCommon.cs create mode 100644 ZeroLevel/Services/Extensions/LinqExtension.cs create mode 100644 ZeroLevel/Services/Extensions/Monades.cs create mode 100644 ZeroLevel/Services/Extensions/StringExtensions.cs create mode 100644 ZeroLevel/Services/Extensions/TypeExtensions.cs create mode 100644 ZeroLevel/Services/FileSystem/FSUtils.cs create mode 100644 ZeroLevel/Services/FileSystem/FileArchive.cs create mode 100644 ZeroLevel/Services/FileSystem/FileMeta.cs create mode 100644 ZeroLevel/Services/FileSystem/PeriodicFileSystemWatcher.cs create mode 100644 ZeroLevel/Services/IdGenerator.cs create mode 100644 ZeroLevel/Services/Impersonation/IImpersonationExecutor.cs create mode 100644 ZeroLevel/Services/Impersonation/Impersonation.cs create mode 100644 ZeroLevel/Services/Impersonation/ImpersonationNativeMethods.cs create mode 100644 ZeroLevel/Services/Impersonation/MySafeTokenHandle.cs create mode 100644 ZeroLevel/Services/Impersonation/ProcessImpersonationExecutor.cs create mode 100644 ZeroLevel/Services/Impersonation/UserImpersonationExecutor.cs create mode 100644 ZeroLevel/Services/Invokation/IInvokeWrapper.cs create mode 100644 ZeroLevel/Services/Invokation/InvokeWrapper.cs create mode 100644 ZeroLevel/Services/Invokation/Invoker.cs create mode 100644 ZeroLevel/Services/Logging/FixSizeLogMessageBuffer.cs create mode 100644 ZeroLevel/Services/Logging/ILog.cs create mode 100644 ZeroLevel/Services/Logging/ILogComposer.cs create mode 100644 ZeroLevel/Services/Logging/ILogMessageBuffer.cs create mode 100644 ZeroLevel/Services/Logging/ILogger.cs create mode 100644 ZeroLevel/Services/Logging/Implementation/ConsoleLogger.cs create mode 100644 ZeroLevel/Services/Logging/Implementation/DelegateLogger.cs create mode 100644 ZeroLevel/Services/Logging/Implementation/EncryptedFileLog.cs create mode 100644 ZeroLevel/Services/Logging/Implementation/TextFileLogger.cs create mode 100644 ZeroLevel/Services/Logging/Log.cs create mode 100644 ZeroLevel/Services/Logging/LogLevel.cs create mode 100644 ZeroLevel/Services/Logging/LogLevelNameMapping.cs create mode 100644 ZeroLevel/Services/Logging/LogRouterr.cs create mode 100644 ZeroLevel/Services/Logging/NoLimitedLogMessageBuffer.cs create mode 100644 ZeroLevel/Services/Network/Contract/IExClient.cs create mode 100644 ZeroLevel/Services/Network/Contract/IExService.cs create mode 100644 ZeroLevel/Services/Network/Contract/IZBackward.cs create mode 100644 ZeroLevel/Services/Network/Contract/IZObservableServer.cs create mode 100644 ZeroLevel/Services/Network/Contract/IZTransport.cs create mode 100644 ZeroLevel/Services/Network/Exceptions/NoConnectionException.cs create mode 100644 ZeroLevel/Services/Network/Models/ExchangeAttributes.cs create mode 100644 ZeroLevel/Services/Network/Models/Frame.cs create mode 100644 ZeroLevel/Services/Network/Models/MicroserviceInfo.cs create mode 100644 ZeroLevel/Services/Network/Models/RequestInfo.cs create mode 100644 ZeroLevel/Services/Network/Models/ServiceEndpointInfo.cs create mode 100644 ZeroLevel/Services/Network/Models/ServiceEndpointsInfo.cs create mode 100644 ZeroLevel/Services/Network/Models/ZTransportStatus.cs create mode 100644 ZeroLevel/Services/Network/NetworkStats.cs create mode 100644 ZeroLevel/Services/Network/NetworkStreamDataObfuscator.cs create mode 100644 ZeroLevel/Services/Network/Services/ExClient.cs create mode 100644 ZeroLevel/Services/Network/Services/ExRouter.cs create mode 100644 ZeroLevel/Services/Network/Services/ExService.cs create mode 100644 ZeroLevel/Services/Network/Services/FrameBuilder.cs create mode 100644 ZeroLevel/Services/Network/Services/FrameExchange.cs create mode 100644 ZeroLevel/Services/Network/Services/FrameParser.cs create mode 100644 ZeroLevel/Services/Network/Services/IPFinder.cs create mode 100644 ZeroLevel/Services/Network/Services/ZExSocketObservableServer.cs create mode 100644 ZeroLevel/Services/Network/SocketExtensions.cs create mode 100644 ZeroLevel/Services/Network/ZBaseNetwork.cs create mode 100644 ZeroLevel/Services/Network/ZSocketClient.cs create mode 100644 ZeroLevel/Services/Network/ZSocketServer.cs create mode 100644 ZeroLevel/Services/Network/ZSocketServerClient.cs create mode 100644 ZeroLevel/Services/ObjectMapping/IMapper.cs create mode 100644 ZeroLevel/Services/ObjectMapping/IMemberInfo.cs create mode 100644 ZeroLevel/Services/ObjectMapping/MapFieldInfo.cs create mode 100644 ZeroLevel/Services/ObjectMapping/TypeMapper.cs create mode 100644 ZeroLevel/Services/Pools/ObjectPool.cs create mode 100644 ZeroLevel/Services/Queries/AndQuery.cs create mode 100644 ZeroLevel/Services/Queries/BaseQuery.cs create mode 100644 ZeroLevel/Services/Queries/Builder/IQueryBuilder.cs create mode 100644 ZeroLevel/Services/Queries/Builder/MemoryStorageQueryBuilder.cs create mode 100644 ZeroLevel/Services/Queries/IQuery.cs create mode 100644 ZeroLevel/Services/Queries/NotQuery.cs create mode 100644 ZeroLevel/Services/Queries/OrQuery.cs create mode 100644 ZeroLevel/Services/Queries/Query.cs create mode 100644 ZeroLevel/Services/Queries/QueryOp.cs create mode 100644 ZeroLevel/Services/Queries/QueryOperation.cs create mode 100644 ZeroLevel/Services/Queries/Storage/IRealQuery.cs create mode 100644 ZeroLevel/Services/Queries/Storage/IStorage.cs create mode 100644 ZeroLevel/Services/Queries/Storage/MemoryStorage.cs create mode 100644 ZeroLevel/Services/Queries/Storage/QueryResult.cs create mode 100644 ZeroLevel/Services/Reflection/ITypeFastAccessMethodBuilder.cs create mode 100644 ZeroLevel/Services/Reflection/ReferenceHelper.cs create mode 100644 ZeroLevel/Services/Reflection/StringToTypeConverter.cs create mode 100644 ZeroLevel/Services/Reflection/TypeActivator.cs create mode 100644 ZeroLevel/Services/Reflection/TypeFastAccessMethodBuilder.cs create mode 100644 ZeroLevel/Services/Reflection/TypeFastAccessMethodBuilderImpl.cs create mode 100644 ZeroLevel/Services/Reflection/TypeHelpers.cs create mode 100644 ZeroLevel/Services/Semantic/Contracts/ILexProvider.cs create mode 100644 ZeroLevel/Services/Semantic/Contracts/ILexer.cs create mode 100644 ZeroLevel/Services/Semantic/Contracts/Languages.cs create mode 100644 ZeroLevel/Services/Semantic/Contracts/LexToken.cs create mode 100644 ZeroLevel/Services/Semantic/Contracts/README.txt create mode 100644 ZeroLevel/Services/Semantic/Contracts/WordToken.cs create mode 100644 ZeroLevel/Services/Semantic/Helpers/StopWords.cs create mode 100644 ZeroLevel/Services/Semantic/Helpers/TextAnalizer.cs create mode 100644 ZeroLevel/Services/Semantic/LexProvider.cs create mode 100644 ZeroLevel/Services/Semantic/LexProviderFactory.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/Among.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/CzechStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/DanishStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/DutchStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/EnglishStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/FinnishStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/FrenchStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/GermanStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/HungarianStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/ItalianStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/NorwegianStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/PortugalStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/RomanianStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/RussianStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/SpanishStemmer.cs create mode 100644 ZeroLevel/Services/Semantic/Snowball/StemmerOperations.cs create mode 100644 ZeroLevel/Services/Semantic/WordLexer.cs create mode 100644 ZeroLevel/Services/Serialization/IBinaryReader.cs create mode 100644 ZeroLevel/Services/Serialization/IBinarySerializable.cs create mode 100644 ZeroLevel/Services/Serialization/IBinaryWriter.cs create mode 100644 ZeroLevel/Services/Serialization/JsonEscaper.cs create mode 100644 ZeroLevel/Services/Serialization/MemoryStreamReader.cs create mode 100644 ZeroLevel/Services/Serialization/MemoryStreamWriter.cs create mode 100644 ZeroLevel/Services/Serialization/MessageSerializer.cs create mode 100644 ZeroLevel/Services/Serialization/SerializedObjectWrapper.cs create mode 100644 ZeroLevel/Services/Serialization/SerializedObjectWrapperExtension.cs create mode 100644 ZeroLevel/Services/Shedulling/AsyncShedullerImpl.cs create mode 100644 ZeroLevel/Services/Shedulling/DateTimeAsyncSheduller.cs create mode 100644 ZeroLevel/Services/Shedulling/DateTimeSheduller.cs create mode 100644 ZeroLevel/Services/Shedulling/ExpiredAsyncObject.cs create mode 100644 ZeroLevel/Services/Shedulling/ExpiredObject.cs create mode 100644 ZeroLevel/Services/Shedulling/IAsyncSheduller.cs create mode 100644 ZeroLevel/Services/Shedulling/IExpirationAsyncSheduller.cs create mode 100644 ZeroLevel/Services/Shedulling/IExpirationSheduller.cs create mode 100644 ZeroLevel/Services/Shedulling/ISheduller.cs create mode 100644 ZeroLevel/Services/Shedulling/Sheduller.cs create mode 100644 ZeroLevel/Services/Shedulling/ShedullerImpl.cs create mode 100644 ZeroLevel/Services/Specification/AndSpecification.cs create mode 100644 ZeroLevel/Services/Specification/BaseNamedSpecification.cs create mode 100644 ZeroLevel/Services/Specification/BaseSpecification.cs create mode 100644 ZeroLevel/Services/Specification/Building/ISpecificationBuilder.cs create mode 100644 ZeroLevel/Services/Specification/Building/ISpecificationConstructor.cs create mode 100644 ZeroLevel/Services/Specification/Building/SpecificationConstructorParameterKind.cs create mode 100644 ZeroLevel/Services/Specification/Building/SpecificationConstructorParametersResolver.cs create mode 100644 ZeroLevel/Services/Specification/Building/SpecificationParameter.cs create mode 100644 ZeroLevel/Services/Specification/CurrySpecification.cs create mode 100644 ZeroLevel/Services/Specification/ExpressionSpecification.cs create mode 100644 ZeroLevel/Services/Specification/ISpecification.cs create mode 100644 ZeroLevel/Services/Specification/ISpecificationFinder.cs create mode 100644 ZeroLevel/Services/Specification/NotSpecification.cs create mode 100644 ZeroLevel/Services/Specification/OrSpecification.cs create mode 100644 ZeroLevel/Services/Specification/PredicateBuilder.cs create mode 100644 ZeroLevel/Services/Specification/Services/AssemblySpecificationFactory.cs create mode 100644 ZeroLevel/Services/Specification/Services/SpecificationBuilder.cs create mode 100644 ZeroLevel/Services/Specification/Services/SpecificationConstructor.cs create mode 100644 ZeroLevel/Services/Specification/Specification.cs create mode 100644 ZeroLevel/Services/Specification/SpecificationReader.cs create mode 100644 ZeroLevel/Services/Specification/TrueSpecification.cs create mode 100644 ZeroLevel/Services/Text/PlainTextTables/TextTableData.cs create mode 100644 ZeroLevel/Services/Text/PlainTextTables/TextTableRender.cs create mode 100644 ZeroLevel/Services/Text/PlainTextTables/TextTableRenderOptions.cs create mode 100644 ZeroLevel/Services/Text/PlainTextTables/TextTableStyle.cs create mode 100644 ZeroLevel/Services/Text/TStringReader.cs create mode 100644 ZeroLevel/Services/Trees/Contracts/ITree.cs create mode 100644 ZeroLevel/Services/Trees/Contracts/ITreeNode.cs create mode 100644 ZeroLevel/Services/Trees/Generic.cs create mode 100644 ZeroLevel/Services/Trees/ItemFieldToTreeConverter.cs create mode 100644 ZeroLevel/Services/Trees/Tree.cs create mode 100644 ZeroLevel/Services/Trees/TreeNode.cs create mode 100644 ZeroLevel/Services/Trees/TreesVisitor.cs create mode 100644 ZeroLevel/Services/Web/HtmlUtility.cs create mode 100644 ZeroLevel/Services/Web/UrlUtility.cs create mode 100644 ZeroLevel/Startup.cs create mode 100644 ZeroLevel/ZeroLevel.csproj create mode 100644 ZeroLevel/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs create mode 100644 ZeroLevel/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs create mode 100644 ZeroLevel/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs create mode 100644 ZeroLevel/obj/Debug/ZeroLevel.csproj.CoreCompileInputs.cache create mode 100644 ZeroLevel/obj/Debug/ZeroLevel.csprojAssemblyReference.cache diff --git a/ZeroLevel.Discovery/App.config b/ZeroLevel.Discovery/App.config new file mode 100644 index 0000000..430fe22 --- /dev/null +++ b/ZeroLevel.Discovery/App.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ZeroLevel.Discovery/Controllers/BaseController.cs b/ZeroLevel.Discovery/Controllers/BaseController.cs new file mode 100644 index 0000000..03c8d5e --- /dev/null +++ b/ZeroLevel.Discovery/Controllers/BaseController.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using ZeroLevel.Services.Web; + +namespace ZeroLevel.Discovery +{ + public abstract class BaseController : ApiController + { + #region Responce create helpers + public static HttpResponseMessage BadRequestAnswer(HttpRequestMessage request, string message) + { + return request.CreateSelfDestroyingResponse(HttpStatusCode.BadRequest, + message.Replace("\r", " ").Replace("\n", " ")); + } + + public static HttpResponseMessage BadRequestAnswer(HttpRequestMessage request, Exception ex) + { + return request.CreateSelfDestroyingResponse(HttpStatusCode.BadRequest, + ex.Message.Replace("\r", " ").Replace("\n", " ")); + } + + public static HttpResponseMessage SuccessAnswer(HttpRequestMessage request) + { + return request.CreateSelfDestroyingResponse(HttpStatusCode.OK); + } + + public static HttpResponseMessage NotFoundAnswer(HttpRequestMessage request, string message) + { + return request.CreateSelfDestroyingResponse(HttpStatusCode.Conflict, + message.Replace("\r", " ").Replace("\n", " ")); + } + + public static HttpResponseMessage HttpActionResult(HttpRequestMessage request, Func responseBuilder) + { + try + { + return request.CreateSelfDestroyingResponse(responseBuilder(), HttpStatusCode.OK); + } + catch (KeyNotFoundException knfEx) + { + return NotFoundAnswer(request, knfEx.Message); + } + catch (Exception ex) + { + Log.Error(ex, "Request {0} fault", request.RequestUri.PathAndQuery); + return BadRequestAnswer(request, ex); + } + } + + protected static DateTime? ParseDateTime(string line) + { + var dateParts = line.Split('.', '/', '\\', '-').Select(p => p.Trim()).ToArray(); + if (dateParts.Last().Length == 4) + { + dateParts = dateParts.Reverse().ToArray(); + } + if (dateParts.First().Length != 4) return null; + int year, month = 1, day = 1; + if (false == int.TryParse(dateParts.First(), out year)) + { + return null; + } + if (dateParts.Count() > 1) + { + if (false == int.TryParse(dateParts[1], out month)) + { + return null; + } + } + if (dateParts.Count() > 2) + { + if (false == int.TryParse(dateParts[2], out day)) + { + return null; + } + } + return new DateTime(year, month, day); + } + + protected static String GetParameter(HttpRequestMessage request, string name) + { + var keys = UrlUtility.ParseQueryString(request.RequestUri.Query); + if (keys.ContainsKey(name)) + { + return keys[name]; + } + return null; + } + #endregion + } +} diff --git a/ZeroLevel.Discovery/Controllers/HttpRequestMessagesExtensions.cs b/ZeroLevel.Discovery/Controllers/HttpRequestMessagesExtensions.cs new file mode 100644 index 0000000..36353f8 --- /dev/null +++ b/ZeroLevel.Discovery/Controllers/HttpRequestMessagesExtensions.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Net.Http; + +namespace ZeroLevel.Discovery +{ + public static class HttpRequestMessagesExtensions + { + private const string HttpContext = "MS_HttpContext"; + private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty"; + private const string OwinContext = "MS_OwinContext"; + + public static string GetClientIpAddress(HttpRequestMessage request) + { + //Web-hosting + if (request.Properties.ContainsKey(HttpContext)) + { + dynamic ctx = request.Properties[HttpContext]; + if (ctx != null) + { + return ctx.Request.UserHostAddress; + } + } + //Self-hosting + if (request.Properties.ContainsKey(RemoteEndpointMessage)) + { + dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage]; + if (remoteEndpoint != null) + { + return remoteEndpoint.Address; + } + } + //Owin-hosting + if (request.Properties.ContainsKey(OwinContext)) + { + dynamic ctx = request.Properties[OwinContext]; + if (ctx != null) + { + return ctx.Request.RemoteIpAddress; + } + } + return null; + } + + public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, HttpStatusCode code = HttpStatusCode.OK) + { + var response = request.CreateResponse(code); + request.RegisterForDispose(response); + return response; + } + + public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, T val, HttpStatusCode code = HttpStatusCode.OK) + { + var response = request.CreateResponse(code, val); + request.RegisterForDispose(response); + return response; + } + + public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, HttpStatusCode code, string reasonPhrase) + { + var response = request.CreateResponse(code); + response.ReasonPhrase = reasonPhrase; + request.RegisterForDispose(response); + return response; + } + + public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, HttpResponseMessage response) + { + request.RegisterForDispose(response); + return response; + } + } +} diff --git a/ZeroLevel.Discovery/Controllers/RoutesController.cs b/ZeroLevel.Discovery/Controllers/RoutesController.cs new file mode 100644 index 0000000..af65ac5 --- /dev/null +++ b/ZeroLevel.Discovery/Controllers/RoutesController.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Description; +using ZeroLevel.Models; +using ZeroLevel.Network.Microservices; + +namespace ZeroLevel.Discovery +{ + public class RoutesController : + BaseController + { + [HttpGet] + [Route("favicon.ico")] + public HttpResponseMessage favicon(HttpRequestMessage request) + { + return null; + } + + [HttpGet] + [Route("api/v0/routes")] + [ResponseType(typeof(IEnumerable))] + public HttpResponseMessage GetRoutes(HttpRequestMessage request) + { + try + { + return request.CreateSelfDestroyingResponse(Injector.Default.Resolve().Get()); + } + catch (Exception ex) + { + Log.Error(ex, "Error with read records"); + return BadRequestAnswer(request, ex); + } + } + + [HttpPost] + [Route("api/v0/routes")] + [ResponseType(typeof(InvokeResult))] + public HttpResponseMessage AddRoute(HttpRequestMessage request, MicroserviceInfo service) + { + try + { + var ir = Injector.Default.Resolve().Append(service); + return request.CreateSelfDestroyingResponse(ir); + } + catch (Exception ex) + { + Log.Error(ex, "Error with append endpoint"); + return BadRequestAnswer(request, ex); + } + } + } +} diff --git a/ZeroLevel.Discovery/DiscoveryService.cs b/ZeroLevel.Discovery/DiscoveryService.cs new file mode 100644 index 0000000..7ad6ff1 --- /dev/null +++ b/ZeroLevel.Discovery/DiscoveryService.cs @@ -0,0 +1,32 @@ +using ZeroLevel.Services.Applications; + +namespace ZeroLevel.Discovery +{ + public sealed class DiscoveryService + : BaseWindowsService, IZeroService + { + public DiscoveryService() + : base("Discovery") + { + } + + public override void PauseAction() + { + } + + public override void ResumeAction() + { + } + + public override void StartAction() + { + Injector.Default.Register(new RouteTable()); + var port = Configuration.Default.First("port"); + Startup.StartWebPanel(port, false); + } + + public override void StopAction() + { + } + } +} diff --git a/ZeroLevel.Discovery/ExchangeTransportFactory.cs b/ZeroLevel.Discovery/ExchangeTransportFactory.cs new file mode 100644 index 0000000..76864fe --- /dev/null +++ b/ZeroLevel.Discovery/ExchangeTransportFactory.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Reflection; +using ZeroLevel.Network.Microservices; +using ZeroLevel.Services.Network; +using ZeroLevel.Services.Network.Contract; + +namespace ZeroLevel.Microservices +{ + internal static class ExchangeTransportFactory + { + private static readonly Dictionary _customServers = new Dictionary(); + private static readonly Dictionary _customClients = new Dictionary(); + private static readonly ConcurrentDictionary _clientInstances = new ConcurrentDictionary(); + + /// + /// Сканирование указанной сборки для поиска типов реализующих интерфейсы + /// IExchangeServer или IExchangeClient + /// + internal static void ScanAndRegisterCustomTransport(Assembly asm) + { + foreach (var type in asm.GetExportedTypes()) + { + var serverAttr = type.GetCustomAttribute(); + if (serverAttr != null && + string.IsNullOrWhiteSpace(serverAttr.Protocol) == false && + typeof(IZObservableServer).IsAssignableFrom(type)) + { + _customServers[serverAttr.Protocol] = type; + } + var clientAttr = type.GetCustomAttribute(); + if (clientAttr != null && + string.IsNullOrWhiteSpace(clientAttr.Protocol) == false && + typeof(IZTransport).IsAssignableFrom(type)) + { + _customClients[clientAttr.Protocol] = type; + } + } + } + + /// + /// Создает сервер для приема сообщений по указанному протоколу + /// + /// Протокол + /// Сервер + internal static ExService GetServer(string protocol) + { + ExService instance = null; + if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase)) + { + instance = new ExService(new ZExSocketObservableServer(new System.Net.IPEndPoint(IPAddress.Any, IPFinder.GetFreeTcpPort()))); + } + else + { + var key = protocol.Trim().ToLowerInvariant(); + if (_customServers.ContainsKey(key)) + { + instance = new ExService((IZObservableServer)Activator.CreateInstance(_customServers[key])); + } + } + if (instance != null) + { + return instance; + } + throw new NotSupportedException($"Protocol {protocol} not supported"); + } + + /// + /// Создает клиента для обращений к серверу по указанному протоколу + /// + /// Протокол + /// Адрес сервера + /// Клиент + internal static ExClient GetClient(string protocol, string endpoint) + { + ExClient instance = null; + var cachee_key = $"{protocol}:{endpoint}"; + if (_clientInstances.ContainsKey(cachee_key)) + { + instance = _clientInstances[cachee_key]; + if (instance.Status == ZTransportStatus.Working) + { + return instance; + } + _clientInstances.TryRemove(cachee_key, out instance); + instance = null; + } + if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase)) + { + instance = new ExClient(new ZSocketClient(SocketExtensions.CreateIPEndPoint(endpoint))); + } + else + { + var key = protocol.Trim().ToLowerInvariant(); + if (_customClients.ContainsKey(key)) + { + instance = new ExClient((IZTransport)Activator.CreateInstance(_customClients[key], new object[] { endpoint })); + } + } + if (instance != null) + { + _clientInstances[cachee_key] = instance; + return instance; + } + throw new NotSupportedException($"Protocol {protocol} not supported"); + } + } +} diff --git a/ZeroLevel.Discovery/Program.cs b/ZeroLevel.Discovery/Program.cs new file mode 100644 index 0000000..bd8b524 --- /dev/null +++ b/ZeroLevel.Discovery/Program.cs @@ -0,0 +1,10 @@ +namespace ZeroLevel.Discovery +{ + class Program + { + static void Main(string[] args) + { + Bootstrap.Startup(args); + } + } +} diff --git a/ZeroLevel.Discovery/Properties/AssemblyInfo.cs b/ZeroLevel.Discovery/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..53517ec --- /dev/null +++ b/ZeroLevel.Discovery/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +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("ZeroLevel.Discovery")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ZeroLevel.Discovery")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[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("4f55b23f-938c-4da2-b6dc-b6bc66d36073")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ZeroLevel.Discovery/RouteTable.cs b/ZeroLevel.Discovery/RouteTable.cs new file mode 100644 index 0000000..535ae63 --- /dev/null +++ b/ZeroLevel.Discovery/RouteTable.cs @@ -0,0 +1,246 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using ZeroLevel.Microservices; +using ZeroLevel.Models; +using ZeroLevel.Network.Microservices; + +namespace ZeroLevel.Discovery +{ + public class RouteTable + : IDisposable + { + private readonly Dictionary _table = new Dictionary(); + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + + public RouteTable() + { + Load(); + Sheduller.RemindEvery(TimeSpan.FromSeconds(10), Heartbeat); + } + + #region Snapshot + private static readonly object _snapshot_lock = new object(); + + private void Save() + { + string snapshot; + _lock.EnterReadLock(); + try + { + snapshot = JsonConvert.SerializeObject(_table); + } + catch (Exception ex) + { + Log.Error(ex, "Fault make snapshot"); + return; + } + finally + { + _lock.ExitReadLock(); + } + try + { + var snapshot_path = Path.Combine(Configuration.BaseDirectory, "snapshot.snp"); + lock (_snapshot_lock) + { + File.WriteAllText(snapshot_path, snapshot); + } + } + catch (Exception ex) + { + Log.Error(ex, "Fault save shapshot"); + } + } + + private void Load() + { + try + { + var path = Path.Combine(Configuration.BaseDirectory, "snapshot.snp"); + if (File.Exists(path)) + { + var snapshot = File.ReadAllText(path); + if (string.IsNullOrWhiteSpace(snapshot) == false) + { + var restored = JsonConvert.DeserializeObject>(snapshot); + _lock.EnterWriteLock(); + try + { + _table.Clear(); + foreach (var r in restored) + { + _table.Add(r.Key, r.Value); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Fault load snapshot"); + } + } + #endregion + + private bool Ping(string protocol, string endpoint, string msg) + { + try + { + using (var client = ExchangeTransportFactory.GetClient(protocol, endpoint)) + { + return client.Status == Services.Network.ZTransportStatus.Working; + } + } + catch (Exception ex) + { + Log.Error(ex, $"[RouteTable] Fault ping endpoint {endpoint}, protocol {protocol}"); + return false; + } + } + + private void Heartbeat(long taskid) + { + try + { + var removeEntities = new Dictionary>(); + _lock.EnterReadLock(); + try + { + foreach (var pair in _table) + { + var endpointsToRemove = new List(); + foreach (var e in pair.Value.Endpoints) + { + if (Ping(e.Protocol, e.Endpoint, "HELLO") == false) + { + if (false == removeEntities.ContainsKey(pair.Key)) + { + removeEntities.Add(pair.Key, new List()); + } + removeEntities[pair.Key].Add(e); + } + } + } + } + finally + { + _lock.ExitReadLock(); + } + _lock.EnterWriteLock(); + try + { + foreach (var pair in removeEntities) + { + foreach (var ep in pair.Value) + { + _table[pair.Key].Endpoints.Remove(ep); + } + } + var badKeys = _table.Where(f => f.Value.Endpoints.Count == 0) + .Select(pair => pair.Key) + .ToList(); + foreach (var badKey in badKeys) + { + _table.Remove(badKey); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Fault heartbeat"); + } + Save(); + } + + public InvokeResult Append(MicroserviceInfo serviceInfo) + { + InvokeResult result = null; + if (Ping(serviceInfo.Protocol, serviceInfo.Endpoint, serviceInfo.ServiceKey)) + { + var key = $"{serviceInfo.ServiceGroup}:{serviceInfo.ServiceType}:{serviceInfo.ServiceKey.Trim().ToLowerInvariant()}"; + _lock.EnterWriteLock(); + try + { + if (false == _table.ContainsKey(key)) + { + _table.Add(key, new ServiceEndpointsInfo + { + ServiceKey = serviceInfo.ServiceKey, + Version = serviceInfo.Version, + ServiceGroup = serviceInfo.ServiceGroup, + ServiceType = serviceInfo.ServiceType, + Endpoints = new List() + }); + _table[key].Endpoints.Add(new ServiceEndpointInfo + { + Endpoint = serviceInfo.Endpoint, + Protocol = serviceInfo.Protocol + }); + Log.SystemInfo($"The service '{serviceInfo.ServiceKey}' registered on protocol {serviceInfo.Protocol}, endpoint: {serviceInfo.Endpoint}"); + } + else + { + var exists = _table[key]; + var endpoint = new ServiceEndpointInfo + { + Endpoint = serviceInfo.Endpoint, + Protocol = serviceInfo.Protocol + }; + if (exists.Endpoints.Contains(endpoint) == false) + { + Log.Info($"The service '{serviceInfo.ServiceKey}' register endpoint: {serviceInfo.Endpoint} on protocol {serviceInfo.Protocol}"); + exists.Endpoints.Add(endpoint); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Fault append service ({0} {1}) endpoint '{2}'", serviceInfo.ServiceKey, serviceInfo.Version, serviceInfo.Endpoint); + result = InvokeResult.Fault(ex.Message); + } + finally + { + _lock.ExitWriteLock(); + } + Save(); + result = InvokeResult.Succeeding(); + } + else + { + result = InvokeResult.Fault($"Appending endpoint '{serviceInfo.Endpoint}' canceled for service {serviceInfo.ServiceKey} ({serviceInfo.Version}) because endpoind no avaliable"); + } + return result; + } + + public IEnumerable Get() + { + _lock.EnterReadLock(); + try + { + return _table.Values.ToList(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Dispose() + { + _lock.Dispose(); + } + } +} diff --git a/ZeroLevel.Discovery/Startup.cs b/ZeroLevel.Discovery/Startup.cs new file mode 100644 index 0000000..71915fc --- /dev/null +++ b/ZeroLevel.Discovery/Startup.cs @@ -0,0 +1,79 @@ +using Microsoft.Owin.Hosting; +using Owin; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Routing; + +namespace ZeroLevel.Discovery +{ + public class LogRequestAndResponseHandler : DelegatingHandler + { + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + // log request body + string requestBody = await request.Content.ReadAsStringAsync(); + Log.Debug(requestBody); + + // let other handlers process the request + var result = await base.SendAsync(request, cancellationToken); + + if (result.Content != null) + { + //(result.Content as ObjectContent).Formatter.MediaTypeMappings.Clear(); + // once response body is ready, log it + var responseBody = await result.Content.ReadAsStringAsync(); + Log.Debug(responseBody); + } + return result; + } + } + + public class EnableInheritRoutingDirectRouteProvider : DefaultDirectRouteProvider + { + protected override IReadOnlyList GetActionRouteFactories(HttpActionDescriptor actionDescriptor) + { + // inherit route attributes decorated on base class controller's actions + return actionDescriptor.GetCustomAttributes(inherit: true); + } + } + + public class Startup + { + // This code configures Web API. The Startup class is specified as a type + // parameter in the WebApp.Start method. + public void Configuration(IAppBuilder appBuilder) + { + // Configure Web API for self-host. + HttpConfiguration config = new HttpConfiguration(); + config.MapHttpAttributeRoutes(new EnableInheritRoutingDirectRouteProvider()); + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{action}/{id}", + defaults: new { id = RouteParameter.Optional } + ); + config.EnsureInitialized(); + config.Formatters.Remove(config.Formatters.XmlFormatter); + config.Formatters.Add(config.Formatters.JsonFormatter); + //if (_log_request_response) + { + config.MessageHandlers.Add(new LogRequestAndResponseHandler()); + } + appBuilder.UseWebApi(config); + } + + private static bool _log_request_response; + public static void StartWebPanel(int port, + bool log_request_response) + { + _log_request_response = log_request_response; + string baseAddress = string.Format("http://*:{0}/", port); + WebApp.Start(url: baseAddress); + Log.Info(string.Format("Web panel url: {0}", baseAddress)); + } + } +} diff --git a/ZeroLevel.Discovery/ZeroLevel.Discovery.csproj b/ZeroLevel.Discovery/ZeroLevel.Discovery.csproj new file mode 100644 index 0000000..299b78e --- /dev/null +++ b/ZeroLevel.Discovery/ZeroLevel.Discovery.csproj @@ -0,0 +1,97 @@ + + + + + Debug + AnyCPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073} + Exe + ZeroLevel.Discovery + ZeroLevel.Discovery + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Host.HttpListener.4.0.1\lib\net45\Microsoft.Owin.Host.HttpListener.dll + + + ..\packages\Microsoft.Owin.Hosting.4.0.1\lib\net45\Microsoft.Owin.Hosting.dll + + + ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + + + + + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + + + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll + + + ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.7\lib\net45\System.Web.Http.Owin.dll + + + ..\packages\Microsoft.AspNet.WebApi.SelfHost.5.2.7\lib\net45\System.Web.Http.SelfHost.dll + + + + + + + + + + + + + + Component + + + + + + + + + + + + + + {37020d8d-34e8-4ec3-a447-8396d5080457} + ZeroLevel + + + + \ No newline at end of file diff --git a/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs b/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs new file mode 100644 index 0000000..e69de29 diff --git a/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs b/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs new file mode 100644 index 0000000..e69de29 diff --git a/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs b/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs new file mode 100644 index 0000000..e69de29 diff --git a/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csproj.CoreCompileInputs.cache b/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..1d781a8 --- /dev/null +++ b/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +564f93dfbd5c0e962177d594e0bc0c5a954a167e diff --git a/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csprojAssemblyReference.cache b/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csprojAssemblyReference.cache new file mode 100644 index 0000000000000000000000000000000000000000..01f2ae07851f8182ce1479fb9fea52425bf5ed95 GIT binary patch literal 27737 zcmeI436LCB8OLX4=X#UCW)jRH2`&&wLc;8kI}no1W=%6K!pbJFDPxt)a>z?oTuI_&C1%iP<-~$TSA`B0a*|bv0Xhpr4Gg6DnNrhFs_}BtwRZ<&?Z^?$wRAzM`wh=jDt+m!PMXN_w9m z_2gw-HLD{NhWW437zq+9ic)rolrf50tnz!}ZN)-K(qwG}5^XDH<<(koK*`El90$6T zZE~1?kse(y<;$`j2-MXDB9UQn(YZS8v?dC!xx_%w|E zqfs*1ws~o~q+U%bG%O*n=na#%&Y3kOy;jc2nyhBzhE`pd3*>oOPgk9rUP5|#OHu1j zr#jo$9oie1k(!-qNmnj#v1CZ8Y(5`_clwUynKl&isTGR4**sgE&emOfwnG=+(6xBE zo^IVnnajL-?577>QghQiikdFv%e{)KrxzyMkDBK znPcLRBF`1Ikh7j%@wF>$Wglp57Y&NEI#c>oPb|H z6t5j$$rqKz)pX+6JD2aD^rGoRb_%7-PKB9BCVCnf&M!fBI*v{vbDX2qicZGyHfrKj zBZxfZcJ>rH%FcjnDj6=(KoI%Q9bpq`Ab9aTb+TwdzcW0_J}(lUL} zv~`3sN{l}AwdQ*nNz5*z5zkP^vq#e9Sid}2k`yf>>0O2vQ}ixWP6@;;m0KwhrW6Wg zv+o<%uhbSn10)a(&sx8ZV)JO+?qz+1A#4bLgII7H-Hs$}QRSCMi23 zC(r6>k!DFUWgRNhG1SMV=i{^;LXkS3qOlDy8(}Vhxez7|a}mrYGD%eV2w~LnHTg44 zhoapprAvw$Z4?z7vlqa+*UAG5de(>$CuwF7rG(8A0v@1t*`F#|fDKMA7^pi=VnL>!64Kfo) z1u+EQfRSyf7@`BJn*M~2~un#-66 zHXk~WH4kWZ95vT(Np$XrsfnR!JQ{8WMQ3H00hlc?TgfER+#o;6$F&pN$*qgmeS|Rr}Y&|o#le8 z(Sz^=yNQmnn{7O_wFoIZp*{g2+;sbdye0G{^63c`LXMm61iOX4Vz-j9gM}%}W6B6H z;jxI1$y-ccA)k(}U|L&N4W%Hb+N4aMqV^`(?erzvO~wuusBRwACIKisy7EDJ3+n6S z(_z)}q{#_(2OVK|+GuEN(Yb)9vr#|?&lG(+-qN|7d^)Z|r(HEvX zdv*^QJ6NpF<5{f}uv$iXpOv?)?jxUax2*TmE_=Y{LR$;XY97rh0UA6<_tAKZ=0WnQ z>hpw0Xoo#&Q=zQ|W(g0bT>u8}oP03e zf_a>LY7fb>`*76sC{kY8X1?c2uqQ~u_L8xKg{zgvwNQWyZwGx`-r{l7 zeKs1}T6E6l>73=JQ;&CuyXij^m`E=oE)N87TO7rMV%lHZ)!G~gQ(E-*w)FC(?fGFH zJ9>7tJT&5&=O(LN+au>r{Kp5oN4K@VGxeNo!;}Me58t=uhHKAj*}3ec>%!NZ`sS`Z z&&_#l$DRXIe)8AvOyBp|-uRy8;1{2akGtu~T~i+KU2~(+dGBp{`y+R08{Qc6{K#jY z`0j&WyJG2%XJ32owol!2V|~kr1$!q%znYM1l+zW|*BE;^HWI+Fx+Fj~W><1L+Ekx$)^=YI`CFS!Vv$`d+8Kq&1K^1NbunSAQF z(|&~xuvf|0!Riy7#KV~&fP*1Ue(~|P`23E1Y8M}S?nr|Do{qAEHXhnqgvRlN#tH~+ zBB@WvTS9*%pMJyv6lr@+o)!^Cs=Gw`?x7wb0b_ zXpR@4xtOFr8gJ43jeP3t$o-v;u(!$B!J?Dk=`aBuiS#}lZ|VG#d|JK#`4{c5cWo-P zwZKGpFku0h9+LWCyan@b@~K^HFu^Xt-lrqX9AQZx5G*m!q3VW26M@)qz;(O}*I#&CZwYYad|aO6N)iTpeBb$snk{b8Q%h*#3JrD{ z2~4A0nBL$qy)M9nsX=~A*Ah zz;zYI1I+{%n&0whUKOCh95}!1cw2TF2t&22a5BK0;sWy<9?VMuFqimXJTEh+5eDbY z&*`8u$wlXvJe>mqI{iK!&*_{&7^+RqAgv0ZWD;yD08MiNdXWe8f&ft72jn@RM#8`~ zIZZ&*>_YP_kLDQx8qAFLn;dVOoEe0{?o~T(f)Z>dFwJsd`Z15`X#pn8UH370i)k)l zaJ)U82RLWB;QWAxvrhoW@Zoq~y`4iCxaw^I&@6PJ`5upEuK*3!Kk&Mkk1{16cgx0wT-OCfYM?h%MC**mj;C#Zs zbqY2B%|;iRyLdEr3earx(Rhv~O&F@3f{OrVlMBpOc`&;LV6cpoU#odrt4V~x`IxN- zbTTeFU*_rDDxia9xqLd_(#a79u14zxnm!krFYst?7NEgmX+9cn(ex9BYK@i$n1Tz; z=XfwT2*6zGgYmqnDG`Q|m1P>t<^Q`j=gk$kHUpC8g7g_4(oO-S%X~(@#w*tbucRZV(?3m+b%gq>XxAyls(C``^)Bho z!%4Arm@`!@BqmH1i�OSkX!-y~UR9S=ytWrg@y^(Jt$2Yfl;RzxGgZ9M6iV?P&6z4*stKidtK&=+ zuXuz~yk&4o@ybCc#dB|`6c53LQapuqO7TcmD8=JIrxecyg;G41GNsj=g@;r^DV~!! zQ^i9Qp%i!SPAP8Vg;Lx`I;FU$6iRVh + + + + + + + + + + + \ No newline at end of file diff --git a/ZeroLevel.Microservices/BaseProxy.cs b/ZeroLevel.Microservices/BaseProxy.cs new file mode 100644 index 0000000..9315394 --- /dev/null +++ b/ZeroLevel.Microservices/BaseProxy.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; + +namespace ZeroLevel.ProxyREST +{ + public abstract class BaseProxy + { + private readonly string _baseUrl; + + private Uri BuildRequestUrl(string baseUri, string resource, IDictionary parameters) + { + if (null == resource) throw new ArgumentNullException("resource"); + var stringBuilder = new StringBuilder(baseUri); + if (baseUri[baseUri.Length - 1] != '/') + stringBuilder.Append('/'); + if (resource[0] != '/') + { + stringBuilder.Append(resource); + } + else + { + stringBuilder.Append(resource.Substring(1)); + } + parameters. + Do(list => + { + if (list.Count > 0) + { + stringBuilder.Append("?"); + foreach (string key in list.Keys) + { + var val = list[key]; + if (val == null) + { + stringBuilder.Append(key); + } + else + { + var vtype = val.GetType(); + if (vtype.IsArray) + { + if (vtype.GetElementType() == typeof(string)) + { + var arr = (string[])val; + stringBuilder.Append(string.Join("&", arr.Select(i => string.Format("{0}[]={1}", key, i)))); + } + else + { + var arr = (object[])val; + stringBuilder.Append(string.Join("&", arr.Select(i => string.Format("{0}[]={1}", key, JsonConvert.SerializeObject(i))))); + } + } + else + { + if (vtype == typeof(string)) + { + stringBuilder.AppendFormat("{0}={1}", key, val); + } + else + { + stringBuilder.AppendFormat("{0}={1}", key, JsonConvert.SerializeObject(val)); + } + } + } + stringBuilder.Append("&"); + } + } + }); + return new Uri(stringBuilder.ToString().TrimEnd('&')); + } + + protected T SendRequest(string resource, string method, object body, IDictionary parameters = null) + { + string statusCode = null; + string reason = null; + try + { + var request = (HttpWebRequest)WebRequest.Create(BuildRequestUrl(_baseUrl, resource, parameters)); + request.UseDefaultCredentials = true; + request.Method = method; + request.Proxy = null; + request.AutomaticDecompression = DecompressionMethods.GZip; + if (body != null) + { + request.Accept = "application/json"; + request.ContentType = "application/json"; + using (var streamWriter = new StreamWriter(request.GetRequestStream())) + { + streamWriter.Write(JsonConvert.SerializeObject(body)); + streamWriter.Flush(); + } + } + using (var response = (HttpWebResponse)request.GetResponse()) + { + statusCode = response.StatusCode.ToString(); + reason = response.StatusDescription; + if (response.StatusCode == HttpStatusCode.OK) + { + using (var stream = new StreamReader(response.GetResponseStream())) + { + string json = stream.ReadToEnd(); + return JsonConvert.DeserializeObject(json); + } + } + else + { + throw new Exception("Bad status code"); + } + } + } + catch (Exception ex) + { + var line = string.Format("Сбой запроса ресурса {0} методом {1}. Код ошибки: {2}. Комментарий: {3}", + resource, + method, + statusCode ?? "Uncknown", + reason ?? ex.Message); + Log.Error(ex, line); + throw new InvalidOperationException(line, ex); + } + } + + protected T GET(string resource, IDictionary parameters = null) + { + return SendRequest(resource, "GET", null, parameters); + } + + protected T POST(string resource, object body, IDictionary parameters = null) + { + return SendRequest(resource, "POST", body, parameters); + } + + protected T DELETE(string resource, object body, IDictionary parameters = null) + { + return SendRequest(resource, "DELETE", body, parameters); + } + + static BaseProxy() + { + ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true; + ServicePointManager.Expect100Continue = false; + ServicePointManager.DefaultConnectionLimit = 8; + } + + public BaseProxy(string baseUri) + { + if (false == baseUri.EndsWith("/")) + _baseUrl = baseUri + "/"; + else + _baseUrl = baseUri; + } + } +} diff --git a/ZeroLevel.Microservices/Contracts/IDiscoveryClient.cs b/ZeroLevel.Microservices/Contracts/IDiscoveryClient.cs new file mode 100644 index 0000000..5470b3b --- /dev/null +++ b/ZeroLevel.Microservices/Contracts/IDiscoveryClient.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using ZeroLevel.Network.Microservices; + +namespace ZeroLevel.Microservices.Contracts +{ + public interface IDiscoveryClient + { + void Register(MicroserviceInfo info); + IEnumerable GetServiceEndpoints(string serviceKey); + IEnumerable GetServiceEndpointsByGroup(string serviceGroup); + IEnumerable GetServiceEndpointsByType(string serviceType); + + ServiceEndpointInfo GetService(string serviceKey, string endpoint); + } +} diff --git a/ZeroLevel.Microservices/Contracts/IExchangeService.cs b/ZeroLevel.Microservices/Contracts/IExchangeService.cs new file mode 100644 index 0000000..0f60134 --- /dev/null +++ b/ZeroLevel.Microservices/Contracts/IExchangeService.cs @@ -0,0 +1,13 @@ +namespace ZeroLevel.Microservices.Contracts +{ + public interface IExchangeService + { + string Name { get; } + string Key { get; } + string Endpoint { get; } + string Version { get; } + string Protocol { get; } + string Group { get; } + string Type { get; } + } +} diff --git a/ZeroLevel.Microservices/ExServiceHost.cs b/ZeroLevel.Microservices/ExServiceHost.cs new file mode 100644 index 0000000..635b517 --- /dev/null +++ b/ZeroLevel.Microservices/ExServiceHost.cs @@ -0,0 +1,519 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using ZeroLevel.Microservices.Contracts; +using ZeroLevel.Network.Microservices; +using ZeroLevel.Services.Network; + +namespace ZeroLevel.Microservices +{ + internal sealed class ExServiceHost + : IDisposable + { + private class MetaService + { + public MicroserviceInfo ServiceInfo { get; set; } + public ExService Server { get; set; } + } + private bool _disposed = false; + private readonly long _registerTaskKey = -1; + private readonly IDiscoveryClient _discoveryClient; + private readonly ConcurrentDictionary _services + = new ConcurrentDictionary(); + + internal ExServiceHost(IDiscoveryClient client) + { + _discoveryClient = client; + _registerTaskKey = Sheduller.RemindEvery(TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(15), RegisterServicesInDiscovery); + } + + internal IExService RegisterService(IExchangeService service) + { + try + { + if (_disposed) throw new ObjectDisposedException("ExServiceHost"); + if (service == null) throw new ArgumentNullException(nameof(service)); + ValidateService(service); + + var key = $"{service.Key}.{service.Protocol}"; + if (_services.ContainsKey(key)) + { + throw new Exception($"[ExServiceHost] Service {key} already registered"); + } + + var server = ExchangeTransportFactory.GetServer(service.Protocol); + if (false == _services.TryAdd(key, new MetaService + { + Server = server, + ServiceInfo = new MicroserviceInfo + { + Endpoint = $"{server.Endpoint.Address}:{server.Endpoint.Port}", + Protocol = service.Protocol, + ServiceKey = service.Key, + Version = service.Version, + ServiceGroup = service.Group, + ServiceType = service.Type + } + })) + { + server.Dispose(); + return null; + } + + RegisterServiceInboxes(service); + + return server; + } + catch (Exception ex) + { + Log.SystemError(ex, "[ExServiceHost] Fault register service"); + return null; + } + } + + internal IExService RegisterService(MicroserviceInfo serviceInfo) + { + try + { + if (_disposed) throw new ObjectDisposedException("ExServiceHost"); + if (serviceInfo == null) throw new ArgumentNullException(nameof(serviceInfo)); + ValidateService(serviceInfo); + + var key = $"{serviceInfo.ServiceKey}.{serviceInfo.Protocol}"; + if (_services.ContainsKey(key)) + { + throw new Exception($"[ExServiceHost] Service {key} already registered"); + } + + var server = ExchangeTransportFactory.GetServer(serviceInfo.Protocol); + if (false == _services.TryAdd(key, new MetaService + { + Server = server, + ServiceInfo = new MicroserviceInfo + { + Endpoint = $"{server.Endpoint.Address}:{server.Endpoint.Port}", + Protocol = serviceInfo.Protocol, + ServiceKey = serviceInfo.ServiceKey, + Version = serviceInfo.Version, + ServiceGroup = serviceInfo.ServiceGroup, + ServiceType = serviceInfo.ServiceType + } + })) + { + server.Dispose(); + return null; + } + return server; + } + catch (Exception ex) + { + Log.SystemError(ex, "[ExServiceHost] Fault register service"); + return null; + } + } + + #region Private methods + private void ValidateService(IExchangeService service) + { + if (string.IsNullOrWhiteSpace(service.Protocol)) + { + throw new ArgumentNullException("Service.Protocol"); + } + if (string.IsNullOrWhiteSpace(service.Key)) + { + throw new ArgumentNullException("Service.Key"); + } + } + private void ValidateService(MicroserviceInfo service) + { + if (string.IsNullOrWhiteSpace(service.Protocol)) + { + throw new ArgumentNullException("Service.Protocol"); + } + if (string.IsNullOrWhiteSpace(service.ServiceKey)) + { + throw new ArgumentNullException("ServiceKey"); + } + } + + private void RegisterServiceInboxes(IExchangeService service) + { + MethodInfo[] methods = service. + GetType(). + GetMethods(BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.FlattenHierarchy | + BindingFlags.Instance); + + var registerHandler = this.GetType().GetMethod("RegisterHandler"); + var registerReplier = this.GetType().GetMethod("RegisterReplier"); + var registerReplierWithNoRequestBody = this.GetType().GetMethod("RegisterReplierWithNoRequestBody"); + + foreach (MethodInfo mi in methods) + { + try + { + foreach (Attribute attr in Attribute.GetCustomAttributes(mi, typeof(ExchangeAttribute))) + { + if (attr.GetType() == typeof(ExchangeMainHandlerAttribute)) + { + var firstArgType = mi.GetParameters().First().ParameterType; + MethodInfo genericMethod = registerHandler.MakeGenericMethod(firstArgType); + genericMethod.Invoke(this, new object[] { service.Protocol, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, CreateDelegate(mi, service) }); + } + else if (attr.GetType() == typeof(ExchangeHandlerAttribute)) + { + var firstArgType = mi.GetParameters().First().ParameterType; + MethodInfo genericMethod = registerHandler.MakeGenericMethod(firstArgType); + genericMethod.Invoke(this, new object[] { service.Protocol, (attr as ExchangeHandlerAttribute).Inbox, CreateDelegate(mi, service) }); + } + else if (attr.GetType() == typeof(ExchangeMainReplierAttribute)) + { + var returnType = mi.ReturnType; + var firstArgType = mi.GetParameters().First().ParameterType; + MethodInfo genericMethod = registerReplier.MakeGenericMethod(firstArgType, returnType); + genericMethod.Invoke(this, new object[] { service.Protocol, ZBaseNetwork.DEFAULT_REQUEST_INBOX, CreateDelegate(mi, service) }); + } + else if (attr.GetType() == typeof(ExchangeReplierAttribute)) + { + var returnType = mi.ReturnType; + var firstArgType = mi.GetParameters().First().ParameterType; + MethodInfo genericMethod = registerReplier.MakeGenericMethod(firstArgType, returnType); + genericMethod.Invoke(this, new object[] { service.Protocol, (attr as ExchangeReplierAttribute).Inbox, CreateDelegate(mi, service) }); + } + else if (attr.GetType() == typeof(ExchangeMainReplierWithoutArgAttribute)) + { + var returnType = mi.ReturnType; + var firstArgType = mi.GetParameters().First().ParameterType; + MethodInfo genericMethod = registerReplierWithNoRequestBody.MakeGenericMethod(returnType); + genericMethod.Invoke(this, new object[] { service.Protocol, ZBaseNetwork.DEFAULT_REQUEST_INBOX, CreateDelegate(mi, service) }); + } + else if (attr.GetType() == typeof(ExchangeReplierWithoutArgAttribute)) + { + var returnType = mi.ReturnType; + var firstArgType = mi.GetParameters().First().ParameterType; + MethodInfo genericMethod = registerReplierWithNoRequestBody.MakeGenericMethod(returnType); + genericMethod.Invoke(this, new object[] { service.Protocol, (attr as ExchangeReplierAttribute).Inbox, CreateDelegate(mi, service) }); + } + } + } + catch (Exception ex) + { + Log.Debug($"[ZExchange] Can't register method {mi.Name} as inbox handler. {ex.ToString()}"); + } + } + } + + private void RegisterServicesInDiscovery() + { + var services = _services. + Values. + Select(s => s.ServiceInfo). + ToList(); + foreach (var service in services) + { + _discoveryClient.Register(service); + } + } + #endregion + + #region Utils + private static Delegate CreateDelegate(MethodInfo methodInfo, object target) + { + Func getType; + var isAction = methodInfo.ReturnType.Equals((typeof(void))); + var types = methodInfo.GetParameters().Select(p => p.ParameterType); + if (isAction) + { + getType = Expression.GetActionType; + } + else + { + getType = Expression.GetFuncType; + types = types.Concat(new[] { methodInfo.ReturnType }); + } + if (methodInfo.IsStatic) + { + return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo); + } + return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); + } + #endregion + + #region Inboxes + /// + /// Регистрация обработчика входящих сообщений + /// + /// Тип сообщения + /// Транспортный протокол + /// Имя точки приема + /// Обработчик + private void RegisterHandler(MetaService meta, string inbox, Action handler) + { + if (_disposed) return; + try + { + meta.Server.RegisterInbox(inbox, handler); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Register inbox handler error. Protocol '{meta.ServiceInfo.Protocol}'. Inbox '{inbox}'. Service '{meta.ServiceInfo.ServiceKey}'"); + } + } + /// + /// Регистрация метода отдающего ответ на входящий запрос + /// + /// Тип входного сообщения + /// Тип ответа + /// Транспортный протокол + /// Имя точки приема + /// Обработчик + private void RegisterReplier(MetaService meta, string inbox, Func handler) + { + if (_disposed) return; + try + { + meta.Server.RegisterInbox(inbox, handler); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Register inbox replier error. Protocol '{meta.ServiceInfo.Protocol}'. Inbox '{inbox}'. Service '{meta.ServiceInfo.ServiceKey}'"); + } + } + /// + /// Регистрация метода отдающего ответ на входящий запрос, не принимающего входящих данных + /// + /// Тип ответа + /// Транспортный протокол + /// Имя точки приема + /// Обработчик + private void RegisterReplierWithNoRequestBody(MetaService meta, string inbox, Func handler) + { + if (_disposed) return; + try + { + meta.Server.RegisterInbox(inbox, handler); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Register inbox replier error. Protocol '{meta.ServiceInfo.Protocol}'. Inbox '{inbox}'. Service '{meta.ServiceInfo.ServiceKey}'"); + } + } + #endregion + + #region Transport helpers + /// + /// Call service with round-robin balancing + /// + /// Service key + /// Service call code + /// true - service called succesfully + internal bool CallService(string serviceKey, Func callHandler) + { + if (_disposed) return false; + List candidates; + try + { + candidates = _discoveryClient.GetServiceEndpoints(serviceKey)?.ToList(); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[ExServiceHost] Error when trying get endpoints for service key '{serviceKey}'"); + return false; + } + if (candidates == null || candidates.Any() == false) + { + Log.Debug($"[ExServiceHost] Not found endpoints for service key '{serviceKey}'"); + return false; + } + var success = false; + foreach (var service in candidates) + { + IExClient transport; + try + { + transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint); + } + catch (Exception ex) + { + Log.SystemError(ex, "[ExServiceHost] Can't get transport for protocol '{0}', service '{1}'", service.Protocol, serviceKey); + continue; + } + try + { + success = callHandler(service.Endpoint, transport); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[ExServiceHost] Error send/request data in service '{serviceKey}'. Endpoint '{service.Endpoint}'"); + success = false; + } + if (success) + { + break; + } + } + return success; + } + + internal bool CallServiceDirect(string endpoint, string serviceKey, Func callHandler) + { + if (_disposed) return false; + ServiceEndpointInfo candidate = null; + try + { + candidate = _discoveryClient.GetService(serviceKey, endpoint); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[ExServiceHost] Error when trying get service info by key '{serviceKey}' and endpoint '{endpoint}'"); + return false; + } + if (candidate == null) + { + Log.Debug($"[ExServiceHost] Not found service info for key '{serviceKey}' and endpoint '{endpoint}'"); + return false; + } + IExClient transport; + try + { + transport = ExchangeTransportFactory.GetClient(candidate.Protocol, candidate.Endpoint); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[ExServiceHost] Can't get transport for protocol '{candidate.Protocol}', service '{serviceKey}'"); + return false; + } + return callHandler(transport); + } + + internal IEnumerable GetClientEnumerator(string serviceKey) + { + if (!_disposed) + { + List candidates; + try + { + candidates = _discoveryClient.GetServiceEndpoints(serviceKey)?.ToList(); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error when trying get endpoints for service key '{serviceKey}'"); + candidates = null; + } + if (candidates != null && candidates.Any()) + { + foreach (var service in candidates) + { + IExClient transport; + try + { + transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint); + } + catch (Exception ex) + { + Log.SystemError(ex, "[Exchange] Can't get transport for protocol '{0}', endpoint '{1}'", service.Protocol, service.Endpoint); + continue; + } + yield return transport; + } + } + else + { + Log.Debug($"[Exchange] Not found endpoints for service key '{serviceKey}'"); + } + } + } + + internal IEnumerable GetClientEnumeratorByType(string serviceType) + { + if (!_disposed) + { + List candidates; + try + { + candidates = _discoveryClient.GetServiceEndpointsByType(serviceType)?.ToList(); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error when trying get endpoints for service type '{serviceType}'"); + candidates = null; + } + if (candidates != null && candidates.Any()) + { + foreach (var service in candidates) + { + IExClient transport; + try + { + transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint); + } + catch (Exception ex) + { + Log.SystemError(ex, "[Exchange] Can't get transport for protocol '{0}', endpoint '{1}'", service.Protocol, service.Endpoint); + continue; + } + yield return transport; + } + } + else + { + Log.Debug($"[Exchange] Not found endpoints for service type '{serviceType}'"); + } + } + } + + internal IEnumerable GetClientEnumeratorByGroup(string serviceGroup) + { + if (!_disposed) + { + List candidates; + try + { + candidates = _discoveryClient.GetServiceEndpointsByGroup(serviceGroup)?.ToList(); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error when trying get endpoints for service group '{serviceGroup}'"); + candidates = null; + } + if (candidates != null && candidates.Any()) + { + foreach (var service in candidates) + { + IExClient transport; + try + { + transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint); + } + catch (Exception ex) + { + Log.SystemError(ex, "[Exchange] Can't get transport for protocol '{0}', endpoint '{1}'", service.Protocol, service.Endpoint); + continue; + } + yield return transport; + } + } + else + { + Log.Debug($"[Exchange] Not found endpoints for service group '{serviceGroup}'"); + } + } + } + #endregion + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + Sheduller.Remove(_registerTaskKey); + foreach (var s in _services) + { + s.Value.Server.Dispose(); + } + } + } +} diff --git a/ZeroLevel.Microservices/Exchange.cs b/ZeroLevel.Microservices/Exchange.cs new file mode 100644 index 0000000..4a90eca --- /dev/null +++ b/ZeroLevel.Microservices/Exchange.cs @@ -0,0 +1,640 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ZeroLevel.Microservices.Contracts; +using ZeroLevel.Microservices.Model; +using ZeroLevel.Network.Microservices; +using ZeroLevel.Services.Network; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.Microservices +{ + /// + /// Обеспечивает обмен данными между сервисами + /// + public sealed class Exchange : + IDisposable + { + private readonly IDiscoveryClient _discoveryClient; + private readonly ExServiceHost _host; + + #region Ctor + public Exchange(IDiscoveryClient discoveryClient) + { + this._discoveryClient = discoveryClient ?? throw new ArgumentNullException(nameof(discoveryClient)); + this._host = new ExServiceHost(this._discoveryClient); + } + #endregion + + /// + /// Регистрация сервиса + /// + public IExService RegisterService(IExchangeService service) + { + return _host.RegisterService(service); + } + + public IExService RegisterService(MicroserviceInfo service) + { + return _host.RegisterService(service); + } + + #region Balanced send + /// + /// Отправка сообщения сервису + /// + /// Ключ сервиса + /// Имя точки приема сообщений + /// Сообщение + /// + public bool Send(string serviceKey, string inbox, T data) + { + try + { + return _host.CallService(serviceKey, (endpoint, transport) => transport.Send(inbox, data).Success); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error send data in service '{serviceKey}'. Inbox '{inbox}'"); + } + return false; + } + + public bool Send(string serviceKey, T data) => Send(serviceKey, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data); + #endregion + + #region Balanced request + public Tresp Request(string serviceKey, string inbox, Treq data) + { + Tresp response = default(Tresp); + try + { + if (false == _host.CallService(serviceKey, (endpoint, transport) => + { + try + { + using (var waiter = new ManualResetEventSlim(false)) + { + if (false == transport.Request(inbox, data, resp => + { + response = resp; + waiter.Set(); + }).Success) + { + return false; + } + if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS)) + { + return false; + } + } + return true; + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'"); + } + return false; + })) + { + Log.SystemWarning($"[Exchange] No responce on request. Service key '{serviceKey}'. Inbox '{inbox}'"); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'"); + } + return response; + } + + public Tresp Request(string serviceKey, string inbox) + { + Tresp response = default(Tresp); + try + { + if (false == _host.CallService(serviceKey, (endpoint, transport) => + { + try + { + using (var waiter = new ManualResetEventSlim(false)) + { + if (false == transport.Request(inbox, resp => + { + response = resp; + waiter.Set(); + }).Success) + { + return false; + } + if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS)) + { + return false; + } + } + return true; + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'"); + } + return false; + })) + { + Log.SystemWarning($"[Exchange] No responce on request. Service key '{serviceKey}'. Inbox '{inbox}'"); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'"); + } + return response; + } + + public Tresp Request(string serviceKey, Treq data) => + Request(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data); + + public Tresp Request(string serviceKey) => + Request(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX); + #endregion + + #region Direct request + public Tresp RequestDirect(string endpoint, string serviceKey, string inbox, Treq data) + { + Tresp response = default(Tresp); + try + { + if (false == _host.CallServiceDirect(endpoint, serviceKey, (transport) => + { + try + { + using (var waiter = new ManualResetEventSlim(false)) + { + if (false == transport.Request(inbox, data, resp => + { + response = resp; + waiter.Set(); + }).Success) + { + return false; + } + if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS)) + { + return false; + } + } + return true; + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'"); + } + return false; + })) + { + Log.SystemWarning($"[Exchange] No responce on direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'"); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'"); + } + return response; + } + + public Tresp RequestDirect(string endpoint, string serviceKey, string inbox) + { + Tresp response = default(Tresp); + try + { + if (false == _host.CallServiceDirect(endpoint, serviceKey, (transport) => + { + try + { + using (var waiter = new ManualResetEventSlim(false)) + { + if (false == transport.Request(inbox, resp => + { + response = resp; + waiter.Set(); + }).Success) + { + return false; + } + if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS)) + { + return false; + } + } + return true; + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'"); + } + return false; + })) + { + Log.SystemWarning($"[Exchange] No responce on direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'"); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error direct request to service '{serviceKey}'. Inbox '{inbox}'"); + } + return response; + } + + public Tresp RequestDirect(string endpoint, string serviceKey, Treq data) => + RequestDirect(endpoint, serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data); + + public Tresp RequestDirect(string endpoint, string serviceKey) => + RequestDirect(endpoint, serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX); + #endregion + + #region Broadcast + /// + /// Отправка сообщения всем сервисам с указанным ключом в указанный обработчик + /// + /// Тип сообщения + /// Ключ сервиса + /// Имя обработчика + /// Сообщение + /// true - при успешной отправке + public bool SendBroadcast(string serviceKey, string inbox, T data) + { + try + { + foreach (var client in _host.GetClientEnumerator(serviceKey)) + { + Task.Run(() => + { + try + { + client.Send(inbox, data); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast send data to services '{serviceKey}'. Inbox '{inbox}'"); + } + }); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast send data in service '{serviceKey}'. Inbox '{inbox}'"); + } + return false; + } + /// + /// Отправка сообщения всем сервисам с указанным ключом, в обработчик по умолчанию + /// + /// Тип сообщения + /// Ключ сервиса + /// Сообщение + /// true - при успешной отправке + public bool SendBroadcast(string serviceKey, T data) => SendBroadcast(serviceKey, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data); + /// + /// Отправка сообщения всем сервисам конкретного типа в указанный обработчик + /// + /// Тип сообщения + /// Тип сервиса + /// Имя обработчика + /// Сообщение + /// true - при успешной отправке + public bool SendBroadcastByType(string serviceType, string inbox, T data) + { + try + { + foreach (var client in _host.GetClientEnumeratorByType(serviceType)) + { + Task.Run(() => + { + try + { + client.Send(inbox, data); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceType}'. Inbox '{inbox}'"); + } + }); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceType}'. Inbox '{inbox}'"); + } + return false; + } + /// + /// Отправка сообщения всем сервисам конкретного типа, в обработчик по умолчанию + /// + /// Тип сообщения + /// Тип сервиса + /// Сообщение + /// true - при успешной отправке + public bool SendBroadcastByType(string serviceType, T data) => + SendBroadcastByType(serviceType, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data); + /// + /// Отправка сообщения всем сервисам конкретной группы в указанный обработчик + /// + /// Тип сообщения + /// Группа сервиса + /// Имя обработчика + /// Сообщение + /// true - при успешной отправке + public bool SendBroadcastByGroup(string serviceGroup, string inbox, T data) + { + try + { + foreach (var client in _host.GetClientEnumeratorByGroup(serviceGroup)) + { + Task.Run(() => + { + try + { + client.Send(inbox, data); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceGroup}'. Inbox '{inbox}'"); + } + }); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceGroup}'. Inbox '{inbox}'"); + } + return false; + } + /// + /// Отправка сообщения всем сервисам конкретной группы, в обработчик по умолчанию + /// + /// Тип сообщения + /// Группа сервиса + /// Сообщение + /// true - при успешной отправке + public bool SendBroadcastByGroup(string serviceGroup, T data) => + SendBroadcastByGroup(serviceGroup, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data); + /// + /// Широковещательный опрос сервисов по ключу + /// + /// Тип запроса + /// Тип ответа + /// Ключ сервиса + /// Имя обработчика + /// Запрос + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcast(string serviceKey, string inbox, Treq data) + { + try + { + var clients = _host.GetClientEnumerator(serviceKey).ToList(); + return _RequestBroadcast(clients, inbox, data); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast request to service '{serviceKey}'. Inbox '{inbox}'"); + } + return Enumerable.Empty(); + } + /// + /// Широковещательный опрос сервисов по ключу, без сообщеня запроса + /// + /// Тип ответа + /// Ключ сервиса + /// Имя обработчика + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcast(string serviceKey, string inbox) + { + try + { + var clients = _host.GetClientEnumerator(serviceKey).ToList(); + return _RequestBroadcast(clients, inbox); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast request to service '{serviceKey}'. Inbox '{inbox}'"); + } + return Enumerable.Empty(); + } + /// + /// Широковещательный опрос сервисов по ключу, в обработчик по умолчанию + /// + /// Тип запроса + /// Тип ответа + /// Ключ сервиса + /// Запрос + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcast(string serviceKey, Treq data) => + RequestBroadcast(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data); + /// + /// Широковещательный опрос сервисов по ключу, без сообщеня запроса, в обработчик по умолчанию + /// + /// Тип ответа + /// Ключ сервиса + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcast(string serviceKey) => + RequestBroadcast(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX); + /// + /// Широковещательный опрос сервисов по типу сервису + /// + /// Тип запроса + /// Тип ответа + /// Тип сервиса + /// Имя обработчика + /// Запрос + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByType(string serviceType, string inbox, Treq data) + { + try + { + var clients = _host.GetClientEnumeratorByType(serviceType).ToList(); + return _RequestBroadcast(clients, inbox, data); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceType}'. Inbox '{inbox}'"); + } + return Enumerable.Empty(); + } + /// + /// Широковещательный опрос сервисов по типу сервису, без сообщеня запроса + /// + /// Тип ответа + /// Тип сервиса + /// Имя обработчика + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByType(string serviceType, string inbox) + { + try + { + var clients = _host.GetClientEnumeratorByType(serviceType).ToList(); + return _RequestBroadcast(clients, inbox); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceType}'. Inbox '{inbox}'"); + } + return Enumerable.Empty(); + } + /// + /// Широковещательный опрос сервисов по типу сервису, в обработчик по умолчанию + /// + /// Тип запроса + /// Тип ответа + /// Тип сервиса + /// Запрос + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByType(string serviceType, Treq data) => + RequestBroadcastByType(serviceType, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data); + /// + /// Широковещательный опрос сервисов по типу, без сообщеня запроса, в обработчик по умолчанию + /// + /// Тип ответа + /// Тип сервиса + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByType(string serviceType) => + RequestBroadcastByType(serviceType, ZBaseNetwork.DEFAULT_REQUEST_INBOX); + /// + /// Широковещательный опрос сервисов по группе сервисов + /// + /// Тип запроса + /// Тип ответа + /// Группа сервиса + /// Имя обработчика + /// Запрос + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByGroup(string serviceGroup, string inbox, Treq data) + { + try + { + var clients = _host.GetClientEnumeratorByGroup(serviceGroup).ToList(); + return _RequestBroadcast(clients, inbox, data); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceGroup}'. Inbox '{inbox}'"); + } + return Enumerable.Empty(); + } + /// + /// Широковещательный опрос сервисов по группе сервисов, без сообщения запроса + /// + /// Тип ответа + /// Группа сервиса + /// Имя обработчика + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByGroup(string serviceGroup, string inbox) + { + try + { + var clients = _host.GetClientEnumeratorByGroup(serviceGroup).ToList(); + return _RequestBroadcast(clients, inbox); + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceGroup}'. Inbox '{inbox}'"); + } + return Enumerable.Empty(); + } + /// + /// Широковещательный опрос сервисов по группе сервисов в обработчик по умолчанию + /// + /// Тип запроса + /// Тип ответа + /// Группа сервиса + /// Запрос + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByGroup(string serviceGroup, Treq data) => + RequestBroadcastByGroup(serviceGroup, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data); + /// + /// Широковещательный опрос сервисов по группе сервисов, без сообщения запроса, в обработчик по умолчанию + /// + /// Тип ответа + /// Группа сервиса + /// Обработчик ответа + /// true - в случае успешной рассылки + public IEnumerable RequestBroadcastByGroup(string serviceGroup) => + RequestBroadcastByGroup(serviceGroup, ZBaseNetwork.DEFAULT_REQUEST_INBOX); + + #region Private + private IEnumerable _RequestBroadcast(List clients, string inbox, Treq data) + { + var response = new List(); + using (var waiter = new CountdownEvent(clients.Count)) + { + foreach (var client in clients) + { + Task.Run(() => + { + try + { + if (false == client.Request(inbox, data, resp => { waiter.Signal(); response.Add(resp); }).Success) + { + waiter.Signal(); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error direct request to service '{client.Endpoint}' in broadcast request. Inbox '{inbox}'"); + waiter.Signal(); + } + }); + } + waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS); + } + return response; + } + + private IEnumerable _RequestBroadcast(List clients, string inbox) + { + var response = new List(); + using (var waiter = new CountdownEvent(clients.Count)) + { + foreach (var client in clients) + { + Task.Run(() => + { + try + { + if (false == client.Request(inbox, resp => { waiter.Signal(); response.Add(resp); }).Success) + { + waiter.Signal(); + } + } + catch (Exception ex) + { + Log.SystemError(ex, $"[Exchange] Error direct request to service '{client.Endpoint}' in broadcast request. Inbox '{inbox}'"); + waiter.Signal(); + } + }); + } + waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS); + } + return response; + } + #endregion + + #endregion + + public void Dispose() + { + this._host.Dispose(); + } + } +} diff --git a/ZeroLevel.Microservices/ExchangeTransportFactory.cs b/ZeroLevel.Microservices/ExchangeTransportFactory.cs new file mode 100644 index 0000000..4015c70 --- /dev/null +++ b/ZeroLevel.Microservices/ExchangeTransportFactory.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using ZeroLevel.Network.Microservices; +using ZeroLevel.Services.Network; +using ZeroLevel.Services.Network.Contract; + +namespace ZeroLevel.Microservices +{ + internal static class ExchangeTransportFactory + { + private static readonly Dictionary _customServers = new Dictionary(); + private static readonly Dictionary _customClients = new Dictionary(); + private static readonly ConcurrentDictionary _clientInstances = new ConcurrentDictionary(); + + /// + /// Сканирование указанной сборки для поиска типов реализующих интерфейсы + /// IExchangeServer или IExchangeClient + /// + internal static void ScanAndRegisterCustomTransport(Assembly asm) + { + foreach (var type in asm.GetExportedTypes()) + { + var serverAttr = type.GetCustomAttribute(); + if (serverAttr != null && + string.IsNullOrWhiteSpace(serverAttr.Protocol) == false && + typeof(IZObservableServer).IsAssignableFrom(type)) + { + _customServers[serverAttr.Protocol] = type; + } + var clientAttr = type.GetCustomAttribute(); + if (clientAttr != null && + string.IsNullOrWhiteSpace(clientAttr.Protocol) == false && + typeof(IZTransport).IsAssignableFrom(type)) + { + _customClients[clientAttr.Protocol] = type; + } + } + } + + /// + /// Создает сервер для приема сообщений по указанному протоколу + /// + /// Протокол + /// Сервер + internal static ExService GetServer(string protocol) + { + ExService instance = null; + if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase)) + { + instance = new ExService(new ZExSocketObservableServer(new System.Net.IPEndPoint(IPFinder.GetNonLoopbackAddress(), IPFinder.GetFreeTcpPort()))); + } + else + { + var key = protocol.Trim().ToLowerInvariant(); + if (_customServers.ContainsKey(key)) + { + instance = new ExService((IZObservableServer)Activator.CreateInstance(_customServers[key])); + } + } + if (instance != null) + { + return instance; + } + throw new NotSupportedException($"Protocol {protocol} not supported"); + } + + /// + /// Создает клиента для обращений к серверу по указанному протоколу + /// + /// Протокол + /// Адрес сервера + /// Клиент + internal static ExClient GetClient(string protocol, string endpoint) + { + ExClient instance = null; + var cachee_key = $"{protocol}:{endpoint}"; + if (_clientInstances.ContainsKey(cachee_key)) + { + instance = _clientInstances[cachee_key]; + if (instance.Status == ZTransportStatus.Working) + { + return instance; + } + _clientInstances.TryRemove(cachee_key, out instance); + instance.Dispose(); + instance = null; + } + if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase)) + { + instance = new ExClient(new ZSocketClient(SocketExtensions.CreateIPEndPoint(endpoint))); + } + else + { + var key = protocol.Trim().ToLowerInvariant(); + if (_customClients.ContainsKey(key)) + { + instance = new ExClient((IZTransport)Activator.CreateInstance(_customClients[key], new object[] { endpoint })); + } + } + if (instance != null) + { + _clientInstances[cachee_key] = instance; + return instance; + } + throw new NotSupportedException($"Protocol {protocol} not supported"); + } + } +} diff --git a/ZeroLevel.Microservices/Model/Checkpoint.cs b/ZeroLevel.Microservices/Model/Checkpoint.cs new file mode 100644 index 0000000..57f40f8 --- /dev/null +++ b/ZeroLevel.Microservices/Model/Checkpoint.cs @@ -0,0 +1,101 @@ +using System; +using System.Runtime.Serialization; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.Microservices.Model +{ + [DataContract] + public class Checkpoint : + ICloneable, + IEquatable, + IBinarySerializable + { + public Guid Id { get; set; } + public string SourceAppKey { get; set; } + public string DestinationAppKey { get; set; } + public string ReasonPhrase { get; set; } + public long Timestamp { get; set; } + public CheckpointType CheckpointType { get; set; } + public byte[] Payload { get; set; } + + #region IBinarySerializable + public void Deserialize(IBinaryReader reader) + { + this.Id = reader.ReadGuid(); + this.Timestamp = reader.ReadLong(); + this.SourceAppKey = reader.ReadString(); + this.DestinationAppKey = reader.ReadString(); + this.ReasonPhrase = reader.ReadString(); + this.CheckpointType = (CheckpointType)reader.ReadInt32(); + this.Payload = reader.ReadBytes(); + } + + public void Serialize(IBinaryWriter writer) + { + writer.WriteGuid(this.Id); + writer.WriteLong(this.Timestamp); + writer.WriteString(this.SourceAppKey); + writer.WriteString(this.DestinationAppKey); + writer.WriteString(this.ReasonPhrase); + writer.WriteInt32((int)this.CheckpointType); + writer.WriteBytes(this.Payload); + } + #endregion + + #region Ctors + public Checkpoint() + { + this.Id = Guid.NewGuid(); + this.Timestamp = DateTime.Now.Ticks; + } + public Checkpoint(Guid id) + { + this.Timestamp = DateTime.Now.Ticks; + this.Id = id; + } + public Checkpoint(Checkpoint other) + { + this.Id = other.Id; + this.Timestamp = other.Timestamp; + this.SourceAppKey = other.SourceAppKey; + this.DestinationAppKey = other.DestinationAppKey; + this.CheckpointType = other.CheckpointType; + this.Payload = other.Payload; + this.ReasonPhrase = other.ReasonPhrase; + } + #endregion + + #region Equals & Hash + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object obj) + { + return this.Equals(obj as Checkpoint); + } + #endregion + + #region ICloneable + public object Clone() + { + return new Checkpoint(this); + } + #endregion + + #region IEquatable + public bool Equals(Checkpoint other) + { + if (this.Id != other.Id) return false; + if (this.Timestamp != other.Timestamp) return false; + if (this.CheckpointType != other.CheckpointType) return false; + if (string.Compare(this.SourceAppKey, other.SourceAppKey, StringComparison.OrdinalIgnoreCase) != 0) return false; + if (string.Compare(this.DestinationAppKey, other.DestinationAppKey, StringComparison.OrdinalIgnoreCase) != 0) return false; + if (string.Compare(this.ReasonPhrase, other.ReasonPhrase, StringComparison.OrdinalIgnoreCase) != 0) return false; + if (false == ArrayExtensions.Equals(this.Payload, other.Payload)) return false; + return true; + } + #endregion + } +} diff --git a/ZeroLevel.Microservices/Model/CheckpointArc.cs b/ZeroLevel.Microservices/Model/CheckpointArc.cs new file mode 100644 index 0000000..21f9d43 --- /dev/null +++ b/ZeroLevel.Microservices/Model/CheckpointArc.cs @@ -0,0 +1,21 @@ +using System.Runtime.Serialization; + +namespace ZeroLevel.Microservices.Model +{ + [DataContract] + public sealed class CheckpointArc + : Checkpoint + { + public CheckpointArc(Checkpoint other) + { + this.Id = other.Id; + this.Id = other.Id; + this.Timestamp = other.Timestamp; + this.SourceAppKey = other.SourceAppKey; + this.DestinationAppKey = other.DestinationAppKey; + this.CheckpointType = other.CheckpointType; + this.Payload = other.Payload; + this.ReasonPhrase = other.ReasonPhrase; + } + } +} diff --git a/ZeroLevel.Microservices/Model/CheckpointType.cs b/ZeroLevel.Microservices/Model/CheckpointType.cs new file mode 100644 index 0000000..7dac225 --- /dev/null +++ b/ZeroLevel.Microservices/Model/CheckpointType.cs @@ -0,0 +1,10 @@ +namespace ZeroLevel.Microservices.Model +{ + public enum CheckpointType + { + Interrupt = 0, + Fatal = 1, + Finish = 2, + Transfer = 3 + } +} diff --git a/ZeroLevel.Microservices/Properties/AssemblyInfo.cs b/ZeroLevel.Microservices/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..da29277 --- /dev/null +++ b/ZeroLevel.Microservices/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +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("ZeroLevel.Microservices")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ZeroLevel.Microservices")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[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("6452d91a-2dac-4982-83af-77472051e81b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ZeroLevel.Microservices/WebApiDiscoveryClient.cs b/ZeroLevel.Microservices/WebApiDiscoveryClient.cs new file mode 100644 index 0000000..cbbdb8b --- /dev/null +++ b/ZeroLevel.Microservices/WebApiDiscoveryClient.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using ZeroLevel.Microservices.Contracts; +using ZeroLevel.Models; +using ZeroLevel.Network.Microservices; +using ZeroLevel.ProxyREST; +using ZeroLevel.Services.Collections; + +namespace ZeroLevel.Microservices +{ + public sealed class WebApiDiscoveryClient : + BaseProxy, IDiscoveryClient + { + #region WebAPI + private IEnumerable GetRecords() + { + return GET>("api/v0/routes"); + } + + public InvokeResult Post(MicroserviceInfo info) + { + return POST("api/v0/routes", info); + } + #endregion + + // Таблица по ключам + private readonly ConcurrentDictionary> _tableByKey = + new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _tableByGroups = + new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _tableByTypes = + new ConcurrentDictionary>(); + + private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + + public WebApiDiscoveryClient(string url) + : base(url) + { + UpdateServiceListInfo(); + Sheduller.RemindEvery(TimeSpan.FromSeconds(30), UpdateServiceListInfo); + } + + private void UpdateOrAddRecord(string key, ServiceEndpointsInfo info) + { + var groupName = info.ServiceGroup.ToLowerInvariant(); + var typeName = info.ServiceType.ToLowerInvariant(); + if (_tableByKey.ContainsKey(key) == false) + { + _tableByKey.TryAdd(key, new RoundRobinCollection()); + } + else + { + _tableByKey[key].Clear(); + } + if (_tableByGroups.ContainsKey(groupName) == false) + { + _tableByGroups.TryAdd(groupName, new RoundRobinCollection()); + } + if (_tableByTypes.ContainsKey(typeName) == false) + { + _tableByTypes.TryAdd(typeName, new RoundRobinCollection()); + } + foreach (var e in info.Endpoints) + { + if (false == _tableByKey[key].Contains(e)) + { + _tableByKey[key].Add(e); + _tableByGroups[groupName].Add(e); + _tableByTypes[typeName].Add(e); + } + } + } + + private void UpdateServiceListInfo() + { + IEnumerable records; + try + { + records = GetRecords(); + } + catch (Exception ex) + { + Log.Error(ex, "[WebApiDiscoveryClient] Update service list error, discrovery service response is absent"); + return; + } + if (records == null) + { + Log.Warning("[WebApiDiscoveryClient] Update service list canceled, discrovery response is empty"); + return; + } + _lock.EnterWriteLock(); + try + { + _tableByGroups.Clear(); + _tableByTypes.Clear(); + var keysToRemove = new List(_tableByKey.Keys); + foreach (var info in records) + { + var key = info.ServiceKey.Trim().ToLowerInvariant(); + UpdateOrAddRecord(key, info); + keysToRemove.Remove(key); + } + RoundRobinCollection removed; + foreach (var key in keysToRemove) + { + _tableByKey.TryRemove(key, out removed); + removed.Dispose(); + } + } + catch (Exception ex) + { + Log.Error(ex, "[WebApiDiscoveryClient] Update service list error"); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void Register(MicroserviceInfo info) + { + try + { + var result = Post(info); + if (result.Success == false) + { + Log.Warning($"[WebApiDiscoveryClient] Service can't register. Discovery reason: {result.Comment}. Comment: {result.Comment}"); + } + } + catch (Exception ex) + { + Log.Error(ex, "[WebApiDiscoveryClient] Fault register"); + } + } + + public ServiceEndpointInfo GetService(string serviceKey, string endpoint) + { + var key = serviceKey.Trim().ToLowerInvariant(); + if (_tableByKey.ContainsKey(key) && _tableByKey[key].MoveNext()) + { + return _tableByKey[key].Find(s => s.Endpoint.Equals(endpoint, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + } + return null; + } + + public IEnumerable GetServiceEndpoints(string serviceKey) + { + var key = serviceKey.Trim().ToLowerInvariant(); + if (_tableByKey.ContainsKey(key) && _tableByKey[key].MoveNext()) + { + return _tableByKey[key].GetCurrentSeq(); + } + return Enumerable.Empty(); + } + + public IEnumerable GetServiceEndpointsByGroup(string serviceGroup) + { + var group = serviceGroup.Trim().ToLowerInvariant(); + if (_tableByGroups.ContainsKey(group) && _tableByGroups[group].MoveNext()) + { + return _tableByGroups[group].GetCurrentSeq(); + } + return Enumerable.Empty(); + } + + public IEnumerable GetServiceEndpointsByType(string serviceType) + { + var type = serviceType.Trim().ToLowerInvariant(); + if (_tableByTypes.ContainsKey(type) && _tableByTypes[type].MoveNext()) + { + return _tableByTypes[type].GetCurrentSeq(); + } + return Enumerable.Empty(); + } + } +} diff --git a/ZeroLevel.Microservices/ZeroLevel.Microservices.csproj b/ZeroLevel.Microservices/ZeroLevel.Microservices.csproj new file mode 100644 index 0000000..a8f737e --- /dev/null +++ b/ZeroLevel.Microservices/ZeroLevel.Microservices.csproj @@ -0,0 +1,71 @@ + + + + + Debug + AnyCPU + {6452D91A-2DAC-4982-83AF-77472051E81B} + Library + Properties + ZeroLevel.Microservices + ZeroLevel.Microservices + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\..\..\source\repos\ES\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + {37020d8d-34e8-4ec3-a447-8396d5080457} + ZeroLevel + + + + + + + \ No newline at end of file diff --git a/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs b/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs new file mode 100644 index 0000000..e69de29 diff --git a/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs b/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs new file mode 100644 index 0000000..e69de29 diff --git a/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs b/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs new file mode 100644 index 0000000..e69de29 diff --git a/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csproj.CoreCompileInputs.cache b/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..2a7843d --- /dev/null +++ b/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +f044fc552d067d297acec51362b39633b46e17c2 diff --git a/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csprojAssemblyReference.cache b/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csprojAssemblyReference.cache new file mode 100644 index 0000000000000000000000000000000000000000..6a06b165189eee78aacf9e1b4431e90d84576e67 GIT binary patch literal 27363 zcmeI44~!Jm9mjX?_TO9n90yjQ1$!tJD7bgWd4MRwd3VTx9LOSqE}V0_b9dv-&Mq^v zhlfzXrd2C#uvXjnCt@R{YDA*8Ra-Gi8*SsCs8O0|Q#H{hXrpb^Mopx@@668Lo11q{ zfw{yc^Dg__ow@IOZ{~g8y!V?o?@cHi3WbhSz&FC!Dl%JED_Ol@6nf0mMI}YerPfPE zzmaM!=<>X}4YF=11#L-VbE+}bkZPD$*IH7|k}faNb$zOrEXQpu964W7y4yw zNq0k2Pjk=0p2o&pbA!|*MUmMIaUSVG!<6%BQ!-_!SdXI0)>id2`(Jbvsw!tqIs_xN zLNfYHsautC)U29J1m?d=<0MF4Qjl`XrL0-d7v=;J3Ntg8rNVK((lh^8n0VOBv z3G7HK+vNzIkrCSdioTrkPjZCF*Gs{UY zZ!75inN(-{`k}3Xb5jdb^E2fGOw0O!lEo=!Wy472R5gZj>Kn~GH|EvUC5pDy$}K^; zbq&vL=rromZL5q-%XX?})_Gt%GcZ4OUZz{oGDWr2t7t~1O_6%Ff?+CIBhx8wGpUN8 z4zDx{nw1HoOr+tN_)?5{BU{i_rQ6CTIoc)HDVwPkrdhOd8$-EeN9Cl-bpxDGA;yXj z`BHl2jF1Vcdu}w(2920?I^!v)(rD(iuB4etUQW@qrAVr>T|%2>WnDvAE2ENi<+@L` zru!t_t|4e!Mzu`aBvZ0Ztd?>b7?l$%R}m{Gn6zxRUt`J|x+1&1nn-ys8=iNjqa)3= zBA3hEb!~D{EezIeXc9FM6VUFW4zfuwr@?#!=5(0JFjHWr!h93vTQJjLro)^8^KCLw zBrtb~2j7v}Cu4PMbJ3JrztG-R=L@+Aefp~;Es~RqM$AA5Xhd~$K-Q~zv{0xj5(|e| zA~if^;Ffc#avw0*Nv(dRfUew({xJ*_nmqNhX}7+fKvaiu$g3H=&JY#Q54fCoNuJ6s*28c-ie&=(AiGezv}f_uUTiy&c+cZ zlc6wJdQWW@?Pasc#O6TAmj$?cBYYPT<|4wp;So?ryb;bpgcKsw508NQDfMLT05$;N}PW5wWz&}B$mr;CbHyscT0vG@0SOQ zlA=c?BW>z&#Ym$?OCn~m)Iy0cC7&-@zJEfO^HQ-W=R)Cl9PU54=3#0ysI8>0@#UVw zaq|=5M00spYeDN#dP}<9F%Nmql}2=heRu_3G5hit{I9XfIoT(qbStW24u->4>l==( zro*Zk=9{uk+R4_ytR;hLV|}ipACZUY=IrDS7-_@@aJSOc6-q>QQDO%vu~Rz_SDMHn zmL}7SUTSfDeSPcVde=QleWls9JotLcgRjS(2)66t!Pk=qUtjj%>p2g;epo5N!Jtcv zF0eS7YO9Fwed1SFEn0b8n6dR_#%cvUujDi-rK!c(Wwd7u_Gmp>YsZG69XV^qxkx*3YbHp6Uz=^|5$ z4hbQS-sV&e`YMXfB$#e8wdl7H;!UuqDks?%W+^Jm!N@Q@Fui1I(a|Bq(TiC*#OV7Z zilvXDF$Lyon0^=)CQqgo-6KLAec!2+K6!5;R)8tOY$a2Rt`s4Te$q-DH>oFa4mw3+ z28;<)f*F9>26GL}Ak20$wdju#;^=Ho<&>_a=xhhfPMGhJsa-|}A&lM-ok%!_mdw-Y z?V_D*H_Y{9P(iE@wBeo9P@(0E?4pew^aCvy{vG=PnH3{8H{|YfO^(amAGtk=wo;n^ zXFXk&)6>B{wtBj+Sx5InI;w;-ERtk5&?dW)j1$5Z=WS)q*c%?;IKzvgralTEC_tn3 z`sst*Ol2Cgy_qE2LmTYJWSn4w(Rnai1z_4qAAs=}%w8(fq48wiIqlF4QzHL|-9i%f zQ->9O+qm*PE>(bQIY|Rt{^Gig%Jj7IUBTswmMd&CQp?G8G?MIg`WO2-87J77`glye z0!+Ba3o!YM=?*GWr*mx6rEQAVn`C#=PIi|=hrVq0Hlsl>cUC`-Ep<&Tjm-QTzb*+;J z6g+z+MJPcbh96vrVhA&+sQy;e$Ei#?N9hULWKVL1Zr9RwzLvImYiaa~0FCw7$IOTeeq#{dD;VJ84qTu01O8EgKNg$Yvx%hQ+MNj1a$u3p>sY@XR&||9-ahr{H608 zl_}SvJr6X0@}OD3qiGhP!6U5zjlXDKpfatrXfFcHOCB(dJeURn7(8YS!1xR16)Mw- zWp~w@50~DmTKDF!0@7<9Nb`6|a|MvP0!Y6518-27I%C7Gc_vAA6qx?v!8D70+Q={+h_*9h_Zw1fTKAw%CboGCn<$pSc-<`cm27tS#%Q?7aYfHv8O zWSn55nZ%=+C_s}VeSpSaG#^o!R^024X@mXKp+es_nDIQAaRM+hNdqwcg878X)YYJU zN;}wRWSn5bVLY6K08S6-133P|`JBqMrhMO8_qOOXZl|s{U;#g zFQIChN6R?{5!z(dl59|*V59khNAtM=jY84@jlXEHP|el%Fu+NYae@u!Qy$JI0ytL( zaC{%q8Ali@9@}_;squjMmnBEp(!aR`xlfRf|5C+bVI}>Qm@}PN(NAsot4Hl#YX#7QUHesl^3bO!a zwg=2n9?Tm8FvS3j?_JMa!r-0_G7ogl@z8mdr}K({4pwglmyN%dO#@+YrXaQr(mMo{ zY?3ts(0mV|7kNN02moP0Yyik#K+S}K>v9$V%|Z{F=Xo^G3D97fa)8EPG>ZvCrOP=V zU@q{0Il_Z^RsaS|#{)3_f>}x!Tw^-R0H?(R=P(cFDFGZT=?~!e3#W}ROezl@tk)&Y zpm##zjbcgG4ob^Cl%C)zJuaYxcP<2!{H4@E7@TJcLr;BHg3u}tp~rYaj|vFkg%|-L ze+hLGh7pDg)&SF552lBCOa}#+@XC?^lfRhKgyHmZbLT8fYn8Hn){^M;0CkxM)B`-I z`vsuzTA2Wpzo0e}2KRmHCeXRULuWrv=RN_Q?ExL%M-es?hNz(A>$RxkG>k@3;yYIe#0uUc%rsa#+Za zWPPBcc<9{D)45GR2QMEB==e)VB@C4Tg*?D$9x%7?VD<{Y;DvAj7=OWRB@E7GgSVL{ znGQOJht3|J&dpvrHM@w;KKgw9m#golqjW7-4_{Avuw;Gs!t7k`v*VVqBTpY$rxP(%?`^lXuUIZnS-swmy zsp6GHLg_w7S~=ExN0RRMNDqu8#alz%$NJz%QoOOkEj=`n6t7QkOYtrR;b~zFyIYC{ z?LsM5RlB8FVl9+niLYCV^}a&s^ByVIp9-Z|1nEu{DiLkgvskmr_ScAij*DPnFZ=8FlXn4M%v z8=qdZ;h_bc&0$Pc5=t>q#+@o=%?PEKtYJwjC4iY5LMbK>xKqVU0-+R-v)xiW+ZIak zY}GBrgI1vwPx;)(ibsAzDV{pHQ^g}Fp%l+S+)_Lg5lS&C@0MbSUMR)Tuv?08Vxbgc zlx`^oEQL~xlUdRVEex6or5LGkOEGLClw#PxeE=9k5K3{^>P{86utF*BqTH$CmP#mv dOYfG#zZXj3WV)sBIE7MpJZ>r6ACYv-e*j;?!FB)u literal 0 HcmV?d00001 diff --git a/ZeroLevel.Microservices/packages.config b/ZeroLevel.Microservices/packages.config new file mode 100644 index 0000000..97c22dc --- /dev/null +++ b/ZeroLevel.Microservices/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ZeroLevel.sln b/ZeroLevel.sln new file mode 100644 index 0000000..c2f94eb --- /dev/null +++ b/ZeroLevel.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.421 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel", "ZeroLevel\ZeroLevel.csproj", "{37020D8D-34E8-4EC3-A447-8396D5080457}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.Microservices", "ZeroLevel.Microservices\ZeroLevel.Microservices.csproj", "{6452D91A-2DAC-4982-83AF-77472051E81B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.Discovery", "ZeroLevel.Discovery\ZeroLevel.Discovery.csproj", "{4F55B23F-938C-4DA2-B6DC-B6BC66D36073}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x64.ActiveCfg = Debug|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x64.Build.0 = Debug|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x86.ActiveCfg = Debug|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x86.Build.0 = Debug|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|Any CPU.Build.0 = Release|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x64.ActiveCfg = Release|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x64.Build.0 = Release|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x86.ActiveCfg = Release|Any CPU + {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x86.Build.0 = Release|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x64.ActiveCfg = Debug|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x64.Build.0 = Debug|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x86.ActiveCfg = Debug|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x86.Build.0 = Debug|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|Any CPU.Build.0 = Release|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x64.ActiveCfg = Release|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x64.Build.0 = Release|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x86.ActiveCfg = Release|Any CPU + {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x86.Build.0 = Release|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x64.Build.0 = Debug|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x86.Build.0 = Debug|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|Any CPU.Build.0 = Release|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x64.ActiveCfg = Release|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x64.Build.0 = Release|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x86.ActiveCfg = Release|Any CPU + {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0081B29F-FCF8-45FA-A5DF-732CD3ED2E11} + EndGlobalSection +EndGlobal diff --git a/ZeroLevel/Models/BaseModel.cs b/ZeroLevel/Models/BaseModel.cs new file mode 100644 index 0000000..f2c8c2e --- /dev/null +++ b/ZeroLevel/Models/BaseModel.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.Serialization; + +namespace ZeroLevel.Models +{ + [DataContract] + [Serializable] + public abstract class BaseModel + { + #region Equal + public bool Equals(BaseModel other) + { + if (this == null) // и так бывает + throw new NullReferenceException(); + if (other == null) + return false; + if (ReferenceEquals(this, other)) + return true; + if (this.GetType() != other.GetType()) + return false; + return true; + } + + public override bool Equals(object obj) + { + if (this == null) + throw new NullReferenceException(); + + return Equals(obj as BaseModel); + } + + public static bool operator ==(BaseModel first, BaseModel second) => Equals(first, second); + public static bool operator !=(BaseModel first, BaseModel second) => !Equals(first, second); + #endregion + + public abstract override int GetHashCode(); + public abstract object Clone(); + } +} diff --git a/ZeroLevel/Models/BinaryDocument.cs b/ZeroLevel/Models/BinaryDocument.cs new file mode 100644 index 0000000..ae76f40 --- /dev/null +++ b/ZeroLevel/Models/BinaryDocument.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using ZeroLevel.DocumentObjectModel; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.Models +{ + /// + /// Документ в бинарном представлении + /// + public class BinaryDocument : + IBinarySerializable, + IEquatable, + ICloneable + { + /// + /// Идентификатор + /// + public Guid Id { get; set; } + /// + /// Имя файла + /// + public string FileName { get; set; } + /// + /// Тип содержимого (pdf, doc и т.п.) + /// + public string ContentType { get; set; } + /// + /// Содержимое + /// + public byte[] Document { get; set; } + /// + /// Дата создания + /// + public DateTime Created { get; set; } + /// + /// Опциональные заголовки + /// + public List
Headers { get; set; } + /// + /// Категории + /// + public List Categories { get; set; } + + #region Ctors + public BinaryDocument() + { + Created = DateTime.Now; + Headers = new List
(); + Categories = new List(); + } + + public BinaryDocument(BinaryDocument other) + { + var data = MessageSerializer.Serialize(other); + using (var reader = new MemoryStreamReader(data)) + { + Deserialize(reader); + } + } + #endregion + + #region IBinarySerializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteGuid(this.Id); + writer.WriteString(this.FileName); + writer.WriteString(this.ContentType); + writer.WriteBytes(this.Document); + writer.WriteDateTime(this.Created); + writer.WriteCollection(this.Headers); + writer.WriteCollection(this.Categories); + } + + public void Deserialize(IBinaryReader reader) + { + this.Id = reader.ReadGuid(); + this.FileName = reader.ReadString(); + this.ContentType = reader.ReadString(); + this.Document = reader.ReadBytes(); + this.Created = reader.ReadDateTime() ?? DateTime.Now; + this.Headers = reader.ReadCollection
(); + this.Categories = reader.ReadCollection(); + } + #endregion + + #region Equals & Hash + public override bool Equals(object obj) + { + return this.Equals(obj as BinaryDocument); + } + + public bool Equals(BinaryDocument other) + { + if (this == null) + throw new NullReferenceException(); + if (other == null) return false; + if (ReferenceEquals(this, other)) return true; + if (this.GetType() != other.GetType()) return false; + if (this.Id != other.Id) return false; + if (DateTime.Compare(this.Created, other.Created) != 0) return false; + if (ArrayExtensions.UnsafeEquals(this.Document, other.Document) == false) return false; + if (string.Compare(this.ContentType, other.ContentType) != 0) return false; + if (string.Compare(this.FileName, other.FileName) != 0) return false; + if (this.Headers.NoOrderingEquals(other.Headers) == false) return false; + if (this.Categories.NoOrderingEquals(other.Categories) == false) return false; + return true; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + #endregion + + public object Clone() + { + return new BinaryDocument(this); + } + } +} diff --git a/ZeroLevel/Models/IEntity.cs b/ZeroLevel/Models/IEntity.cs new file mode 100644 index 0000000..2c00619 --- /dev/null +++ b/ZeroLevel/Models/IEntity.cs @@ -0,0 +1,16 @@ +using System; + +namespace ZeroLevel.Models +{ + public interface IEntity + : ICloneable + { + Guid Id { get; } + } + + public interface IEntity + : ICloneable + { + TKey Id { get; } + } +} diff --git a/ZeroLevel/Models/InvokeResult.cs b/ZeroLevel/Models/InvokeResult.cs new file mode 100644 index 0000000..2ec5753 --- /dev/null +++ b/ZeroLevel/Models/InvokeResult.cs @@ -0,0 +1,111 @@ +using System; +using System.Runtime.Serialization; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.Models +{ + /// + /// Результат выполнения действий + /// + [DataContract] + public class InvokeResult : + IBinarySerializable + { + #region Static + private static readonly InvokeResult _successResultWitoutComment = new InvokeResult(true, String.Empty); + #endregion + + #region Ctor + public InvokeResult() + { + } + + public InvokeResult(bool success, string comment) + { + Success = success; + Comment = comment; + } + #endregion + + #region Properties + /// + /// Успех выполнения операции + /// + [DataMember] + public bool Success; + /// + /// Комментарий (сообщение об ошибке при сбое, или доп. информация) + /// + [DataMember] + public string Comment; + #endregion + + #region Fabric methods + /// + /// Сбой при выполнении плана действий + /// + public static InvokeResult Fault(string comment) { return new InvokeResult(false, comment); } + /// + /// Успешное выполнение + /// + public static InvokeResult Succeeding(string comment = "") { return new InvokeResult(true, comment); } + /// + /// Успешное выполнение + /// + public static InvokeResult Succeeding() { return _successResultWitoutComment; } + #endregion + + public virtual void Serialize(IBinaryWriter writer) + { + writer.WriteBoolean(this.Success); + writer.WriteString(this.Comment); + } + + public virtual void Deserialize(IBinaryReader reader) + { + this.Success = reader.ReadBoolean(); + this.Comment = reader.ReadString(); + } + } + + public sealed class InvokeResult : + InvokeResult + { + private T _value; + public T Value { get { return _value; } } + + #region Ctor + public InvokeResult(bool success, string comment) + { + Success = success; + Comment = comment; + } + + public InvokeResult(T value, bool success, string comment) + { + _value = value; + Success = success; + Comment = comment; + } + #endregion + + #region Fabric methods + public static InvokeResult Succeeding(T value, string comment = "") { return new InvokeResult(value, true, comment); } + public static InvokeResult Fault(string comment) { return new InvokeResult(false, comment); } + #endregion + + public override void Serialize(IBinaryWriter writer) + { + writer.WriteBoolean(this.Success); + writer.WriteString(this.Comment); + writer.WriteCompatible(this.Value); + } + + public override void Deserialize(IBinaryReader reader) + { + this.Success = reader.ReadBoolean(); + this.Comment = reader.ReadString(); + this._value = reader.ReadCompatible(); + } + } +} diff --git a/ZeroLevel/Properties/AssemblyInfo.cs b/ZeroLevel/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9d2c8ed --- /dev/null +++ b/ZeroLevel/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +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("ZeroLevel")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ZeroLevel")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[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("658210ea-6b29-4c1b-a13c-3bc7edc2770e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ZeroLevel/Services/Application/BaseWindowsService.cs b/ZeroLevel/Services/Application/BaseWindowsService.cs new file mode 100644 index 0000000..ffbd7dd --- /dev/null +++ b/ZeroLevel/Services/Application/BaseWindowsService.cs @@ -0,0 +1,121 @@ +using System; +using System.ServiceProcess; +using System.Threading; + +namespace ZeroLevel.Services.Applications +{ + public abstract class BaseWindowsService + : ServiceBase, IZeroService + { + public string Name { get; } + + public BaseWindowsService() + { + Name = GetType().Name; + } + + public BaseWindowsService(string name) + { + Name = name; + } + + public ZeroServiceState State => _state; + private ZeroServiceState _state; + + + private ManualResetEvent InteraciveModeWorkingFlag = new ManualResetEvent(false); + + public void InteractiveStart(string[] args) + { + InteraciveModeWorkingFlag.Reset(); + OnStart(args); + while (false == InteraciveModeWorkingFlag.WaitOne(2000)) + { + } + } + + #region IZeroService + public abstract void StartAction(); + public abstract void StopAction(); + public abstract void PauseAction(); + public abstract void ResumeAction(); + #endregion + + #region Windows service + protected override void OnStart(string[] args) + { + if (_state == ZeroServiceState.Initialized) + { + try + { + StartAction(); + _state = ZeroServiceState.Started; + Log.Debug($"[{Name}] Service started"); + } + catch (Exception ex) + { + Log.SystemFatal(ex, $"[{Name}] Failed to start service"); + Stop(); + } + } + } + + protected override void OnPause() + { + if (_state == ZeroServiceState.Started) + { + try + { + PauseAction(); + _state = ZeroServiceState.Paused; + Log.Debug($"[{Name}] Service paused"); + } + catch (Exception ex) + { + Log.SystemFatal(ex, $"[{Name}] Failed to pause service"); + Stop(); + } + } + } + + protected override void OnContinue() + { + if (_state == ZeroServiceState.Paused) + { + try + { + ResumeAction(); + _state = ZeroServiceState.Started; + Log.Debug($"[{Name}] Service continue work after pause"); + } + catch (Exception ex) + { + Log.SystemFatal(ex, $"[{Name}] Failed to continue work service after pause"); + Stop(); + } + } + } + + protected override void OnStop() + { + if (_state != ZeroServiceState.Stopped) + { + _state = ZeroServiceState.Stopped; + try + { + StopAction(); + Log.Debug($"[{Name}] Service stopped"); + } + catch (Exception ex) + { + Log.SystemFatal(ex, $"[{Name}] Failed to stop service"); + } + finally + { + InteraciveModeWorkingFlag?.Set(); + } + } + } + #endregion + } +} diff --git a/ZeroLevel/Services/Application/BasicServiceInstaller.cs b/ZeroLevel/Services/Application/BasicServiceInstaller.cs new file mode 100644 index 0000000..c98a18b --- /dev/null +++ b/ZeroLevel/Services/Application/BasicServiceInstaller.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Configuration.Install; +using System.ServiceProcess; + +namespace ZeroLevel.Services.Applications +{ + internal static class BasicServiceInstaller + { + private class InstallOptions + { + public string ServiceName; + public string ServiceDisplayName; + public string ServiceDescription; + public ServiceStartMode ServiceStartType = ServiceStartMode.Automatic; + public ServiceAccount ServiceAccountType = ServiceAccount.LocalSystem; + public string ServiceUserName; + public string ServiceUserPassword; + } + + private static InstallOptions ReadOptions(IConfiguration configuration) + { + if (configuration == null) + { + configuration = Configuration.Default; + } + var options = new InstallOptions(); + if (configuration.Contains("ServiceDescription")) + { + options.ServiceDescription = configuration.First("ServiceDescription"); + } + if (configuration.Contains("ServiceName")) + { + options.ServiceName = configuration.First("ServiceName"); + } + if (configuration.Contains("ServiceDisplayName")) + { + options.ServiceDisplayName = configuration.First("ServiceDisplayName"); + } + else + { + options.ServiceDisplayName = options.ServiceName; + } + if (configuration.Contains("ServiceUserName")) + { + options.ServiceUserName = configuration.First("ServiceUserName"); + } + if (configuration.Contains("ServiceUserPassword")) + { + options.ServiceUserPassword = configuration.First("ServiceUserPassword"); + } + + if (configuration.Contains("ServiceStartType")) + { + var startType = configuration.First("ServiceStartType"); + ServiceStartMode mode; + if (Enum.TryParse(startType, out mode)) + { + options.ServiceStartType = mode; + } + else + { + options.ServiceStartType = ServiceStartMode.Automatic; + } + } + if (configuration.Contains("ServiceAccountType")) + { + var accountType = configuration.First("ServiceAccountType"); + ServiceAccount type; + if (Enum.TryParse(accountType, out type)) + { + options.ServiceAccountType = type; + } + else + { + options.ServiceAccountType = ServiceAccount.LocalService; + } + } + return options; + } + + public static void Install(IConfiguration configuration) + { + CreateInstaller(ReadOptions(configuration)).Install(new Hashtable()); + } + + public static void Uninstall(IConfiguration configuration) + { + CreateInstaller(ReadOptions(configuration)).Uninstall(null); + } + + private static Installer CreateInstaller(InstallOptions options) + { + var installer = new TransactedInstaller(); + installer.Installers.Add(new ServiceInstaller() + { + ServiceName = options.ServiceName, + DisplayName = options.ServiceDisplayName, + StartType = options.ServiceStartType, + Description = options.ServiceDescription + }); + installer.Installers.Add(new ServiceProcessInstaller + { + Account = options.ServiceAccountType, + Username = (options.ServiceAccountType == ServiceAccount.User) ? options.ServiceUserName : null, + Password = (options.ServiceAccountType == ServiceAccount.User) ? options.ServiceUserPassword : null + }); + var installContext = new InstallContext(options.ServiceName + ".install.log", null); + installContext.Parameters["assemblypath"] = Configuration.AppLocation; + installer.Context = installContext; + return installer; + } + } +} diff --git a/ZeroLevel/Services/Application/BusinessApplication.cs b/ZeroLevel/Services/Application/BusinessApplication.cs new file mode 100644 index 0000000..899fff6 --- /dev/null +++ b/ZeroLevel/Services/Application/BusinessApplication.cs @@ -0,0 +1,161 @@ +using System; +using System.IO; +using System.Reflection; +using System.ServiceProcess; +using ZeroLevel.Services.Applications; + +namespace ZeroLevel +{ + public class Bootstrap + { + static Bootstrap() + { + // Хак, чтобы не переписывать runtime секцию конфига при каждом обновлении Newtonsoft пакета + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + } + + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + try + { + Log.Debug($"[Bootstrap] Resolve assembly '{args.Name}'"); + if (args.Name.StartsWith("Newtonsoft.Json", StringComparison.Ordinal)) + { + return Assembly.LoadFile(Path.Combine(Configuration.BaseDirectory, "Newtonsoft.Json.dll")); + } + else if (args.Name.Equals("Microsoft.Owin", StringComparison.Ordinal)) + { + return Assembly.LoadFile(Path.Combine(Configuration.BaseDirectory, "Microsoft.Owin.dll")); + } + } + catch (Exception ex) + { + Log.Error(ex, $"[Bootstrap] Fault load assembly '{args.Name}'"); + } + return null; + } + + /// + /// Установка приложения в качестве службы + /// + private static void InstallApplication() + { + try + { + Configuration.Save(Configuration.ReadFromApplicationConfig()); + Log.AddTextFileLogger("install.log"); + BasicServiceInstaller.Install(Configuration.Default); + } + catch (Exception ex) + { + Log.SystemFatal(ex, "[Bootstrap] Fault service install"); + } + } + /// + /// Удаление приложения из служб + /// + private static void UninstallApplication() + { + try + { + Configuration.Save(Configuration.ReadFromApplicationConfig()); + Log.AddTextFileLogger("uninstall.log"); + BasicServiceInstaller.Uninstall(Configuration.Default); + } + catch (Exception ex) + { + Log.SystemFatal(ex, "[Bootstrap] Fault service uninstall"); + } + } + + public static void Startup(string[] args, Func preStartConfiguration = null, Func postStartConfiguration = null) + where T : IZeroService, new() + { + var cmd = Configuration.ReadFromCommandLine(args); + if (cmd.Contains("install", "setup")) + { + InstallApplication(); + } + else if (cmd.Contains("uninstall", "remove")) + { + UninstallApplication(); + } + else + { + Configuration.Save(Configuration.ReadFromApplicationConfig()); + Log.CreateLoggingFromConfiguration(Configuration.Default); + IZeroService service = null; + if (preStartConfiguration != null) + { + try + { + if (preStartConfiguration() == false) + { + Log.SystemInfo("[Bootstrap] Service start canceled, because custom preconfig return false"); + return; + } + } + catch (Exception ex) + { + Log.SystemError(ex, "[Bootstrap] Service start canceled, preconfig faulted"); + return; + } + } + try + { + service = new T(); + } + catch (Exception ex) + { + Log.SystemError(ex, "[Bootstrap] Service start canceled, service constructor call fault"); + } + if (postStartConfiguration != null) + { + try + { + if (postStartConfiguration() == false) + { + Log.SystemInfo("[Bootstrap] Service start canceled, because custom postconfig return false"); + return; + } + } + catch (Exception ex) + { + Log.SystemError(ex, "[Bootstrap] Service start canceled, postconfig faulted"); + return; + } + } + // Исключения в процессе работы приложения перехыватываются уровнем ниже + if (Environment.UserInteractive) + { + try + { + Log.Debug("[Bootstrap] The service starting (interactive mode)"); + service?.InteractiveStart(args); + Log.Debug("[Bootstrap] The service stopped (interactive mode)"); + } + catch (Exception ex) + { + Log.SystemFatal(ex, "[Bootstrap] The service start in interactive mode was faulted with error"); + } + } + else + { + try + { + Log.Debug("[Bootstrap] The service starting (windows service)"); + ServiceBase.Run(new ServiceBase[] { service as ServiceBase }); + Log.Debug("[Bootstrap] The service stopped (windows service)"); + } + catch (Exception ex) + { + Log.SystemFatal(ex, "[Bootstrap] The service start was faulted with error"); + } + } + } + try { Sheduller.Dispose(); } catch { } + try { Log.Dispose(); } catch { } + try { Injector.Default.Dispose(); Injector.Dispose(); } catch { } + } + } +} diff --git a/ZeroLevel/Services/Application/IZeroService.cs b/ZeroLevel/Services/Application/IZeroService.cs new file mode 100644 index 0000000..9440799 --- /dev/null +++ b/ZeroLevel/Services/Application/IZeroService.cs @@ -0,0 +1,14 @@ +namespace ZeroLevel.Services.Applications +{ + public interface IZeroService + { + ZeroServiceState State { get; } + + void StartAction(); + void StopAction(); + void PauseAction(); + void ResumeAction(); + + void InteractiveStart(string[] args); + } +} diff --git a/ZeroLevel/Services/Application/ZeroServiceState.cs b/ZeroLevel/Services/Application/ZeroServiceState.cs new file mode 100644 index 0000000..317757c --- /dev/null +++ b/ZeroLevel/Services/Application/ZeroServiceState.cs @@ -0,0 +1,22 @@ +using System; + +namespace ZeroLevel.Services.Applications +{ + [Flags] + public enum ZeroServiceState : int + { + Initialized = 0, + /// + /// Сервис работает + /// + Started = 1, + /// + /// Работа сервиса приостановлена + /// + Paused = 2, + /// + /// Сервис остановлен (ресурсы освобождены) + /// + Stopped = 3 + } +} diff --git a/ZeroLevel/Services/Async/AsyncConditionVariable.cs b/ZeroLevel/Services/Async/AsyncConditionVariable.cs new file mode 100644 index 0000000..074e595 --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncConditionVariable.cs @@ -0,0 +1,184 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// An async-compatible condition variable. This type uses Mesa-style semantics (the notifying tasks do not yield). + /// + [DebuggerDisplay("Id = {Id}, AsyncLockId = {_asyncLock.Id}")] + [DebuggerTypeProxy(typeof(DebugView))] + public sealed class AsyncConditionVariable + { + /// + /// The lock associated with this condition variable. + /// + private readonly AsyncLock _asyncLock; + + /// + /// The queue of waiting tasks. + /// + private readonly IAsyncWaitQueue _queue; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; + + /// + /// Creates an async-compatible condition variable associated with an async-compatible lock. + /// + /// The lock associated with this condition variable. + /// The wait queue used to manage waiters. + public AsyncConditionVariable(AsyncLock asyncLock, IAsyncWaitQueue queue) + { + _asyncLock = asyncLock; + _queue = queue; + _mutex = new object(); + } + + /// + /// Creates an async-compatible condition variable associated with an async-compatible lock. + /// + /// The lock associated with this condition variable. + public AsyncConditionVariable(AsyncLock asyncLock) + : this(asyncLock, new DefaultAsyncWaitQueue()) + { + } + + /// + /// Gets a semi-unique identifier for this asynchronous condition variable. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } + + /// + /// Sends a signal to a single task waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns. + /// + public void Notify() + { + IDisposable finish = null; + lock (_mutex) + { + if (!_queue.IsEmpty) + finish = _queue.Dequeue(); + } + if (finish != null) + finish.Dispose(); + } + + /// + /// Sends a signal to all tasks waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns. + /// + public void NotifyAll() + { + IDisposable finish; + lock (_mutex) + { + finish = _queue.DequeueAll(); + } + finish.Dispose(); + } + + /// + /// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled. + /// + /// The cancellation signal used to cancel this wait. + public Task WaitAsync(CancellationToken cancellationToken) + { + lock (_mutex) + { + // Begin waiting for either a signal or cancellation. + var task = _queue.Enqueue(cancellationToken); + + // Attach to the signal or cancellation. + var retTcs = new TaskCompletionSource(); + task.ContinueWith(async t => + { + // Re-take the lock. + await _asyncLock.LockAsync().ConfigureAwait(false); + + // Propagate the cancellation exception if necessary. + retTcs.TryCompleteFromCompletedTask(t); + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + + var ret = retTcs.Task; + + // Release the lock while we are waiting. + _asyncLock.ReleaseLock(); + + return ret; + } + } + + /// + /// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled. + /// + /// The cancellation signal used to cancel this wait. + public void Wait(CancellationToken cancellationToken) + { + Task enqueuedTask; + lock (_mutex) + { + // Begin waiting for either a signal or cancellation. + enqueuedTask = _queue.Enqueue(cancellationToken); + } + + // Release the lock while we are waiting. + _asyncLock.ReleaseLock(); + + // Wait for the signal or cancellation. + enqueuedTask.WaitWithoutException(); + + // Re-take the lock. + _asyncLock.Lock(); + + // Propagate the cancellation exception if necessary. + enqueuedTask.WaitAndUnwrapException(); + } + + /// + /// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns. + /// + public Task WaitAsync() + { + return WaitAsync(CancellationToken.None); + } + + /// + /// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns. + /// + public void Wait() + { + Wait(CancellationToken.None); + } + + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncConditionVariable _cv; + + public DebugView(AsyncConditionVariable cv) + { + _cv = cv; + } + + public int Id { get { return _cv.Id; } } + + public AsyncLock AsyncLock { get { return _cv._asyncLock; } } + + public IAsyncWaitQueue WaitQueue { get { return _cv._queue; } } + } + // ReSharper restore UnusedMember.Local + } +} diff --git a/ZeroLevel/Services/Async/AsyncHelper.cs b/ZeroLevel/Services/Async/AsyncHelper.cs new file mode 100644 index 0000000..90e25ee --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + public static class AsyncHelper + { + private static readonly TaskFactory _taskFactory = new + TaskFactory(CancellationToken.None, + TaskCreationOptions.None, + TaskContinuationOptions.None, + TaskScheduler.Default); + + public static TResult RunSync(Func> func) + => _taskFactory + .StartNew(func) + .Unwrap() + .GetAwaiter() + .GetResult(); + + public static void RunSync(Func func) + => _taskFactory + .StartNew(func) + .Unwrap() + .GetAwaiter() + .GetResult(); + + public async static Task WithTimeout(this Task task, int duration) + { + var retTask = await Task.WhenAny(task, Task.Delay(duration)) + .ConfigureAwait(false); + + if (retTask is Task) return task.Result; + return default(T); + } + } +} diff --git a/ZeroLevel/Services/Async/AsyncLock.cs b/ZeroLevel/Services/Async/AsyncLock.cs new file mode 100644 index 0000000..f2cb7ae --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncLock.cs @@ -0,0 +1,191 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// A mutual exclusion lock that is compatible with async. Note that this lock is not recursive! + /// + [DebuggerDisplay("Id = {Id}, Taken = {_taken}")] + [DebuggerTypeProxy(typeof(DebugView))] + public sealed class AsyncLock + { + /// + /// Whether the lock is taken by a task. + /// + private bool _taken; + + /// + /// The queue of TCSs that other tasks are awaiting to acquire the lock. + /// + private readonly IAsyncWaitQueue _queue; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; + + /// + /// Creates a new async-compatible mutual exclusion lock. + /// + public AsyncLock() + : this(new DefaultAsyncWaitQueue()) + { + } + + /// + /// Creates a new async-compatible mutual exclusion lock using the specified wait queue. + /// + /// The wait queue used to manage waiters. + public AsyncLock(IAsyncWaitQueue queue) + { + _queue = queue; + _mutex = new object(); + } + + /// + /// Gets a semi-unique identifier for this asynchronous lock. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } + + /// + /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public Task LockAsync(CancellationToken cancellationToken) + { + Task ret; + lock (_mutex) + { + if (!_taken) + { + // If the lock is available, take it immediately. + _taken = true; + ret = TaskShim.FromResult(new Key(this)); + } + else + { + // Wait for the lock to become available or cancellation. + ret = _queue.Enqueue(cancellationToken); + } + } + return ret; + } + + /// + /// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + public IDisposable Lock(CancellationToken cancellationToken) + { + Task enqueuedTask; + lock (_mutex) + { + if (!_taken) + { + _taken = true; + return new Key(this); + } + + enqueuedTask = _queue.Enqueue(cancellationToken); + } + + return enqueuedTask.WaitAndUnwrapException(); + } + + /// + /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. + /// + /// A disposable that releases the lock when disposed. + public Task LockAsync() + { + return LockAsync(CancellationToken.None); + } + + /// + /// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + public IDisposable Lock() + { + return Lock(CancellationToken.None); + } + + /// + /// Releases the lock. + /// + internal void ReleaseLock() + { + IDisposable finish = null; + lock (_mutex) + { + if (_queue.IsEmpty) + _taken = false; + else + finish = _queue.Dequeue(new Key(this)); + } + if (finish != null) + finish.Dispose(); + } + + /// + /// The disposable which releases the lock. + /// + private sealed class Key : IDisposable + { + /// + /// The lock to release. + /// + private AsyncLock _asyncLock; + + /// + /// Creates the key for a lock. + /// + /// The lock to release. May not be null. + public Key(AsyncLock asyncLock) + { + _asyncLock = asyncLock; + } + + /// + /// Release the lock. + /// + public void Dispose() + { + if (_asyncLock == null) + return; + _asyncLock.ReleaseLock(); + _asyncLock = null; + } + } + + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncLock _mutex; + + public DebugView(AsyncLock mutex) + { + _mutex = mutex; + } + + public int Id { get { return _mutex.Id; } } + + public bool Taken { get { return _mutex._taken; } } + + public IAsyncWaitQueue WaitQueue { get { return _mutex._queue; } } + } + // ReSharper restore UnusedMember.Local + } +} diff --git a/ZeroLevel/Services/Async/AsyncManualResetEvent.cs b/ZeroLevel/Services/Async/AsyncManualResetEvent.cs new file mode 100644 index 0000000..b7a488d --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncManualResetEvent.cs @@ -0,0 +1,159 @@ +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// An async-compatible manual-reset event. + /// + [DebuggerDisplay("Id = {Id}, IsSet = {GetStateForDebugger}")] + [DebuggerTypeProxy(typeof(DebugView))] + public sealed class AsyncManualResetEvent + { + /// + /// The object used for synchronization. + /// + private readonly object _mutex; + + /// + /// The current state of the event. + /// + private TaskCompletionSource _tcs; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + [DebuggerNonUserCode] + private bool GetStateForDebugger + { + get + { + return _tcs.Task.IsCompleted; + } + } + + /// + /// Creates an async-compatible manual-reset event. + /// + /// Whether the manual-reset event is initially set or unset. + public AsyncManualResetEvent(bool set) + { + _mutex = new object(); + _tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); + if (set) + _tcs.TrySetResult(null); + } + + /// + /// Creates an async-compatible manual-reset event that is initially unset. + /// + public AsyncManualResetEvent() + : this(false) + { + } + + /// + /// Gets a semi-unique identifier for this asynchronous manual-reset event. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } + + /// + /// Whether this event is currently set. This member is seldom used; code using this member has a high possibility of race conditions. + /// + public bool IsSet + { + get { lock (_mutex) return _tcs.Task.IsCompleted; } + } + + /// + /// Asynchronously waits for this event to be set. + /// + public Task WaitAsync() + { + lock (_mutex) + { + return _tcs.Task; + } + } + + /// + /// Asynchronously waits for this event to be set or for the wait to be canceled. + /// + /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. + public Task WaitAsync(CancellationToken cancellationToken) + { + var waitTask = WaitAsync(); + if (waitTask.IsCompleted) + return waitTask; + return waitTask.WaitAsync(cancellationToken); + } + + /// + /// Synchronously waits for this event to be set. This method may block the calling thread. + /// + public void Wait() + { + WaitAsync().WaitAndUnwrapException(); + } + + /// + /// Synchronously waits for this event to be set. This method may block the calling thread. + /// + /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. + public void Wait(CancellationToken cancellationToken) + { + var ret = WaitAsync(); + if (ret.IsCompleted) + return; + ret.WaitAndUnwrapException(cancellationToken); + } + + /// + /// Sets the event, atomically completing every task returned by . If the event is already set, this method does nothing. + /// + public void Set() + { + lock (_mutex) + { + _tcs.TrySetResult(null); + } + } + + /// + /// Resets the event. If the event is already reset, this method does nothing. + /// + public void Reset() + { + lock (_mutex) + { + if (_tcs.Task.IsCompleted) + _tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); + } + } + + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncManualResetEvent _mre; + + public DebugView(AsyncManualResetEvent mre) + { + _mre = mre; + } + + public int Id { get { return _mre.Id; } } + + public bool IsSet { get { return _mre.GetStateForDebugger; } } + + public Task CurrentTask { get { return _mre._tcs.Task; } } + } + // ReSharper restore UnusedMember.Local + } +} diff --git a/ZeroLevel/Services/Async/AsyncMonitor.cs b/ZeroLevel/Services/Async/AsyncMonitor.cs new file mode 100644 index 0000000..84e0216 --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncMonitor.cs @@ -0,0 +1,135 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// An async-compatible monitor. + /// + [DebuggerDisplay("Id = {Id}, ConditionVariableId = {_conditionVariable.Id}")] + public sealed class AsyncMonitor + { + /// + /// The lock. + /// + private readonly AsyncLock _asyncLock; + + /// + /// The condition variable. + /// + private readonly AsyncConditionVariable _conditionVariable; + + /// + /// Constructs a new monitor. + /// + public AsyncMonitor(IAsyncWaitQueue lockQueue, IAsyncWaitQueue conditionVariableQueue) + { + _asyncLock = new AsyncLock(lockQueue); + _conditionVariable = new AsyncConditionVariable(_asyncLock, conditionVariableQueue); + } + + /// + /// Constructs a new monitor. + /// + public AsyncMonitor() + : this(new DefaultAsyncWaitQueue(), new DefaultAsyncWaitQueue()) + { + } + + /// + /// Gets a semi-unique identifier for this monitor. + /// + public int Id + { + get { return _asyncLock.Id; } + } + + /// + /// Asynchronously enters the monitor. Returns a disposable that leaves the monitor when disposed. + /// + /// The cancellation token used to cancel the enter. If this is already set, then this method will attempt to enter the monitor immediately (succeeding if the monitor is currently available). + /// A disposable that leaves the monitor when disposed. + public Task EnterAsync(CancellationToken cancellationToken) + { + return _asyncLock.LockAsync(cancellationToken); + } + + /// + /// Synchronously enters the monitor. Returns a disposable that leaves the monitor when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the enter. If this is already set, then this method will attempt to enter the monitor immediately (succeeding if the monitor is currently available). + public IDisposable Enter(CancellationToken cancellationToken) + { + return _asyncLock.Lock(cancellationToken); + } + + /// + /// Asynchronously enters the monitor. Returns a disposable that leaves the monitor when disposed. + /// + /// A disposable that leaves the monitor when disposed. + public Task EnterAsync() + { + return EnterAsync(CancellationToken.None); + } + + /// + /// Asynchronously enters the monitor. Returns a disposable that leaves the monitor when disposed. This method may block the calling thread. + /// + public IDisposable Enter() + { + return Enter(CancellationToken.None); + } + + /// + /// Asynchronously waits for a pulse signal on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns, even if the method is cancelled. This method internally will leave the monitor while waiting for a notification. + /// + /// The cancellation signal used to cancel this wait. + public Task WaitAsync(CancellationToken cancellationToken) + { + return _conditionVariable.WaitAsync(cancellationToken); + } + + /// + /// Asynchronously waits for a pulse signal on this monitor. This method may block the calling thread. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns, even if the method is cancelled. This method internally will leave the monitor while waiting for a notification. + /// + /// The cancellation signal used to cancel this wait. + public void Wait(CancellationToken cancellationToken) + { + _conditionVariable.Wait(cancellationToken); + } + + /// + /// Asynchronously waits for a pulse signal on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns. This method internally will leave the monitor while waiting for a notification. + /// + public Task WaitAsync() + { + return WaitAsync(CancellationToken.None); + } + + /// + /// Asynchronously waits for a pulse signal on this monitor. This method may block the calling thread. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns. This method internally will leave the monitor while waiting for a notification. + /// + public void Wait() + { + Wait(CancellationToken.None); + } + + /// + /// Sends a signal to a single task waiting on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns. + /// + public void Pulse() + { + _conditionVariable.Notify(); + } + + /// + /// Sends a signal to all tasks waiting on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns. + /// + public void PulseAll() + { + _conditionVariable.NotifyAll(); + } + } +} diff --git a/ZeroLevel/Services/Async/AsyncProducerConsumerQueue.cs b/ZeroLevel/Services/Async/AsyncProducerConsumerQueue.cs new file mode 100644 index 0000000..8d42231 --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncProducerConsumerQueue.cs @@ -0,0 +1,737 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// An async-compatible producer/consumer queue. + /// + /// The type of elements contained in the queue. + [DebuggerDisplay("Count = {_queue.Count}, MaxCount = {_maxCount}")] + [DebuggerTypeProxy(typeof(AsyncProducerConsumerQueue<>.DebugView))] + public sealed class AsyncProducerConsumerQueue : IDisposable + { + /// + /// The underlying queue. + /// + private readonly Queue _queue; + + /// + /// The maximum number of elements allowed in the queue. + /// + private readonly int _maxCount; + + /// + /// The mutual-exclusion lock protecting the queue. + /// + private readonly AsyncLock _mutex; + + /// + /// A condition variable that is signalled when the queue is not full. + /// + private readonly AsyncConditionVariable _notFull; + + /// + /// A condition variable that is signalled when the queue is completed or not empty. + /// + private readonly AsyncConditionVariable _completedOrNotEmpty; + + /// + /// A cancellation token source that is canceled when the queue is marked completed for adding. + /// + private readonly CancellationTokenSource _completed; + + /// + /// A cached result that is common when calling . + /// + internal static readonly DequeueResult FalseResult = new DequeueResult(null, default(T)); + + /// + /// Creates a new async-compatible producer/consumer queue with the specified initial elements and a maximum element count. + /// + /// The initial elements to place in the queue. + /// The maximum element count. This must be greater than zero. + public AsyncProducerConsumerQueue(IEnumerable collection, int maxCount) + { + if (maxCount <= 0) + throw new ArgumentOutOfRangeException("maxCount", "The maximum count must be greater than zero."); + _queue = collection == null ? new Queue() : new Queue(collection); + if (maxCount < _queue.Count) + throw new ArgumentException("The maximum count cannot be less than the number of elements in the collection.", "maxCount"); + _maxCount = maxCount; + + _mutex = new AsyncLock(); + _notFull = new AsyncConditionVariable(_mutex); + _completedOrNotEmpty = new AsyncConditionVariable(_mutex); + _completed = new CancellationTokenSource(); + } + + /// + /// Creates a new async-compatible producer/consumer queue with the specified initial elements. + /// + /// The initial elements to place in the queue. + public AsyncProducerConsumerQueue(IEnumerable collection) + : this(collection, int.MaxValue) + { + } + + /// + /// Creates a new async-compatible producer/consumer queue with a maximum element count. + /// + /// The maximum element count. This must be greater than zero. + public AsyncProducerConsumerQueue(int maxCount) + : this(null, maxCount) + { + } + + /// + /// Creates a new async-compatible producer/consumer queue. + /// + public AsyncProducerConsumerQueue() + : this(null, int.MaxValue) + { + } + + /// + /// Whether the queue is empty. + /// + private bool Empty { get { return _queue.Count == 0; } } + + /// + /// Whether the queue is full. + /// + private bool Full { get { return _queue.Count == _maxCount; } } + + /// + /// Releases resources held by this instance. After disposal, any use of this instance is undefined. + /// + public void Dispose() + { + _completed.Dispose(); + } + + /// + /// Asynchronously marks the producer/consumer queue as complete for adding. + /// + [Obsolete("Use CompleteAdding() instead.")] + public async Task CompleteAddingAsync() + { + using (await _mutex.LockAsync().ConfigureAwait(false)) + { + if (_completed.IsCancellationRequested) + return; + _completed.Cancel(); + _completedOrNotEmpty.NotifyAll(); + } + } + + /// + /// Synchronously marks the producer/consumer queue as complete for adding. + /// + public void CompleteAdding() + { + using (_mutex.Lock()) + { + if (_completed.IsCancellationRequested) + return; + _completed.Cancel(); + _completedOrNotEmpty.NotifyAll(); + } + } + + /// + /// Attempts to enqueue an item. + /// + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. If is not null, then this token must include signals from the object. + /// A synchronization object used to cancel related enqueue operations. May be null if this is the only enqueue operation. + internal async Task> TryEnqueueAsync(T item, CancellationToken cancellationToken, TaskCompletionSource abort) + { + try + { + using (var combinedToken = CancellationTokenHelpers.Normalize(_completed.Token, cancellationToken)) + using (await _mutex.LockAsync().ConfigureAwait(false)) + { + // Wait for the queue to be not full. + while (Full) + await _notFull.WaitAsync(combinedToken.Token).ConfigureAwait(false); + + // Explicitly check whether the queue has been marked complete to prevent a race condition where notFull is signalled at the same time the queue is marked complete. + if (_completed.IsCancellationRequested) + return null; + + // Set the abort signal. If another queue has already set the abort signal, then abort. + if (abort != null && !abort.TrySetCanceled()) + return null; + + _queue.Enqueue(item); + _completedOrNotEmpty.Notify(); + return this; + } + } + catch (OperationCanceledException) + { + return null; + } + } + + /// + /// Attempts to enqueue an item. This method may block the calling thread. + /// + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + internal AsyncProducerConsumerQueue DoTryEnqueue(T item, CancellationToken cancellationToken) + { + try + { + using (var combinedToken = CancellationTokenHelpers.Normalize(_completed.Token, cancellationToken)) + using (_mutex.Lock()) + { + // Wait for the queue to be not full. + while (Full) + _notFull.Wait(combinedToken.Token); + + // Explicitly check whether the queue has been marked complete to prevent a race condition where notFull is signalled at the same time the queue is marked complete. + if (_completed.IsCancellationRequested) + return null; + + _queue.Enqueue(item); + _completedOrNotEmpty.Notify(); + return this; + } + } + catch (OperationCanceledException) + { + return null; + } + } + + /// + /// Attempts to enqueue an item to the producer/consumer queue. Returns false if the producer/consumer queue has completed adding. + /// + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + public async Task TryEnqueueAsync(T item, CancellationToken cancellationToken) + { + var ret = await TryEnqueueAsync(item, cancellationToken, null).ConfigureAwait(false); + if (ret != null) + return true; + cancellationToken.ThrowIfCancellationRequested(); + return false; + } + + /// + /// Attempts to enqueue an item to the producer/consumer queue. Returns false if the producer/consumer queue has completed adding. This method may block the calling thread. + /// + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + public bool TryEnqueue(T item, CancellationToken cancellationToken) + { + var ret = DoTryEnqueue(item, cancellationToken); + if (ret != null) + return true; + cancellationToken.ThrowIfCancellationRequested(); + return false; + } + + /// + /// Attempts to enqueue an item to the producer/consumer queue. Returns false if the producer/consumer queue has completed adding. + /// + /// The item to enqueue. + public Task TryEnqueueAsync(T item) + { + return TryEnqueueAsync(item, CancellationToken.None); + } + + /// + /// Attempts to enqueue an item to the producer/consumer queue. Returns false if the producer/consumer queue has completed adding. This method may block the calling thread. + /// + /// The item to enqueue. + public bool TryEnqueue(T item) + { + return TryEnqueue(item, CancellationToken.None); + } + + /// + /// Enqueues an item to the producer/consumer queue. Throws if the producer/consumer queue has completed adding. + /// + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + public async Task EnqueueAsync(T item, CancellationToken cancellationToken) + { + var result = await TryEnqueueAsync(item, cancellationToken).ConfigureAwait(false); + if (!result) + throw new InvalidOperationException("Enqueue failed; the producer/consumer queue has completed adding."); + } + + /// + /// Enqueues an item to the producer/consumer queue. Throws if the producer/consumer queue has completed adding. This method may block the calling thread. + /// + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + public void Enqueue(T item, CancellationToken cancellationToken) + { + var result = TryEnqueue(item, cancellationToken); + if (!result) + throw new InvalidOperationException("Enqueue failed; the producer/consumer queue has completed adding."); + } + + /// + /// Enqueues an item to the producer/consumer queue. Throws if the producer/consumer queue has completed adding. + /// + /// The item to enqueue. + public Task EnqueueAsync(T item) + { + return EnqueueAsync(item, CancellationToken.None); + } + + /// + /// Enqueues an item to the producer/consumer queue. This method may block the calling thread. Throws if the producer/consumer queue has completed adding. + /// + /// The item to enqueue. + public void Enqueue(T item) + { + Enqueue(item, CancellationToken.None); + } + + /// + /// Asynchronously waits until an item is available to dequeue. Returns false if the producer/consumer queue has completed adding and there are no more items. + /// + /// A cancellation token that can be used to abort the asynchronous wait. + public async Task OutputAvailableAsync(CancellationToken cancellationToken) + { + using (await _mutex.LockAsync().ConfigureAwait(false)) + { + while (!_completed.IsCancellationRequested && Empty) + await _completedOrNotEmpty.WaitAsync(cancellationToken).ConfigureAwait(false); + return !Empty; + } + } + + /// + /// Asynchronously waits until an item is available to dequeue. Returns false if the producer/consumer queue has completed adding and there are no more items. + /// + public Task OutputAvailableAsync() + { + return OutputAvailableAsync(CancellationToken.None); + } + + /// + /// Provides a (synchronous) consuming enumerable for items in the producer/consumer queue. + /// + /// A cancellation token that can be used to abort the synchronous enumeration. + public IEnumerable GetConsumingEnumerable(CancellationToken cancellationToken) + { + while (true) + { + var result = DoTryDequeue(cancellationToken); + if (!result.Success) + yield break; + yield return result.Item; + } + } + + /// + /// Provides a (synchronous) consuming enumerable for items in the producer/consumer queue. + /// + public IEnumerable GetConsumingEnumerable() + { + return GetConsumingEnumerable(CancellationToken.None); + } + + /// + /// Attempts to dequeue an item. + /// + /// A cancellation token that can be used to abort the dequeue operation. If is not null, then this token must include signals from the object. + /// A synchronization object used to cancel related dequeue operations. May be null if this is the only dequeue operation. + internal async Task TryDequeueAsync(CancellationToken cancellationToken, TaskCompletionSource abort) + { + try + { + using (await _mutex.LockAsync().ConfigureAwait(false)) + { + while (!_completed.IsCancellationRequested && Empty) + await _completedOrNotEmpty.WaitAsync(cancellationToken).ConfigureAwait(false); + if (_completed.IsCancellationRequested && Empty) + return FalseResult; + if (abort != null && !abort.TrySetCanceled()) + return FalseResult; + var item = _queue.Dequeue(); + _notFull.Notify(); + return new DequeueResult(this, item); + } + } + catch (OperationCanceledException) + { + return FalseResult; + } + } + + /// + /// Attempts to dequeue an item. This method may block the calling thread. + /// + /// A cancellation token that can be used to abort the dequeue operation. + internal DequeueResult DoTryDequeue(CancellationToken cancellationToken) + { + try + { + using (_mutex.Lock()) + { + while (!_completed.IsCancellationRequested && Empty) + _completedOrNotEmpty.Wait(cancellationToken); + if (_completed.IsCancellationRequested && Empty) + return FalseResult; + var item = _queue.Dequeue(); + _notFull.Notify(); + return new DequeueResult(this, item); + } + } + catch (OperationCanceledException) + { + return FalseResult; + } + } + + /// + /// Attempts to dequeue an item from the producer/consumer queue. + /// + /// A cancellation token that can be used to abort the dequeue operation. + public async Task TryDequeueAsync(CancellationToken cancellationToken) + { + var ret = await TryDequeueAsync(cancellationToken, null).ConfigureAwait(false); + if (ret.Success) + return ret; + cancellationToken.ThrowIfCancellationRequested(); + return ret; + } + + /// + /// Attempts to dequeue an item from the producer/consumer queue. This method may block the calling thread. + /// + /// A cancellation token that can be used to abort the dequeue operation. + public DequeueResult TryDequeue(CancellationToken cancellationToken) + { + var ret = DoTryDequeue(cancellationToken); + if (ret.Success) + return ret; + cancellationToken.ThrowIfCancellationRequested(); + return ret; + } + + /// + /// Attempts to dequeue an item from the producer/consumer queue. + /// + public Task TryDequeueAsync() + { + return TryDequeueAsync(CancellationToken.None); + } + + /// + /// Attempts to dequeue an item from the producer/consumer queue. This method may block the calling thread. + /// + public DequeueResult TryDequeue() + { + return TryDequeue(CancellationToken.None); + } + + /// + /// Dequeues an item from the producer/consumer queue. Returns the dequeued item. Throws if the producer/consumer queue has completed adding and is empty. + /// + /// A cancellation token that can be used to abort the dequeue operation. + /// The dequeued item. + public async Task DequeueAsync(CancellationToken cancellationToken) + { + var ret = await TryDequeueAsync(cancellationToken).ConfigureAwait(false); + if (!ret.Success) + throw new InvalidOperationException("Dequeue failed; the producer/consumer queue has completed adding and is empty."); + return ret.Item; + } + + /// + /// Dequeues an item from the producer/consumer queue. Returns the dequeued item. This method may block the calling thread. Throws if the producer/consumer queue has completed adding and is empty. + /// + /// A cancellation token that can be used to abort the dequeue operation. + public T Dequeue(CancellationToken cancellationToken) + { + var ret = TryDequeue(cancellationToken); + if (!ret.Success) + throw new InvalidOperationException("Dequeue failed; the producer/consumer queue has completed adding and is empty."); + return ret.Item; + } + + /// + /// Dequeues an item from the producer/consumer queue. Returns the dequeued item. Throws if the producer/consumer queue has completed adding and is empty. + /// + /// The dequeued item. + public Task DequeueAsync() + { + return DequeueAsync(CancellationToken.None); + } + + /// + /// Dequeues an item from the producer/consumer queue. Returns the dequeued item. This method may block the calling thread. Throws if the producer/consumer queue has completed adding and is empty. + /// + /// The dequeued item. + public T Dequeue() + { + return Dequeue(CancellationToken.None); + } + + /// + /// The result of a TryDequeue, DequeueFromAny, or TryDequeueFromAny operation. + /// + public sealed class DequeueResult + { + internal DequeueResult(AsyncProducerConsumerQueue queue, T item) + { + Queue = queue; + Item = item; + } + + /// + /// The queue from which the item was dequeued, or null if the operation failed. + /// + public AsyncProducerConsumerQueue Queue { get; private set; } + + /// + /// Whether the operation was successful. This is true if and only if is not null. + /// + public bool Success { get { return Queue != null; } } + + /// + /// The dequeued item. This is only valid if is not null. + /// + public T Item { get; private set; } + } + + [DebuggerNonUserCode] + internal sealed class DebugView + { + private readonly AsyncProducerConsumerQueue _queue; + + public DebugView(AsyncProducerConsumerQueue queue) + { + _queue = queue; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get { return _queue._queue.ToArray(); } + } + } + } + + /// + /// Provides methods for working on multiple instances. + /// + public static class AsyncProducerConsumerQueueExtensions + { + /// + /// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns null if all producer/consumer queues have completed adding. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + /// The producer/consumer queue that received the item. + public static async Task> TryEnqueueToAnyAsync(this IEnumerable> queues, T item, CancellationToken cancellationToken) + { + var abort = new TaskCompletionSource(); + using (var abortCancellationToken = CancellationTokenHelpers.FromTask(abort.Task)) + using (var combinedToken = CancellationTokenHelpers.Normalize(abortCancellationToken.Token, cancellationToken)) + { + var token = combinedToken.Token; + var tasks = queues.Select(q => q.TryEnqueueAsync(item, token, abort)); + var results = await TaskShim.WhenAll(tasks).ConfigureAwait(false); + var ret = results.FirstOrDefault(x => x != null); + if (ret == null) + cancellationToken.ThrowIfCancellationRequested(); + return ret; + } + } + + /// + /// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns null if all producer/consumer queues have completed adding. This method may block the calling thread. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + /// The producer/consumer queue that received the item. + public static AsyncProducerConsumerQueue TryEnqueueToAny(this IEnumerable> queues, T item, CancellationToken cancellationToken) + { + return TryEnqueueToAnyAsync(queues, item, cancellationToken).WaitAndUnwrapException(); + } + + /// + /// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns null if all producer/consumer queues have completed adding. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// The producer/consumer queue that received the item. + public static Task> TryEnqueueToAnyAsync(this IEnumerable> queues, T item) + { + return TryEnqueueToAnyAsync(queues, item, CancellationToken.None); + } + + /// + /// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns null if all producer/consumer queues have completed adding. This method may block the calling thread. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// The producer/consumer queue that received the item. + public static AsyncProducerConsumerQueue TryEnqueueToAny(this IEnumerable> queues, T item) + { + return TryEnqueueToAny(queues, item, CancellationToken.None); + } + + /// + /// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws if all producer/consumer queues have completed adding. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + /// The producer/consumer queue that received the item. + public static async Task> EnqueueToAnyAsync(this IEnumerable> queues, T item, CancellationToken cancellationToken) + { + var ret = await TryEnqueueToAnyAsync(queues, item, cancellationToken).ConfigureAwait(false); + if (ret == null) + throw new InvalidOperationException("Enqueue failed; all producer/consumer queues have completed adding."); + return ret; + } + + /// + /// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws if all producer/consumer queues have completed adding. This method may block the calling thread. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// A cancellation token that can be used to abort the enqueue operation. + /// The producer/consumer queue that received the item. + public static AsyncProducerConsumerQueue EnqueueToAny(this IEnumerable> queues, T item, CancellationToken cancellationToken) + { + var ret = TryEnqueueToAny(queues, item, cancellationToken); + if (ret == null) + throw new InvalidOperationException("Enqueue failed; all producer/consumer queues have completed adding."); + return ret; + } + + /// + /// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws if all producer/consumer queues have completed adding. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// The producer/consumer queue that received the item. + public static Task> EnqueueToAnyAsync(this IEnumerable> queues, T item) + { + return EnqueueToAnyAsync(queues, item, CancellationToken.None); + } + + /// + /// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws if all producer/consumer queues have completed adding. This method may block the calling thread. + /// + /// The producer/consumer queues. + /// The item to enqueue. + /// The producer/consumer queue that received the item. + public static AsyncProducerConsumerQueue EnqueueToAny(this IEnumerable> queues, T item) + { + return EnqueueToAny(queues, item, CancellationToken.None); + } + + /// + /// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty. + /// + /// The producer/consumer queues. + /// A cancellation token that can be used to abort the dequeue operation. + public static async Task.DequeueResult> TryDequeueFromAnyAsync(this IEnumerable> queues, CancellationToken cancellationToken) + { + var abort = new TaskCompletionSource(); + using (var abortCancellationToken = CancellationTokenHelpers.FromTask(abort.Task)) + using (var combinedToken = CancellationTokenHelpers.Normalize(abortCancellationToken.Token, cancellationToken)) + { + var token = combinedToken.Token; + var tasks = queues.Select(q => q.TryDequeueAsync(token, abort)); + var results = await TaskShim.WhenAll(tasks).ConfigureAwait(false); + var result = results.FirstOrDefault(x => x.Success); + if (result != null) + return result; + cancellationToken.ThrowIfCancellationRequested(); + return AsyncProducerConsumerQueue.FalseResult; + } + } + + /// + /// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread. + /// + /// The producer/consumer queues. + /// A cancellation token that can be used to abort the dequeue operation. + public static AsyncProducerConsumerQueue.DequeueResult TryDequeueFromAny(this IEnumerable> queues, CancellationToken cancellationToken) + { + return TryDequeueFromAnyAsync(queues, cancellationToken).WaitAndUnwrapException(); + } + + /// + /// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty. + /// + /// The producer/consumer queues. + public static Task.DequeueResult> TryDequeueFromAnyAsync(this IEnumerable> queues) + { + return TryDequeueFromAnyAsync(queues, CancellationToken.None); + } + + /// + /// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread. + /// + /// The producer/consumer queues. + public static AsyncProducerConsumerQueue.DequeueResult TryDequeueFromAny(this IEnumerable> queues) + { + return TryDequeueFromAny(queues, CancellationToken.None); + } + + /// + /// Dequeues an item from any of a number of producer/consumer queues. Throws if all the producer/consumer queues have completed adding and are empty. + /// + /// The producer/consumer queues. + /// A cancellation token that can be used to abort the dequeue operation. + public static async Task.DequeueResult> DequeueFromAnyAsync(this IEnumerable> queues, CancellationToken cancellationToken) + { + var ret = await TryDequeueFromAnyAsync(queues, cancellationToken).ConfigureAwait(false); + if (!ret.Success) + throw new InvalidOperationException("Dequeue failed; all producer/consumer queues have completed adding and are empty."); + return ret; + } + + /// + /// Dequeues an item from any of a number of producer/consumer queues. Throws if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread. + /// + /// The producer/consumer queues. + /// A cancellation token that can be used to abort the dequeue operation. + public static AsyncProducerConsumerQueue.DequeueResult DequeueFromAny(this IEnumerable> queues, CancellationToken cancellationToken) + { + var ret = TryDequeueFromAny(queues, cancellationToken); + if (!ret.Success) + throw new InvalidOperationException("Dequeue failed; all producer/consumer queues have completed adding and are empty."); + return ret; + } + + /// + /// Dequeues an item from any of a number of producer/consumer queues. Throws if all the producer/consumer queues have completed adding and are empty. + /// + /// The producer/consumer queues. + public static Task.DequeueResult> DequeueFromAnyAsync(this IEnumerable> queues) + { + return DequeueFromAnyAsync(queues, CancellationToken.None); + } + + /// + /// Dequeues an item from any of a number of producer/consumer queues. Throws if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread. + /// + /// The producer/consumer queues. + public static AsyncProducerConsumerQueue.DequeueResult DequeueFromAny(this IEnumerable> queues) + { + return DequeueFromAny(queues, CancellationToken.None); + } + } +} diff --git a/ZeroLevel/Services/Async/AsyncReaderWriterLock.cs b/ZeroLevel/Services/Async/AsyncReaderWriterLock.cs new file mode 100644 index 0000000..d22cc1f --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncReaderWriterLock.cs @@ -0,0 +1,769 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// A reader/writer lock that is compatible with async. Note that this lock is not recursive! + /// + [DebuggerDisplay("Id = {Id}, State = {GetStateForDebugger}, ReaderCount = {GetReaderCountForDebugger}, UpgradeInProgress = {GetUpgradeInProgressForDebugger}")] + [DebuggerTypeProxy(typeof(DebugView))] + public sealed class AsyncReaderWriterLock + { + /// + /// The queue of TCSs that other tasks are awaiting to acquire the lock as writers. + /// + private readonly IAsyncWaitQueue _writerQueue; + + /// + /// The queue of TCSs that other tasks are awaiting to acquire the lock as readers. + /// + private readonly IAsyncWaitQueue _readerQueue; + + /// + /// The queue of TCSs that other tasks are awaiting to acquire the lock as upgradeable readers. + /// + private readonly IAsyncWaitQueue _upgradeableReaderQueue; + + /// + /// The queue of TCSs that other tasks are awaiting to upgrade a reader lock to a writer lock. + /// + private readonly IAsyncWaitQueue _upgradeReaderQueue; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + /// + /// The current upgradeable reader lock key, if any. If this is not null, then there is an upgradeable reader lock held. + /// + private UpgradeableReaderKey _upgradeableReaderKey; + + /// + /// Number of reader locks held (including an upgradeable reader lock, if applicable); -1 if a writer lock is held; 0 if no locks are held. + /// + private int _locksHeld; + + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; + + [DebuggerNonUserCode] + internal State GetStateForDebugger + { + get + { + if (_locksHeld == 0) + return State.Unlocked; + if (_locksHeld == -1) + if (_upgradeableReaderKey != null) + return State.WriteLockedWithUpgradeableReader; + else + return State.WriteLocked; + if (_upgradeableReaderKey != null) + return State.ReadLockedWithUpgradeableReader; + return State.ReadLocked; + } + } + + internal enum State + { + Unlocked, + ReadLocked, + ReadLockedWithUpgradeableReader, + WriteLocked, + WriteLockedWithUpgradeableReader, + } + + [DebuggerNonUserCode] + internal int GetReaderCountForDebugger { get { return (_locksHeld > 0 ? _locksHeld : 0); } } + [DebuggerNonUserCode] + internal bool GetUpgradeInProgressForDebugger { get { return !_upgradeReaderQueue.IsEmpty; } } + + /// + /// Creates a new async-compatible reader/writer lock. + /// + public AsyncReaderWriterLock(IAsyncWaitQueue writerQueue, IAsyncWaitQueue readerQueue, + IAsyncWaitQueue upgradeableReaderQueue, IAsyncWaitQueue upgradeReaderQueue) + { + _writerQueue = writerQueue; + _readerQueue = readerQueue; + _upgradeableReaderQueue = upgradeableReaderQueue; + _upgradeReaderQueue = upgradeReaderQueue; + _mutex = new object(); + } + + /// + /// Creates a new async-compatible reader/writer lock. + /// + public AsyncReaderWriterLock() + : this(new DefaultAsyncWaitQueue(), new DefaultAsyncWaitQueue(), + new DefaultAsyncWaitQueue(), new DefaultAsyncWaitQueue()) + { + } + + /// + /// Gets a semi-unique identifier for this asynchronous lock. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } + + internal object SyncObject + { + get { return _mutex; } + } + + /// + /// Applies a continuation to the task that will call if the task is canceled. This method may not be called while holding the sync lock. + /// + /// The task to observe for cancellation. + private void ReleaseWaitersWhenCanceled(Task task) + { + task.ContinueWith(t => + { + List finishes; + lock (SyncObject) { finishes = ReleaseWaiters(); } + foreach (var finish in finishes) + finish.Dispose(); + }, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + /// + /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public Task ReaderLockAsync(CancellationToken cancellationToken) + { + Task ret; + lock (SyncObject) + { + // If the lock is available or in read mode and there are no waiting writers, upgradeable readers, or upgrading readers, take it immediately. + if (_locksHeld >= 0 && _writerQueue.IsEmpty && _upgradeableReaderQueue.IsEmpty && _upgradeReaderQueue.IsEmpty) + { + ++_locksHeld; + ret = TaskShim.FromResult(new ReaderKey(this)); + } + else + { + // Wait for the lock to become available or cancellation. + ret = _readerQueue.Enqueue(cancellationToken); + } + } + + return ret; + } + + /// + /// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public IDisposable ReaderLock(CancellationToken cancellationToken) + { + Task ret; + lock (SyncObject) + { + // If the lock is available or in read mode and there are no waiting writers, upgradeable readers, or upgrading readers, take it immediately. + if (_locksHeld >= 0 && _writerQueue.IsEmpty && _upgradeableReaderQueue.IsEmpty && _upgradeReaderQueue.IsEmpty) + { + ++_locksHeld; + return new ReaderKey(this); + } + + // Wait for the lock to become available or cancellation. + ret = _readerQueue.Enqueue(cancellationToken); + } + + return ret.WaitAndUnwrapException(); + } + + /// + /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. + /// + /// A disposable that releases the lock when disposed. + public Task ReaderLockAsync() + { + return ReaderLockAsync(CancellationToken.None); + } + + /// + /// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// A disposable that releases the lock when disposed. + public IDisposable ReaderLock() + { + return ReaderLock(CancellationToken.None); + } + + /// + /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public Task WriterLockAsync(CancellationToken cancellationToken) + { + Task ret; + lock (SyncObject) + { + // If the lock is available, take it immediately. + if (_locksHeld == 0) + { + _locksHeld = -1; + ret = TaskShim.FromResult(new WriterKey(this)); + } + else + { + // Wait for the lock to become available or cancellation. + ret = _writerQueue.Enqueue(cancellationToken); + } + } + + ReleaseWaitersWhenCanceled(ret); + return ret; + } + + /// + /// Synchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public IDisposable WriterLock(CancellationToken cancellationToken) + { + Task ret; + lock (SyncObject) + { + // If the lock is available, take it immediately. + if (_locksHeld == 0) + { + _locksHeld = -1; + return new WriterKey(this); + } + + // Wait for the lock to become available or cancellation. + ret = _writerQueue.Enqueue(cancellationToken); + } + + ReleaseWaitersWhenCanceled(ret); + return ret.WaitAndUnwrapException(); + } + + /// + /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. + /// + /// A disposable that releases the lock when disposed. + public Task WriterLockAsync() + { + return WriterLockAsync(CancellationToken.None); + } + + /// + /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// A disposable that releases the lock when disposed. + public IDisposable WriterLock() + { + return WriterLock(CancellationToken.None); + } + + /// + /// Asynchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed. + public Task UpgradeableReaderLockAsync(CancellationToken cancellationToken) + { + Task ret; + lock (SyncObject) + { + // If the lock is available, take it immediately. + if (_locksHeld == 0 || (_locksHeld > 0 && _upgradeableReaderKey == null)) + { + ++_locksHeld; + _upgradeableReaderKey = new UpgradeableReaderKey(this); + ret = TaskShim.FromResult(_upgradeableReaderKey); + } + else + { + // Wait for the lock to become available or cancellation. + ret = _upgradeableReaderQueue.Enqueue(cancellationToken); + } + } + + ReleaseWaitersWhenCanceled(ret); + return ret; + } + + /// + /// Synchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed. + public UpgradeableReaderKey UpgradeableReaderLock(CancellationToken cancellationToken) + { + Task ret; + lock (SyncObject) + { + // If the lock is available, take it immediately. + if (_locksHeld == 0 || (_locksHeld > 0 && _upgradeableReaderKey == null)) + { + ++_locksHeld; + _upgradeableReaderKey = new UpgradeableReaderKey(this); + return _upgradeableReaderKey; + } + + // Wait for the lock to become available or cancellation. + ret = _upgradeableReaderQueue.Enqueue(cancellationToken); + } + + ReleaseWaitersWhenCanceled(ret); + return ret.WaitAndUnwrapException(); + } + + /// + /// Asynchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed. + /// + /// A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed. + public Task UpgradeableReaderLockAsync() + { + return UpgradeableReaderLockAsync(CancellationToken.None); + } + + /// + /// Synchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed. This method may block the calling thread. + /// + /// A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed. + public UpgradeableReaderKey UpgradeableReaderLock() + { + return UpgradeableReaderLock(CancellationToken.None); + } + + /// + /// Asynchronously upgrades a reader lock to a writer lock. This method assumes the sync lock is already held. + /// + internal Task UpgradeAsync(CancellationToken cancellationToken) + { + Task ret; + + // If the lock is available, take it immediately. + if (_locksHeld == 1) + { + _locksHeld = -1; + ret = TaskShim.FromResult(new UpgradeableReaderKey.UpgradeKey(_upgradeableReaderKey)); + } + else + { + // Wait for the lock to become available or cancellation. + ret = _upgradeReaderQueue.Enqueue(cancellationToken); + } + return ret; + } + + /// + /// Downgrades a writer lock to a reader lock. This method assumes the sync lock is already held. + /// + internal List Downgrade() + { + _locksHeld = 1; + return ReleaseWaiters(); + } + + /// + /// Grants lock(s) to waiting tasks. This method assumes the sync lock is already held. + /// + private List ReleaseWaiters() + { + var ret = new List(); + + if (_locksHeld == 0) + { + // Give priority to writers. + if (!_writerQueue.IsEmpty) + { + ret.Add(_writerQueue.Dequeue(new WriterKey(this))); + _locksHeld = -1; + return ret; + } + + // Then to upgradeable readers. + if (!_upgradeableReaderQueue.IsEmpty) + { + _upgradeableReaderKey = new UpgradeableReaderKey(this); + ret.Add(_upgradeableReaderQueue.Dequeue(_upgradeableReaderKey)); + ++_locksHeld; + } + + // Finally to readers. + while (!_readerQueue.IsEmpty) + { + ret.Add(_readerQueue.Dequeue(new ReaderKey(this))); + ++_locksHeld; + } + + return ret; + } + + // Give priority to upgrading readers. + if (_locksHeld == 1) + { + if (!_upgradeReaderQueue.IsEmpty) + { + ret.Add(_upgradeReaderQueue.Dequeue(new UpgradeableReaderKey.UpgradeKey(_upgradeableReaderKey))); + _locksHeld = -1; + } + } + + if (_locksHeld > 0) + { + // If there are current reader locks and waiting writers, then do nothing. + if (!_writerQueue.IsEmpty || !_upgradeableReaderQueue.IsEmpty || !_upgradeReaderQueue.IsEmpty) + return ret; + + // If there are current reader locks but no upgradeable reader lock, try to release an upgradeable reader. + if (_upgradeableReaderKey == null && !_upgradeableReaderQueue.IsEmpty) + { + _upgradeableReaderKey = new UpgradeableReaderKey(this); + ret.Add(_upgradeableReaderQueue.Dequeue(_upgradeableReaderKey)); + } + } + + return ret; + } + + /// + /// Releases the lock as a reader. + /// + internal void ReleaseReaderLock() + { + List finishes; + lock (SyncObject) + { + --_locksHeld; + finishes = ReleaseWaiters(); + } + foreach (var finish in finishes) + finish.Dispose(); + } + + /// + /// Releases the lock as a writer. + /// + internal void ReleaseWriterLock() + { + List finishes; + lock (SyncObject) + { + _locksHeld = 0; + finishes = ReleaseWaiters(); + } + foreach (var finish in finishes) + finish.Dispose(); + } + + /// + /// Releases the lock as an upgradeable reader. + /// + internal void ReleaseUpgradeableReaderLock(Task upgrade) + { + IDisposable cancelFinish = null; + List finishes; + lock (SyncObject) + { + if (upgrade != null) + cancelFinish = _upgradeReaderQueue.TryCancel(upgrade); + _upgradeableReaderKey = null; + --_locksHeld; + finishes = ReleaseWaiters(); + } + if (cancelFinish != null) + cancelFinish.Dispose(); + foreach (var finish in finishes) + finish.Dispose(); + } + + /// + /// The disposable which releases the reader lock. + /// + private sealed class ReaderKey : IDisposable + { + /// + /// The lock to release. + /// + private AsyncReaderWriterLock _asyncReaderWriterLock; + + /// + /// Creates the key for a lock. + /// + /// The lock to release. May not be null. + public ReaderKey(AsyncReaderWriterLock asyncReaderWriterLock) + { + _asyncReaderWriterLock = asyncReaderWriterLock; + } + + /// + /// Release the lock. + /// + public void Dispose() + { + if (_asyncReaderWriterLock == null) + return; + _asyncReaderWriterLock.ReleaseReaderLock(); + _asyncReaderWriterLock = null; + } + } + + /// + /// The disposable which releases the writer lock. + /// + private sealed class WriterKey : IDisposable + { + /// + /// The lock to release. + /// + private AsyncReaderWriterLock _asyncReaderWriterLock; + + /// + /// Creates the key for a lock. + /// + /// The lock to release. May not be null. + public WriterKey(AsyncReaderWriterLock asyncReaderWriterLock) + { + _asyncReaderWriterLock = asyncReaderWriterLock; + } + + /// + /// Release the lock. + /// + public void Dispose() + { + if (_asyncReaderWriterLock == null) + return; + _asyncReaderWriterLock.ReleaseWriterLock(); + _asyncReaderWriterLock = null; + } + } + + /// + /// The disposable which manages the upgradeable reader lock. + /// + [DebuggerDisplay("State = {GetStateForDebugger}, ReaderWriterLockId = {_asyncReaderWriterLock.Id}")] + public sealed class UpgradeableReaderKey : IDisposable + { + /// + /// The lock to release. + /// + private readonly AsyncReaderWriterLock _asyncReaderWriterLock; + + /// + /// The task doing the upgrade. + /// + private Task _upgrade; + + /// + /// Whether or not this instance has been disposed. + /// + private bool _disposed; + + [DebuggerNonUserCode] + internal State GetStateForDebugger + { + get + { + if (_upgrade == null) + return State.Reader; + if (_upgrade.Status == TaskStatus.RanToCompletion) + return State.Writer; + return State.UpgradingToWriter; + } + } + + internal enum State + { + Reader, + UpgradingToWriter, + Writer, + } + + /// + /// Creates the key for a lock. + /// + /// The lock to release. May not be null. + internal UpgradeableReaderKey(AsyncReaderWriterLock asyncReaderWriterLock) + { + _asyncReaderWriterLock = asyncReaderWriterLock; + } + + /// + /// Gets a value indicating whether this lock has been upgraded to a write lock. + /// + public bool Upgraded + { + get + { + Task task; + lock (_asyncReaderWriterLock.SyncObject) { task = _upgrade; } + return (task != null && task.Status == TaskStatus.RanToCompletion); + } + } + + /// + /// Upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed. + /// + /// The cancellation token used to cancel the upgrade. If this is already set, then this method will attempt to upgrade immediately (succeeding if the lock is currently available). + public Task UpgradeAsync(CancellationToken cancellationToken) + { + lock (_asyncReaderWriterLock.SyncObject) + { + if (_upgrade != null) + throw new InvalidOperationException("Cannot upgrade."); + + _upgrade = _asyncReaderWriterLock.UpgradeAsync(cancellationToken); + } + + _asyncReaderWriterLock.ReleaseWaitersWhenCanceled(_upgrade); + var ret = new TaskCompletionSource(); + _upgrade.ContinueWith(t => + { + if (t.IsCanceled) + lock (_asyncReaderWriterLock.SyncObject) { _upgrade = null; } + ret.TryCompleteFromCompletedTask(t); + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + return ret.Task; + } + + /// + /// Synchronously upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the upgrade. If this is already set, then this method will attempt to upgrade immediately (succeeding if the lock is currently available). + public IDisposable Upgrade(CancellationToken cancellationToken) + { + lock (_asyncReaderWriterLock.SyncObject) + { + if (_upgrade != null) + throw new InvalidOperationException("Cannot upgrade."); + + _upgrade = _asyncReaderWriterLock.UpgradeAsync(cancellationToken); + } + + _asyncReaderWriterLock.ReleaseWaitersWhenCanceled(_upgrade); + var ret = new TaskCompletionSource(); + _upgrade.ContinueWith(t => + { + if (t.IsCanceled) + lock (_asyncReaderWriterLock.SyncObject) { _upgrade = null; } + ret.TryCompleteFromCompletedTask(t); + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + return ret.Task.WaitAndUnwrapException(); + } + + /// + /// Upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed. + /// + public Task UpgradeAsync() + { + return UpgradeAsync(CancellationToken.None); + } + + /// + /// Synchronously upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed. This method may block the calling thread. + /// + public IDisposable Upgrade() + { + return Upgrade(CancellationToken.None); + } + + /// + /// Downgrades the writer lock to a reader lock. + /// + private void Downgrade() + { + List finishes; + lock (_asyncReaderWriterLock.SyncObject) + { + finishes = _asyncReaderWriterLock.Downgrade(); + _upgrade = null; + } + foreach (var finish in finishes) + finish.Dispose(); + } + + /// + /// Release the lock. + /// + public void Dispose() + { + if (_disposed) + return; + _asyncReaderWriterLock.ReleaseUpgradeableReaderLock(_upgrade); + _disposed = true; + } + + /// + /// The disposable which downgrades an upgradeable reader key. + /// + internal sealed class UpgradeKey : IDisposable + { + /// + /// The upgradeable reader key to downgrade. + /// + private UpgradeableReaderKey _key; + + /// + /// Creates the upgrade key for an upgradeable reader key. + /// + /// The upgradeable reader key to downgrade. May not be null. + public UpgradeKey(UpgradeableReaderKey key) + { + _key = key; + } + + /// + /// Downgrade the upgradeable reader key. + /// + public void Dispose() + { + if (_key == null) + return; + _key.Downgrade(); + _key = null; + } + } + } + + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncReaderWriterLock _rwl; + + public DebugView(AsyncReaderWriterLock rwl) + { + _rwl = rwl; + } + + public int Id { get { return _rwl.Id; } } + + public State State { get { return _rwl.GetStateForDebugger; } } + + public int ReaderCount { get { return _rwl.GetReaderCountForDebugger; } } + + public bool UpgradeInProgress { get { return _rwl.GetUpgradeInProgressForDebugger; } } + + public IAsyncWaitQueue ReaderWaitQueue { get { return _rwl._readerQueue; } } + + public IAsyncWaitQueue WriterWaitQueue { get { return _rwl._writerQueue; } } + + public IAsyncWaitQueue UpgradeableReaderWaitQueue { get { return _rwl._upgradeableReaderQueue; } } + + public IAsyncWaitQueue UpgradeReaderWaitQueue { get { return _rwl._upgradeReaderQueue; } } + } + // ReSharper restore UnusedMember.Local + } +} diff --git a/ZeroLevel/Services/Async/AsyncSemaphore.cs b/ZeroLevel/Services/Async/AsyncSemaphore.cs new file mode 100644 index 0000000..4581e41 --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncSemaphore.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// An async-compatible semaphore. Alternatively, you could use SemaphoreSlim on .NET 4.5 / Windows Store. + /// + [DebuggerDisplay("Id = {Id}, CurrentCount = {_count}")] + [DebuggerTypeProxy(typeof(DebugView))] + public sealed class AsyncSemaphore + { + /// + /// The queue of TCSs that other tasks are awaiting to acquire the semaphore. + /// + private readonly IAsyncWaitQueue _queue; + + /// + /// The number of waits that will be immediately granted. + /// + private int _count; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; + + /// + /// Creates a new async-compatible semaphore with the specified initial count. + /// + /// The initial count for this semaphore. This must be greater than or equal to zero. + /// The wait queue used to manage waiters. + public AsyncSemaphore(int initialCount, IAsyncWaitQueue queue) + { + _queue = queue; + _count = initialCount; + _mutex = new object(); + } + + /// + /// Creates a new async-compatible semaphore with the specified initial count. + /// + /// The initial count for this semaphore. This must be greater than or equal to zero. + public AsyncSemaphore(int initialCount) + : this(initialCount, new DefaultAsyncWaitQueue()) + { + } + + /// + /// Gets a semi-unique identifier for this asynchronous semaphore. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } + + /// + /// Gets the number of slots currently available on this semaphore. + /// + public int CurrentCount + { + get { lock (_mutex) { return _count; } } + } + + /// + /// Asynchronously waits for a slot in the semaphore to be available. + /// + /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). + public Task WaitAsync(CancellationToken cancellationToken) + { + Task ret; + lock (_mutex) + { + // If the semaphore is available, take it immediately and return. + if (_count != 0) + { + --_count; + ret = TaskConstants.Completed; + } + else + { + // Wait for the semaphore to become available or cancellation. + ret = _queue.Enqueue(cancellationToken); + } + } + + return ret; + } + + /// + /// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread. + /// + /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). + public void Wait(CancellationToken cancellationToken) + { + WaitAsync(cancellationToken).WaitAndUnwrapException(); + } + + /// + /// Asynchronously waits for a slot in the semaphore to be available. + /// + public Task WaitAsync() + { + return WaitAsync(CancellationToken.None); + } + + /// + /// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread. + /// + public void Wait() + { + Wait(CancellationToken.None); + } + + /// + /// Releases the semaphore. + /// + public void Release(int releaseCount) + { + if (releaseCount == 0) + return; + var finishes = new List(); + lock (_mutex) + { + if (_count > int.MaxValue - releaseCount) + throw new InvalidOperationException("Could not release semaphore."); + + var oldCount = _count; + while (releaseCount != 0) + { + if (_queue.IsEmpty) + ++_count; + else + finishes.Add(_queue.Dequeue()); + --releaseCount; + } + } + foreach (var finish in finishes) + finish.Dispose(); + } + + /// + /// Releases the semaphore. + /// + public void Release() + { + Release(1); + } + + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncSemaphore _semaphore; + + public DebugView(AsyncSemaphore semaphore) + { + _semaphore = semaphore; + } + + public int Id { get { return _semaphore.Id; } } + + public int CurrentCount { get { return _semaphore._count; } } + + public IAsyncWaitQueue WaitQueue { get { return _semaphore._queue; } } + } + // ReSharper restore UnusedMember.Local + } +} diff --git a/ZeroLevel/Services/Async/AsyncWaitQueue.cs b/ZeroLevel/Services/Async/AsyncWaitQueue.cs new file mode 100644 index 0000000..4382a3c --- /dev/null +++ b/ZeroLevel/Services/Async/AsyncWaitQueue.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// A collection of cancelable instances. Implementations must be threadsafe and must work correctly if the caller is holding a lock. + /// + /// The type of the results. If this isn't needed, use . + public interface IAsyncWaitQueue + { + /// + /// Gets whether the queue is empty. + /// + bool IsEmpty { get; } + + /// + /// Creates a new entry and queues it to this wait queue. The returned task must support both synchronous and asynchronous waits. + /// + /// The queued task. + Task Enqueue(); + + /// + /// Removes a single entry in the wait queue. Returns a disposable that completes that entry. + /// + /// The result used to complete the wait queue entry. If this isn't needed, use default(T). + IDisposable Dequeue(T result = default(T)); + + /// + /// Removes all entries in the wait queue. Returns a disposable that completes all entries. + /// + /// The result used to complete the wait queue entries. If this isn't needed, use default(T). + IDisposable DequeueAll(T result = default(T)); + + /// + /// Attempts to remove an entry from the wait queue. Returns a disposable that cancels the entry. + /// + /// The task to cancel. + /// A value indicating whether the entry was found and canceled. + IDisposable TryCancel(Task task); + + /// + /// Removes all entries from the wait queue. Returns a disposable that cancels all entries. + /// + IDisposable CancelAll(); + } + + /// + /// Provides extension methods for wait queues. + /// + public static class AsyncWaitQueueExtensions + { + /// + /// Creates a new entry and queues it to this wait queue. If the cancellation token is already canceled, this method immediately returns a canceled task without modifying the wait queue. + /// + /// The wait queue. + /// The token used to cancel the wait. + /// The queued task. + public static Task Enqueue(this IAsyncWaitQueue @this, CancellationToken token) + { + if (token.IsCancellationRequested) + return TaskConstants.Canceled; + + var ret = @this.Enqueue(); + if (token.CanBeCanceled) + { + var registration = token.Register(() => @this.TryCancel(ret).Dispose(), useSynchronizationContext: false); + ret.ContinueWith(_ => registration.Dispose(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + return ret; + } + } + + /// + /// The default wait queue implementation, which uses a double-ended queue. + /// + /// The type of the results. If this isn't needed, use . + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(DefaultAsyncWaitQueue<>.DebugView))] + public sealed class DefaultAsyncWaitQueue : IAsyncWaitQueue + { + private readonly Deque> _queue = new Deque>(); + + private int Count + { + get { lock (_queue) { return _queue.Count; } } + } + + bool IAsyncWaitQueue.IsEmpty + { + get { return Count == 0; } + } + + Task IAsyncWaitQueue.Enqueue() + { + var tcs = new TaskCompletionSource(); + lock (_queue) + _queue.AddToBack(tcs); + return tcs.Task; + } + + IDisposable IAsyncWaitQueue.Dequeue(T result) + { + TaskCompletionSource tcs; + lock (_queue) + tcs = _queue.RemoveFromFront(); + return new CompleteDisposable(result, tcs); + } + + IDisposable IAsyncWaitQueue.DequeueAll(T result) + { + TaskCompletionSource[] taskCompletionSources; + lock (_queue) + { + taskCompletionSources = _queue.ToArray(); + _queue.Clear(); + } + return new CompleteDisposable(result, taskCompletionSources); + } + + IDisposable IAsyncWaitQueue.TryCancel(Task task) + { + TaskCompletionSource tcs = null; + lock (_queue) + { + for (int i = 0; i != _queue.Count; ++i) + { + if (_queue[i].Task == task) + { + tcs = _queue[i]; + _queue.RemoveAt(i); + break; + } + } + } + if (tcs == null) + return new CancelDisposable(); + return new CancelDisposable(tcs); + } + + IDisposable IAsyncWaitQueue.CancelAll() + { + TaskCompletionSource[] taskCompletionSources; + lock (_queue) + { + taskCompletionSources = _queue.ToArray(); + _queue.Clear(); + } + return new CancelDisposable(taskCompletionSources); + } + + private sealed class CancelDisposable : IDisposable + { + private readonly TaskCompletionSource[] _taskCompletionSources; + + public CancelDisposable(params TaskCompletionSource[] taskCompletionSources) + { + _taskCompletionSources = taskCompletionSources; + } + + public void Dispose() + { + foreach (var cts in _taskCompletionSources) + cts.TrySetCanceled(); + } + } + + private sealed class CompleteDisposable : IDisposable + { + private readonly TaskCompletionSource[] _taskCompletionSources; + private readonly T _result; + + public CompleteDisposable(T result, params TaskCompletionSource[] taskCompletionSources) + { + _result = result; + _taskCompletionSources = taskCompletionSources; + } + + public void Dispose() + { + foreach (var cts in _taskCompletionSources) + cts.TrySetResult(_result); + } + } + + [DebuggerNonUserCode] + internal sealed class DebugView + { + private readonly DefaultAsyncWaitQueue _queue; + + public DebugView(DefaultAsyncWaitQueue queue) + { + _queue = queue; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public Task[] Tasks + { + get { return _queue._queue.Select(x => x.Task).ToArray(); } + } + } + } +} diff --git a/ZeroLevel/Services/Async/CancellationTokenHelpers.cs b/ZeroLevel/Services/Async/CancellationTokenHelpers.cs new file mode 100644 index 0000000..48edbf5 --- /dev/null +++ b/ZeroLevel/Services/Async/CancellationTokenHelpers.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// Helper methods for cancellation tokens. + /// + public static class CancellationTokenHelpers + { + /// + /// Initializes the static members. + /// + static CancellationTokenHelpers() + { + Canceled = new CancellationToken(true); + } + + /// + /// Gets , a cancellation token that is never canceled. + /// + public static CancellationToken None { get { return CancellationToken.None; } } + + /// + /// Gets a cancellation token that is already canceled. + /// + public static CancellationToken Canceled { get; private set; } + + /// + /// Creates a cancellation token that is canceled after the due time. + /// + /// The due time after which to cancel the token. + /// A cancellation token that is canceled after the due time. + public static NormalizedCancellationToken Timeout(TimeSpan dueTime) + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(dueTime); + return new NormalizedCancellationToken(cts); + } + + /// + /// Creates a cancellation token that is canceled after the due time. + /// + /// The due time after which to cancel the token. + /// A cancellation token that is canceled after the due time. + public static NormalizedCancellationToken Timeout(int dueTime) + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(dueTime); + return new NormalizedCancellationToken(cts); + } + + /// + /// Reduces a set of cancellation tokens by removing any cancellation tokens that cannot be canceled. If any tokens are already canceled, the returned token will be canceled. + /// + /// The cancellation tokens to reduce. + public static NormalizedCancellationToken Normalize(params CancellationToken[] cancellationTokens) + { + return Normalize((IEnumerable)cancellationTokens); + } + + /// + /// Reduces a set of cancellation tokens by removing any cancellation tokens that cannot be canceled. If any tokens are already canceled, the returned token will be canceled. + /// + /// The cancellation tokens to reduce. + public static NormalizedCancellationToken Normalize(IEnumerable cancellationTokens) + { + var tokens = cancellationTokens.Where(t => t.CanBeCanceled).ToArray(); + if (tokens.Length == 0) + return new NormalizedCancellationToken(); + if (tokens.Length == 1) + return new NormalizedCancellationToken(tokens[0]); + var alreadyCanceled = tokens.FirstOrDefault(t => t.IsCancellationRequested); + if (alreadyCanceled.IsCancellationRequested) + return new NormalizedCancellationToken(alreadyCanceled); + return new NormalizedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(tokens)); + } + + /// + /// Creates a cancellation token that is canceled when the provided completes. + /// + /// The task to observe. + /// The options to use for the task continuation. + public static NormalizedCancellationToken FromTask(Task source, TaskContinuationOptions continuationOptions) + { + var cts = new CancellationTokenSource(); + source.ContinueWith(_ => cts.Cancel(), CancellationToken.None, continuationOptions, TaskScheduler.Default); + return new NormalizedCancellationToken(cts); + } + + /// + /// Creates a cancellation token that is canceled when the provided completes. + /// + /// The task to observe. + public static NormalizedCancellationToken FromTask(Task source) + { + return FromTask(source, TaskContinuationOptions.None); + } + } +} diff --git a/ZeroLevel/Services/Async/CancellationTokenTaskSource.cs b/ZeroLevel/Services/Async/CancellationTokenTaskSource.cs new file mode 100644 index 0000000..879f182 --- /dev/null +++ b/ZeroLevel/Services/Async/CancellationTokenTaskSource.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed. + /// + public sealed class CancellationTokenTaskSource + : IDisposable + { + /// + /// The cancellation token registration, if any. This is null if the registration was not necessary. + /// + private readonly IDisposable _registration; + + /// + /// Creates a task for the specified cancellation token, registering with the token if necessary. + /// + /// The cancellation token to observe. + public CancellationTokenTaskSource(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + Task = System.Threading.Tasks.Task.FromCanceled(cancellationToken); + return; + } + var tcs = new TaskCompletionSource(); + _registration = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken), useSynchronizationContext: false); + Task = tcs.Task; + } + + /// + /// Gets the task for the source cancellation token. + /// + public Task Task { get; private set; } + + /// + /// Disposes the cancellation token registration, if any. Note that this may cause to never complete. + /// + public void Dispose() + { + _registration?.Dispose(); + } + } +} diff --git a/ZeroLevel/Services/Async/Deque.cs b/ZeroLevel/Services/Async/Deque.cs new file mode 100644 index 0000000..e93239c --- /dev/null +++ b/ZeroLevel/Services/Async/Deque.cs @@ -0,0 +1,836 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace ZeroLevel.Services.Async +{ + /// + /// A double-ended queue (deque), which provides O(1) indexed access, O(1) removals from the front and back, amortized O(1) insertions to the front and back, and O(N) insertions and removals anywhere else (with the operations getting slower as the index approaches the middle). + /// + /// The type of elements contained in the deque. + [DebuggerDisplay("Count = {Count}, Capacity = {Capacity}")] + [DebuggerTypeProxy(typeof(Deque<>.DebugView))] + internal sealed class Deque : IList, System.Collections.IList + { + /// + /// The default capacity. + /// + private const int DefaultCapacity = 8; + + /// + /// The circular buffer that holds the view. + /// + private T[] buffer; + + /// + /// The offset into where the view begins. + /// + private int offset; + + /// + /// Initializes a new instance of the class with the specified capacity. + /// + /// The initial capacity. Must be greater than 0. + public Deque(int capacity) + { + if (capacity < 1) + throw new ArgumentOutOfRangeException("capacity", "Capacity must be greater than 0."); + buffer = new T[capacity]; + } + + /// + /// Initializes a new instance of the class with the elements from the specified collection. + /// + /// The collection. + public Deque(IEnumerable collection) + { + int count = collection.Count(); + if (count > 0) + { + buffer = new T[count]; + DoInsertRange(0, collection, count); + } + else + { + buffer = new T[DefaultCapacity]; + } + } + + /// + /// Initializes a new instance of the class. + /// + public Deque() + : this(DefaultCapacity) + { + } + + #region GenericListImplementations + + /// + /// Gets a value indicating whether this list is read-only. This implementation always returns false. + /// + /// true if this list is read-only; otherwise, false. + bool ICollection.IsReadOnly + { + get { return false; } + } + + /// + /// Gets or sets the item at the specified index. + /// + /// The index of the item to get or set. + /// is not a valid index in this list. + /// This property is set and the list is read-only. + public T this[int index] + { + get + { + CheckExistingIndexArgument(this.Count, index); + return DoGetItem(index); + } + + set + { + CheckExistingIndexArgument(this.Count, index); + DoSetItem(index, value); + } + } + + /// + /// Inserts an item to this list at the specified index. + /// + /// The zero-based index at which should be inserted. + /// The object to insert into this list. + /// + /// is not a valid index in this list. + /// + /// + /// This list is read-only. + /// + public void Insert(int index, T item) + { + CheckNewIndexArgument(Count, index); + DoInsert(index, item); + } + + /// + /// Removes the item at the specified index. + /// + /// The zero-based index of the item to remove. + /// + /// is not a valid index in this list. + /// + /// + /// This list is read-only. + /// + public void RemoveAt(int index) + { + CheckExistingIndexArgument(Count, index); + DoRemoveAt(index); + } + + /// + /// Determines the index of a specific item in this list. + /// + /// The object to locate in this list. + /// The index of if found in this list; otherwise, -1. + public int IndexOf(T item) + { + var comparer = EqualityComparer.Default; + int ret = 0; + foreach (var sourceItem in this) + { + if (comparer.Equals(item, sourceItem)) + return ret; + ++ret; + } + + return -1; + } + + /// + /// Adds an item to the end of this list. + /// + /// The object to add to this list. + /// + /// This list is read-only. + /// + void ICollection.Add(T item) + { + DoInsert(Count, item); + } + + /// + /// Determines whether this list contains a specific value. + /// + /// The object to locate in this list. + /// + /// true if is found in this list; otherwise, false. + /// + bool ICollection.Contains(T item) + { + return this.Contains(item, null); + } + + /// + /// Copies the elements of this list to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from this slice. The must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// + /// is null. + /// + /// + /// is less than 0. + /// + /// + /// is equal to or greater than the length of . + /// -or- + /// The number of elements in the source is greater than the available space from to the end of the destination . + /// + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException("array", "Array is null"); + + int count = this.Count; + CheckRangeArguments(array.Length, arrayIndex, count); + for (int i = 0; i != count; ++i) + { + array[arrayIndex + i] = this[i]; + } + } + + /// + /// Removes the first occurrence of a specific object from this list. + /// + /// The object to remove from this list. + /// + /// true if was successfully removed from this list; otherwise, false. This method also returns false if is not found in this list. + /// + /// + /// This list is read-only. + /// + public bool Remove(T item) + { + int index = IndexOf(item); + if (index == -1) + return false; + + DoRemoveAt(index); + return true; + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + int count = this.Count; + for (int i = 0; i != count; ++i) + { + yield return DoGetItem(i); + } + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + #endregion + #region ObjectListImplementations + + int System.Collections.IList.Add(object value) + { + AddToBack((T)value); + return Count - 1; + } + + bool System.Collections.IList.Contains(object value) + { + return this.Contains((T)value); + } + + int System.Collections.IList.IndexOf(object value) + { + return IndexOf((T)value); + } + + void System.Collections.IList.Insert(int index, object value) + { + Insert(index, (T)value); + } + + bool System.Collections.IList.IsFixedSize + { + get { return false; } + } + + bool System.Collections.IList.IsReadOnly + { + get { return false; } + } + + void System.Collections.IList.Remove(object value) + { + Remove((T)value); + } + + object System.Collections.IList.this[int index] + { + get + { + return this[index]; + } + + set + { + this[index] = (T)value; + } + } + + void System.Collections.ICollection.CopyTo(Array array, int index) + { + if (array == null) + throw new ArgumentNullException("array", "Destination array cannot be null."); + CheckRangeArguments(array.Length, index, Count); + + for (int i = 0; i != Count; ++i) + { + try + { + array.SetValue(this[i], index + i); + } + catch (InvalidCastException ex) + { + throw new ArgumentException("Destination array is of incorrect type.", ex); + } + } + } + + bool System.Collections.ICollection.IsSynchronized + { + get { return false; } + } + + object System.Collections.ICollection.SyncRoot + { + get { return this; } + } + + #endregion + #region GenericListHelpers + + /// + /// Checks the argument to see if it refers to a valid insertion point in a source of a given length. + /// + /// The length of the source. This parameter is not checked for validity. + /// The index into the source. + /// is not a valid index to an insertion point for the source. + private static void CheckNewIndexArgument(int sourceLength, int index) + { + if (index < 0 || index > sourceLength) + { + throw new ArgumentOutOfRangeException("index", "Invalid new index " + index + " for source length " + sourceLength); + } + } + + /// + /// Checks the argument to see if it refers to an existing element in a source of a given length. + /// + /// The length of the source. This parameter is not checked for validity. + /// The index into the source. + /// is not a valid index to an existing element for the source. + private static void CheckExistingIndexArgument(int sourceLength, int index) + { + if (index < 0 || index >= sourceLength) + { + throw new ArgumentOutOfRangeException("index", "Invalid existing index " + index + " for source length " + sourceLength); + } + } + + /// + /// Checks the and arguments for validity when applied to a source of a given length. Allows 0-element ranges, including a 0-element range at the end of the source. + /// + /// The length of the source. This parameter is not checked for validity. + /// The index into source at which the range begins. + /// The number of elements in the range. + /// Either or is less than 0. + /// The range [offset, offset + count) is not within the range [0, sourceLength). + private static void CheckRangeArguments(int sourceLength, int offset, int count) + { + if (offset < 0) + { + throw new ArgumentOutOfRangeException("offset", "Invalid offset " + offset); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", "Invalid count " + count); + } + + if (sourceLength - offset < count) + { + throw new ArgumentException("Invalid offset (" + offset + ") or count + (" + count + ") for source length " + sourceLength); + } + } + + #endregion + + /// + /// Gets a value indicating whether this instance is empty. + /// + private bool IsEmpty + { + get { return Count == 0; } + } + + /// + /// Gets a value indicating whether this instance is at full capacity. + /// + private bool IsFull + { + get { return Count == Capacity; } + } + + /// + /// Gets a value indicating whether the buffer is "split" (meaning the beginning of the view is at a later index in than the end). + /// + private bool IsSplit + { + get + { + // Overflow-safe version of "(offset + Count) > Capacity" + return offset > (Capacity - Count); + } + } + + /// + /// Gets or sets the capacity for this deque. This value must always be greater than zero, and this property cannot be set to a value less than . + /// + /// Capacity cannot be set to a value less than . + public int Capacity + { + get + { + return buffer.Length; + } + + set + { + if (value < 1) + throw new ArgumentOutOfRangeException("value", "Capacity must be greater than 0."); + + if (value < Count) + throw new InvalidOperationException("Capacity cannot be set to a value less than Count"); + + if (value == buffer.Length) + return; + + // Create the new buffer and copy our existing range. + T[] newBuffer = new T[value]; + if (IsSplit) + { + // The existing buffer is split, so we have to copy it in parts + int length = Capacity - offset; + Array.Copy(buffer, offset, newBuffer, 0, length); + Array.Copy(buffer, 0, newBuffer, length, Count - length); + } + else + { + // The existing buffer is whole + Array.Copy(buffer, offset, newBuffer, 0, Count); + } + + // Set up to use the new buffer. + buffer = newBuffer; + offset = 0; + } + } + + /// + /// Gets the number of elements contained in this deque. + /// + /// The number of elements contained in this deque. + public int Count { get; private set; } + + /// + /// Applies the offset to , resulting in a buffer index. + /// + /// The deque index. + /// The buffer index. + private int DequeIndexToBufferIndex(int index) + { + return (index + offset) % Capacity; + } + + /// + /// Gets an element at the specified view index. + /// + /// The zero-based view index of the element to get. This index is guaranteed to be valid. + /// The element at the specified index. + private T DoGetItem(int index) + { + return buffer[DequeIndexToBufferIndex(index)]; + } + + /// + /// Sets an element at the specified view index. + /// + /// The zero-based view index of the element to get. This index is guaranteed to be valid. + /// The element to store in the list. + private void DoSetItem(int index, T item) + { + buffer[DequeIndexToBufferIndex(index)] = item; + } + + /// + /// Inserts an element at the specified view index. + /// + /// The zero-based view index at which the element should be inserted. This index is guaranteed to be valid. + /// The element to store in the list. + private void DoInsert(int index, T item) + { + EnsureCapacityForOneElement(); + + if (index == 0) + { + DoAddToFront(item); + return; + } + else if (index == Count) + { + DoAddToBack(item); + return; + } + + DoInsertRange(index, new[] { item }, 1); + } + + /// + /// Removes an element at the specified view index. + /// + /// The zero-based view index of the element to remove. This index is guaranteed to be valid. + private void DoRemoveAt(int index) + { + if (index == 0) + { + DoRemoveFromFront(); + return; + } + else if (index == Count - 1) + { + DoRemoveFromBack(); + return; + } + + DoRemoveRange(index, 1); + } + + /// + /// Increments by using modulo- arithmetic. + /// + /// The value by which to increase . May not be negative. + /// The value of after it was incremented. + private int PostIncrement(int value) + { + int ret = offset; + offset += value; + offset %= Capacity; + return ret; + } + + /// + /// Decrements by using modulo- arithmetic. + /// + /// The value by which to reduce . May not be negative or greater than . + /// The value of before it was decremented. + private int PreDecrement(int value) + { + offset -= value; + if (offset < 0) + offset += Capacity; + return offset; + } + + /// + /// Inserts a single element to the back of the view. must be false when this method is called. + /// + /// The element to insert. + private void DoAddToBack(T value) + { + buffer[DequeIndexToBufferIndex(Count)] = value; + ++Count; + } + + /// + /// Inserts a single element to the front of the view. must be false when this method is called. + /// + /// The element to insert. + private void DoAddToFront(T value) + { + buffer[PreDecrement(1)] = value; + ++Count; + } + + /// + /// Removes and returns the last element in the view. must be false when this method is called. + /// + /// The former last element. + private T DoRemoveFromBack() + { + T ret = buffer[DequeIndexToBufferIndex(Count - 1)]; + --Count; + return ret; + } + + /// + /// Removes and returns the first element in the view. must be false when this method is called. + /// + /// The former first element. + private T DoRemoveFromFront() + { + --Count; + return buffer[PostIncrement(1)]; + } + + /// + /// Inserts a range of elements into the view. + /// + /// The index into the view at which the elements are to be inserted. + /// The elements to insert. + /// The number of elements in . Must be greater than zero, and the sum of and must be less than or equal to . + private void DoInsertRange(int index, IEnumerable collection, int collectionCount) + { + // Make room in the existing list + if (index < Count / 2) + { + // Inserting into the first half of the list + + // Move lower items down: [0, index) -> [Capacity - collectionCount, Capacity - collectionCount + index) + // This clears out the low "index" number of items, moving them "collectionCount" places down; + // after rotation, there will be a "collectionCount"-sized hole at "index". + int copyCount = index; + int writeIndex = Capacity - collectionCount; + for (int j = 0; j != copyCount; ++j) + buffer[DequeIndexToBufferIndex(writeIndex + j)] = buffer[DequeIndexToBufferIndex(j)]; + + // Rotate to the new view + this.PreDecrement(collectionCount); + } + else + { + // Inserting into the second half of the list + + // Move higher items up: [index, count) -> [index + collectionCount, collectionCount + count) + int copyCount = Count - index; + int writeIndex = index + collectionCount; + for (int j = copyCount - 1; j != -1; --j) + buffer[DequeIndexToBufferIndex(writeIndex + j)] = buffer[DequeIndexToBufferIndex(index + j)]; + } + + // Copy new items into place + int i = index; + foreach (T item in collection) + { + buffer[DequeIndexToBufferIndex(i)] = item; + ++i; + } + + // Adjust valid count + Count += collectionCount; + } + + /// + /// Removes a range of elements from the view. + /// + /// The index into the view at which the range begins. + /// The number of elements in the range. This must be greater than 0 and less than or equal to . + private void DoRemoveRange(int index, int collectionCount) + { + if (index == 0) + { + // Removing from the beginning: rotate to the new view + this.PostIncrement(collectionCount); + Count -= collectionCount; + return; + } + else if (index == Count - collectionCount) + { + // Removing from the ending: trim the existing view + Count -= collectionCount; + return; + } + + if ((index + (collectionCount / 2)) < Count / 2) + { + // Removing from first half of list + + // Move lower items up: [0, index) -> [collectionCount, collectionCount + index) + int copyCount = index; + int writeIndex = collectionCount; + for (int j = copyCount - 1; j != -1; --j) + buffer[DequeIndexToBufferIndex(writeIndex + j)] = buffer[DequeIndexToBufferIndex(j)]; + + // Rotate to new view + this.PostIncrement(collectionCount); + } + else + { + // Removing from second half of list + + // Move higher items down: [index + collectionCount, count) -> [index, count - collectionCount) + int copyCount = Count - collectionCount - index; + int readIndex = index + collectionCount; + for (int j = 0; j != copyCount; ++j) + buffer[DequeIndexToBufferIndex(index + j)] = buffer[DequeIndexToBufferIndex(readIndex + j)]; + } + + // Adjust valid count + Count -= collectionCount; + } + + /// + /// Doubles the capacity if necessary to make room for one more element. When this method returns, is false. + /// + private void EnsureCapacityForOneElement() + { + if (this.IsFull) + { + this.Capacity = this.Capacity * 2; + } + } + + /// + /// Inserts a single element at the back of this deque. + /// + /// The element to insert. + public void AddToBack(T value) + { + EnsureCapacityForOneElement(); + DoAddToBack(value); + } + + /// + /// Inserts a single element at the front of this deque. + /// + /// The element to insert. + public void AddToFront(T value) + { + EnsureCapacityForOneElement(); + DoAddToFront(value); + } + + /// + /// Inserts a collection of elements into this deque. + /// + /// The index at which the collection is inserted. + /// The collection of elements to insert. + /// is not a valid index to an insertion point for the source. + public void InsertRange(int index, IEnumerable collection) + { + int collectionCount = collection.Count(); + CheckNewIndexArgument(Count, index); + + // Overflow-safe check for "this.Count + collectionCount > this.Capacity" + if (collectionCount > Capacity - Count) + { + this.Capacity = checked(Count + collectionCount); + } + + if (collectionCount == 0) + { + return; + } + + this.DoInsertRange(index, collection, collectionCount); + } + + /// + /// Removes a range of elements from this deque. + /// + /// The index into the deque at which the range begins. + /// The number of elements to remove. + /// Either or is less than 0. + /// The range [, + ) is not within the range [0, ). + public void RemoveRange(int offset, int count) + { + CheckRangeArguments(Count, offset, count); + + if (count == 0) + { + return; + } + + this.DoRemoveRange(offset, count); + } + + /// + /// Removes and returns the last element of this deque. + /// + /// The former last element. + /// The deque is empty. + public T RemoveFromBack() + { + if (this.IsEmpty) + throw new InvalidOperationException("The deque is empty."); + + return this.DoRemoveFromBack(); + } + + /// + /// Removes and returns the first element of this deque. + /// + /// The former first element. + /// The deque is empty. + public T RemoveFromFront() + { + if (this.IsEmpty) + throw new InvalidOperationException("The deque is empty."); + + return this.DoRemoveFromFront(); + } + + /// + /// Removes all items from this deque. + /// + public void Clear() + { + this.offset = 0; + this.Count = 0; + } + + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly Deque deque; + + public DebugView(Deque deque) + { + this.deque = deque; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + var array = new T[deque.Count]; + ((ICollection)deque).CopyTo(array, 0); + return array; + } + } + } + } +} diff --git a/ZeroLevel/Services/Async/ExceptionHelpers.cs b/ZeroLevel/Services/Async/ExceptionHelpers.cs new file mode 100644 index 0000000..553608b --- /dev/null +++ b/ZeroLevel/Services/Async/ExceptionHelpers.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.ExceptionServices; + +namespace ZeroLevel.Services.Async +{ + /// + /// Provides helper (non-extension) methods dealing with exceptions. + /// + public static class ExceptionHelpers + { + /// + /// Attempts to prepare the exception for re-throwing by preserving the stack trace. + /// The returned exception should be immediately thrown. + /// + /// The exception. May not be null. + /// The that was passed into this method. + public static Exception PrepareForRethrow(Exception exception) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + return exception; + } + } +} diff --git a/ZeroLevel/Services/Async/IdManager.cs b/ZeroLevel/Services/Async/IdManager.cs new file mode 100644 index 0000000..80600d8 --- /dev/null +++ b/ZeroLevel/Services/Async/IdManager.cs @@ -0,0 +1,48 @@ +using System.Threading; + +namespace ZeroLevel.Services.Async +{ + /// + /// Allocates Ids for instances on demand. 0 is an invalid/unassigned Id. Ids may be non-unique in very long-running systems. This is similar to the Id system used by and . + /// + /// The type for which ids are generated. +// ReSharper disable UnusedTypeParameter + internal static class IdManager + // ReSharper restore UnusedTypeParameter + { + /// + /// The last id generated for this type. This is 0 if no ids have been generated. + /// +// ReSharper disable StaticFieldInGenericType + private static int _lastId; + // ReSharper restore StaticFieldInGenericType + + /// + /// Returns the id, allocating it if necessary. + /// + /// A reference to the field containing the id. + public static int GetId(ref int id) + { + // If the Id has already been assigned, just use it. + if (id != 0) + return id; + + // Determine the new Id without modifying "id", since other threads may also be determining the new Id at the same time. + int newId; + + // The Increment is in a while loop to ensure we get a non-zero Id: + // If we are incrementing -1, then we want to skip over 0. + // If there are tons of Id allocations going on, we want to skip over 0 no matter how many times we get it. + do + { + newId = Interlocked.Increment(ref _lastId); + } while (newId == 0); + + // Update the Id unless another thread already updated it. + Interlocked.CompareExchange(ref id, newId, 0); + + // Return the current Id, regardless of whether it's our new Id or a new Id from another thread. + return id; + } + } +} diff --git a/ZeroLevel/Services/Async/NormalizedCancellationToken.cs b/ZeroLevel/Services/Async/NormalizedCancellationToken.cs new file mode 100644 index 0000000..1c6cea4 --- /dev/null +++ b/ZeroLevel/Services/Async/NormalizedCancellationToken.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; + +namespace ZeroLevel.Services.Async +{ + /// + /// A that may or may not also reference its own . Instances of this type should always be disposed. + /// + public sealed class NormalizedCancellationToken : IDisposable + { + /// + /// The , if any. If this is not null, then is _cts.Token. + /// + private readonly CancellationTokenSource _cts; + + /// + /// The . If is not null, then this is _cts.Token. + /// + private readonly CancellationToken _token; + + /// + /// Creates a normalized cancellation token that can never be canceled. + /// + public NormalizedCancellationToken() + { + } + + /// + /// Creates a normalized cancellation token from a . is set to the property of . + /// + /// The source for this token. + public NormalizedCancellationToken(CancellationTokenSource cts) + { + _cts = cts; + _token = cts.Token; + } + + /// + /// Creates a normalized cancellation token from a . is set to . + /// + /// The source for this token. + public NormalizedCancellationToken(CancellationToken token) + { + _token = token; + } + + /// + /// Releases any resources used by this normalized cancellation token. + /// + public void Dispose() + { + if (_cts != null) + _cts.Dispose(); + } + + /// + /// Gets the for this normalized cancellation token. + /// + public CancellationToken Token { get { return _token; } } + } +} diff --git a/ZeroLevel/Services/Async/TaskCompletionSource.cs b/ZeroLevel/Services/Async/TaskCompletionSource.cs new file mode 100644 index 0000000..03978a0 --- /dev/null +++ b/ZeroLevel/Services/Async/TaskCompletionSource.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// Represents the producer side of a unbound to a delegate, providing access to the consumer side through the property. + /// + public sealed class TaskCompletionSource + { + /// + /// The underlying TCS. + /// + private readonly TaskCompletionSource _tcs; + + /// + /// Initializes a new instance of the class. + /// + public TaskCompletionSource() + { + _tcs = new TaskCompletionSource(); + } + + /// + /// Initializes a new instance of the class with the specified state. + /// + /// The state to use as the underlying 's . + public TaskCompletionSource(object state) + { + _tcs = new TaskCompletionSource(state); + } + + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The options to use when creating the underlying . + public TaskCompletionSource(TaskCreationOptions creationOptions) + { + _tcs = new TaskCompletionSource(creationOptions); + } + + /// + /// Initializes a new instance of the class with the specified state and options. + /// + /// The state to use as the underlying 's . + /// The options to use when creating the underlying . + public TaskCompletionSource(object state, TaskCreationOptions creationOptions) + { + _tcs = new TaskCompletionSource(state, creationOptions); + } + + /// + /// Gets the created by this . + /// + public Task Task + { + get { return _tcs.Task; } + } + + /// + /// Transitions the underlying into the state. + /// + /// The underlying has already been completed. + public void SetCanceled() + { + _tcs.SetCanceled(); + } + + /// + /// Attempts to transition the underlying into the state. + /// + /// true if the operation was successful; otherwise, false. + public bool TrySetCanceled() + { + return _tcs.TrySetCanceled(); + } + + /// + /// Transitions the underlying into the state. + /// + /// The exception to bind to this . May not be null. + /// The underlying has already been completed. + public void SetException(Exception exception) + { + _tcs.SetException(exception); + } + + /// + /// Transitions the underlying into the state. + /// + /// The collection of exceptions to bind to this . May not be null or contain null elements. + /// The underlying has already been completed. + public void SetException(IEnumerable exceptions) + { + _tcs.SetException(exceptions); + } + + /// + /// Attempts to transition the underlying into the state. + /// + /// The exception to bind to this . May not be null. + /// true if the operation was successful; otherwise, false. + public bool TrySetException(Exception exception) + { + return _tcs.TrySetException(exception); + } + + /// + /// Attempts to transition the underlying into the state. + /// + /// The collection of exceptions to bind to this . May not be null or contain null elements. + /// true if the operation was successful; otherwise, false. + public bool TrySetException(IEnumerable exceptions) + { + return _tcs.TrySetException(exceptions); + } + + /// + /// Transitions the underlying into the state. + /// + /// The underlying has already been completed. + public void SetResult() + { + _tcs.SetResult(null); + } + + /// + /// Attempts to transition the underlying into the state. + /// + /// true if the operation was successful; otherwise, false. + public bool TrySetResult() + { + return _tcs.TrySetResult(null); + } + } +} diff --git a/ZeroLevel/Services/Async/TaskCompletionSourceExtensions.cs b/ZeroLevel/Services/Async/TaskCompletionSourceExtensions.cs new file mode 100644 index 0000000..2133c86 --- /dev/null +++ b/ZeroLevel/Services/Async/TaskCompletionSourceExtensions.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// Provides extension methods for . + /// + public static class TaskCompletionSourceExtensions + { + /// + /// Attempts to complete a , propagating the completion of . + /// + /// The type of the result of the target asynchronous operation. + /// The type of the result of the source asynchronous operation. + /// The task completion source. May not be null. + /// The task. May not be null. + /// true if this method completed the task completion source; false if it was already completed. + public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task) where TSourceResult : TResult + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + if (task == null) + throw new ArgumentNullException(nameof(task)); + + if (task.IsFaulted) + return @this.TrySetException(task.Exception.InnerExceptions); + if (task.IsCanceled) + { + try + { + task.WaitAndUnwrapException(); + } + catch (OperationCanceledException exception) + { + var token = exception.CancellationToken; + return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled(); + } + } + return @this.TrySetResult(task.Result); + } + + /// + /// Attempts to complete a , propagating the completion of but using the result value from if the task completed successfully. + /// + /// The type of the result of the target asynchronous operation. + /// The task completion source. May not be null. + /// The task. May not be null. + /// A delegate that returns the result with which to complete the task completion source, if the task completed successfully. May not be null. + /// true if this method completed the task completion source; false if it was already completed. + public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task, Func resultFunc) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + if (task == null) + throw new ArgumentNullException(nameof(task)); + if (resultFunc == null) + throw new ArgumentNullException(nameof(resultFunc)); + + if (task.IsFaulted) + return @this.TrySetException(task.Exception.InnerExceptions); + if (task.IsCanceled) + { + try + { + task.WaitAndUnwrapException(); + } + catch (OperationCanceledException exception) + { + var token = exception.CancellationToken; + return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled(); + } + } + return @this.TrySetResult(resultFunc()); + } + /// + /// Attempts to complete a , propagating the completion of . + /// + /// The type of the result of the asynchronous operation. + /// The task completion source. May not be null. + /// The event arguments passed to the completion event. May not be null. + /// The delegate used to retrieve the result. This is only invoked if indicates successful completion. May not be null. + /// true if this method completed the task completion source; false if it was already completed. + public static bool TryCompleteFromEventArgs(this TaskCompletionSource @this, AsyncCompletedEventArgs eventArgs, Func getResult) + { + if (eventArgs.Cancelled) + return @this.TrySetCanceled(); + if (eventArgs.Error != null) + return @this.TrySetException(eventArgs.Error); + return @this.TrySetResult(getResult()); + } + /// + /// Attempts to complete a , propagating the completion of . + /// + /// The task completion source. May not be null. + /// The task. May not be null. + /// true if this method completed the task completion source; false if it was already completed. + public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task) + { + if (task.IsFaulted) + return @this.TrySetException(task.Exception.InnerExceptions); + if (task.IsCanceled) + return @this.TrySetCanceled(); + return @this.TrySetResult(); + } + /// + /// Attempts to complete a , propagating the completion of . + /// + /// The task completion source. May not be null. + /// The event arguments passed to the completion event. May not be null. + /// true if this method completed the task completion source; false if it was already completed. + public static bool TryCompleteFromEventArgs(this TaskCompletionSource @this, AsyncCompletedEventArgs eventArgs) + { + if (eventArgs.Cancelled) + return @this.TrySetCanceled(); + if (eventArgs.Error != null) + return @this.TrySetException(eventArgs.Error); + return @this.TrySetResult(); + } + /// + /// Attempts to complete a with the specified value, forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The type of the result of the asynchronous operation. + /// The task completion source. May not be null. + /// The result of the asynchronous operation. + public static void TrySetResultWithBackgroundContinuations(this TaskCompletionSource @this, TResult result) + { + // Set the result on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetResult(result)); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + @this.Task.Wait(); + } + + /// + /// Attempts to complete a , forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The task completion source. May not be null. + public static void TrySetResultWithBackgroundContinuations(this TaskCompletionSource @this) + { + // Set the result on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetResult()); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + @this.Task.Wait(); + } + + /// + /// Attempts to complete a as canceled, forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The type of the result of the asynchronous operation. + /// The task completion source. May not be null. + public static void TrySetCanceledWithBackgroundContinuations(this TaskCompletionSource @this) + { + // Complete on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetCanceled()); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + try + { + @this.Task.Wait(); + } + catch (AggregateException) + { + } + } + /// + /// Creates a new TCS for use with async code, and which forces its continuations to execute asynchronously. + /// + /// The type of the result of the TCS. + public static TaskCompletionSource CreateAsyncTaskSource() + { + return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + /// + /// Attempts to complete a as canceled, forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The task completion source. May not be null. + public static void TrySetCanceledWithBackgroundContinuations(this TaskCompletionSource @this) + { + // Set the result on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetCanceled()); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + try + { + @this.Task.Wait(); + } + catch (AggregateException) + { + } + } + + /// + /// Attempts to complete a as faulted, forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The type of the result of the asynchronous operation. + /// The task completion source. May not be null. + /// The exception to bind to the task. + public static void TrySetExceptionWithBackgroundContinuations(this TaskCompletionSource @this, Exception exception) + { + // Complete on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetException(exception)); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + try + { + @this.Task.Wait(); + } + catch (AggregateException) + { + } + } + + /// + /// Attempts to complete a as faulted, forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The task completion source. May not be null. + /// The exception to bind to the task. + public static void TrySetExceptionWithBackgroundContinuations(this TaskCompletionSource @this, Exception exception) + { + // Set the result on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetException(exception)); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + try + { + @this.Task.Wait(); + } + catch (AggregateException) + { + } + } + + /// + /// Attempts to complete a as faulted, forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The type of the result of the asynchronous operation. + /// The task completion source. May not be null. + /// The exceptions to bind to the task. + public static void TrySetExceptionWithBackgroundContinuations(this TaskCompletionSource @this, IEnumerable exceptions) + { + // Complete on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetException(exceptions)); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + try + { + @this.Task.Wait(); + } + catch (AggregateException) + { + } + } + + /// + /// Attempts to complete a as faulted, forcing all continuations onto a threadpool thread even if they specified ExecuteSynchronously. + /// + /// The task completion source. May not be null. + /// The exceptions to bind to the task. + public static void TrySetExceptionWithBackgroundContinuations(this TaskCompletionSource @this, IEnumerable exceptions) + { + // Set the result on a threadpool thread, so any synchronous continuations will execute in the background. + TaskShim.Run(() => @this.TrySetException(exceptions)); + + // Wait for the TCS task to complete; note that the continuations may not be complete. + try + { + @this.Task.Wait(); + } + catch (AggregateException) + { + } + } + } +} diff --git a/ZeroLevel/Services/Async/TaskConstants.cs b/ZeroLevel/Services/Async/TaskConstants.cs new file mode 100644 index 0000000..4078d45 --- /dev/null +++ b/ZeroLevel/Services/Async/TaskConstants.cs @@ -0,0 +1,143 @@ +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// Provides completed task constants. + /// + public static class TaskConstants + { + private static readonly Task booleanTrue = TaskShim.FromResult(true); + private static readonly Task intNegativeOne = TaskShim.FromResult(-1); + + /// + /// A task that has been completed with the value true. + /// + public static Task BooleanTrue + { + get + { + return booleanTrue; + } + } + + /// + /// A task that has been completed with the value false. + /// + public static Task BooleanFalse + { + get + { + return TaskConstants.Default; + } + } + + /// + /// A task that has been completed with the value 0. + /// + public static Task Int32Zero + { + get + { + return TaskConstants.Default; + } + } + + /// + /// A task that has been completed with the value -1. + /// + public static Task Int32NegativeOne + { + get + { + return intNegativeOne; + } + } + + /// + /// A that has been completed. + /// + public static Task Completed + { + get + { + return booleanTrue; + } + } + + /// + /// A that will never complete. + /// + public static Task Never + { + get + { + return TaskConstants.Never; + } + } + + /// + /// A task that has been canceled. + /// + public static Task Canceled + { + get + { + return TaskConstants.Canceled; + } + } + } + + /// + /// Provides completed task constants. + /// + /// The type of the task result. + public static class TaskConstants + { + private static readonly Task defaultValue = TaskShim.FromResult(default(T)); + + private static readonly Task never = new TaskCompletionSource().Task; + + private static readonly Task canceled = CanceledTask(); + + private static Task CanceledTask() + { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } + + /// + /// A task that has been completed with the default value of . + /// + public static Task Default + { + get + { + return defaultValue; + } + } + + /// + /// A that will never complete. + /// + public static Task Never + { + get + { + return never; + } + } + + /// + /// A task that has been canceled. + /// + public static Task Canceled + { + get + { + return canceled; + } + } + } +} diff --git a/ZeroLevel/Services/Async/TaskExtensions.cs b/ZeroLevel/Services/Async/TaskExtensions.cs new file mode 100644 index 0000000..2f45d6a --- /dev/null +++ b/ZeroLevel/Services/Async/TaskExtensions.cs @@ -0,0 +1,155 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + /// + /// Provides synchronous extension methods for tasks. + /// + public static class TaskExtensions + { + /// + /// Asynchronously waits for the task to complete, or for the cancellation token to be canceled. + /// + /// The task to wait for. May not be null. + /// The cancellation token that cancels the wait. + public static Task WaitAsync(this Task @this, CancellationToken cancellationToken) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + if (!cancellationToken.CanBeCanceled) + return @this; + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + return DoWaitAsync(@this, cancellationToken); + } + + private static async Task DoWaitAsync(Task task, CancellationToken cancellationToken) + { + using (var cancelTaskSource = new CancellationTokenTaskSource(cancellationToken)) + await await Task.WhenAny(task, cancelTaskSource.Task).ConfigureAwait(false); + } + + /// + /// Asynchronously waits for the task to complete, or for the cancellation token to be canceled. + /// + /// The type of the task result. + /// The task to wait for. May not be null. + /// The cancellation token that cancels the wait. + public static Task WaitAsync(this Task @this, CancellationToken cancellationToken) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + if (!cancellationToken.CanBeCanceled) + return @this; + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + return DoWaitAsync(@this, cancellationToken); + } + + private static async Task DoWaitAsync(Task task, CancellationToken cancellationToken) + { + using (var cancelTaskSource = new CancellationTokenTaskSource(cancellationToken)) + return await await Task.WhenAny(task, cancelTaskSource.Task).ConfigureAwait(false); + } + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The task. May not be null. + public static void WaitAndUnwrapException(this Task task) + { + task.GetAwaiter().GetResult(); + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The was cancelled before the completed, or the raised an . + public static void WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) + { + try + { + task.Wait(cancellationToken); + } + catch (AggregateException ex) + { + throw ExceptionHelpers.PrepareForRethrow(ex.InnerException); + } + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The type of the result of the task. + /// The task. May not be null. + /// The result of the task. + public static TResult WaitAndUnwrapException(this Task task) + { + return task.GetAwaiter().GetResult(); + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The type of the result of the task. + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The result of the task. + /// The was cancelled before the completed, or the raised an . + public static TResult WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) + { + try + { + task.Wait(cancellationToken); + return task.Result; + } + catch (AggregateException ex) + { + throw ExceptionHelpers.PrepareForRethrow(ex.InnerException); + } + } + + /// + /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. + /// + /// The task. May not be null. + public static void WaitWithoutException(this Task task) + { + // Check to see if it's completed first, so we don't cause unnecessary allocation of a WaitHandle. + if (task.IsCompleted) + { + return; + } + + var asyncResult = (IAsyncResult)task; + asyncResult.AsyncWaitHandle.WaitOne(); + } + + /// + /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. + /// + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The was cancelled before the completed. + public static void WaitWithoutException(this Task task, CancellationToken cancellationToken) + { + // Check to see if it's completed first, so we don't cause unnecessary allocation of a WaitHandle. + if (task.IsCompleted) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var index = WaitHandle.WaitAny(new[] { ((IAsyncResult)task).AsyncWaitHandle, cancellationToken.WaitHandle }); + if (index != 0) + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + } +} diff --git a/ZeroLevel/Services/Async/TaskShim.cs b/ZeroLevel/Services/Async/TaskShim.cs new file mode 100644 index 0000000..762804b --- /dev/null +++ b/ZeroLevel/Services/Async/TaskShim.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ZeroLevel.Services.Async +{ + internal static class TaskShim + { + public static Task Run(Action func) + { + return Task.Run(func); + } + + public static Task Run(Func func) + { + return Task.Run(func); + } + + public static Task Run(Func func) + { + return Task.Run(func); + } + + public static Task Run(Func> func) + { + return Task.Run(func); + } + + public static Task FromResult(T value) + { + return Task.FromResult(value); + } + + public static Task WhenAll(IEnumerable> tasks) + { + return Task.WhenAll(tasks); + } + + public static Task WhenAll(params Task[] tasks) + { + return Task.WhenAll(tasks); + } + + public static Task WhenAll(params Task[] tasks) + { + return Task.WhenAll(tasks); + } + + public static Task WhenAll(IEnumerable tasks) + { + return Task.WhenAll(tasks); + } + } +} diff --git a/ZeroLevel/Services/Collections/EverythingStorage.cs b/ZeroLevel/Services/Collections/EverythingStorage.cs new file mode 100644 index 0000000..60d82de --- /dev/null +++ b/ZeroLevel/Services/Collections/EverythingStorage.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using ZeroLevel.Services.Invokation; + +namespace ZeroLevel.Services.Collections +{ + public class EverythingStorage : + IEverythingStorage + { + private class ConcreteTypeRepository + { + private readonly IInvokeWrapper _wrapper; + + private readonly Invoker _insert; + private readonly Invoker _containsKey; + private readonly Invoker _remove; + private readonly Invoker _getter; + private readonly object _instance; + + public ConcreteTypeRepository(Type entityType) + { + _wrapper = InvokeWrapper.Create(); + var genericType = typeof(Dictionary<,>); + var instanceType = genericType.MakeGenericType(new Type[] { typeof(string), entityType }); + _instance = Activator.CreateInstance(instanceType); + + var insert_key = _wrapper.Configure(instanceType, "Insert").Single(); + _insert = _wrapper.GetInvoker(insert_key); + + var contains_key = _wrapper.Configure(instanceType, "ContainsKey").Single(); + _containsKey = _wrapper.GetInvoker(contains_key); + + var remove_key = _wrapper.Configure(instanceType, "Remove").Single(); + _remove = _wrapper.GetInvoker(remove_key); + + var p = instanceType.GetProperty("Item", entityType); + var getter = p.GetGetMethod(); + var get_key = _wrapper.Configure(getter); + _getter = _wrapper.GetInvoker(get_key); + } + + public void Insert(string key, T entity) + { + _insert.Invoke(_instance, new object[] { key, entity, true }); + } + + public void InsertOrUpdate(string key, T entity) + { + if ((bool)_containsKey.Invoke(_instance, key)) + _remove.Invoke(_instance, key); + _insert.Invoke(_instance, new object[] { key, entity, true }); + } + + public bool ContainsKey(string key) + { + return (bool)_containsKey.Invoke(_instance, key); + } + + public void Remove(string key) + { + _remove.Invoke(_instance, key); + } + + public T Get(string key) + { + return (T)_getter.Invoke(_instance, key); + } + } + private readonly ConcurrentDictionary _shardedRepositories = + new ConcurrentDictionary(); + + private ConcreteTypeRepository this[Type type] + { + get + { + if (_shardedRepositories.ContainsKey(type) == false) + { + var r = new ConcreteTypeRepository(type); + _shardedRepositories.AddOrUpdate(type, r, (t, old) => old); + } + return _shardedRepositories[type]; + } + } + + public bool TryAdd(string key, T value) + { + try + { + this[typeof(T)].Insert(key, value); + return true; + } + catch + { } + return false; + } + + public bool ContainsKey(string key) + { + return this[typeof(T)].ContainsKey(key); + } + + public bool TryRemove(string key) + { + try + { + this[typeof(T)].Remove(key); + return true; + } + catch + { } + return false; + } + + public void Add(string key, T value) + { + this[typeof(T)].Insert(key, value); + } + + public void Remove(string key) + { + this[typeof(T)].Remove(key); + } + + public T Get(string key) + { + return this[typeof(T)].Get(key); + } + + public static IEverythingStorage Create() + { + return new EverythingStorage(); + } + + public void AddOrUpdate(string key, T value) + { + this[typeof(T)].InsertOrUpdate(key, value); + } + } +} diff --git a/ZeroLevel/Services/Collections/FixSizeQueue.cs b/ZeroLevel/Services/Collections/FixSizeQueue.cs new file mode 100644 index 0000000..568946c --- /dev/null +++ b/ZeroLevel/Services/Collections/FixSizeQueue.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; + +namespace ZeroLevel.Services.Collections +{ + /// + /// Очередь фиксированной длины + /// + public sealed class FixSizeQueue : + IFixSizeQueue + { + private readonly T[] _array; + private long _nextIndex; + private long _startIndex; + private long _count; + private readonly object _accessLocker = new object(); + + public FixSizeQueue(long capacity) + { + if (capacity <= 0) + { + capacity = 1024; + } + _array = new T[capacity]; + _nextIndex = 0; + _count = 0; + } + /// + /// Добавление элемента в очередь, при достижении предела по размеру, + /// перезаписывается самый старый элемент + /// + public void Push(T item) + { + lock (_accessLocker) + { + _array[_nextIndex] = item; + _nextIndex = (_nextIndex + 1) % _array.Length; + if (_count < _array.Length) + { + _count++; + } + else + { + _startIndex = (_startIndex + 1) % _array.Length; + } + } + } + + public bool Equals(T x, T y) + { + if (x == null && y == null) return true; + if ((object)x == (object)y) return true; + if (x == null || y == null) return false; + if (ReferenceEquals(x, y)) return true; + return x.Equals(y); + } + + public bool Contains(T item, IComparer comparer = null) + { + lock (_accessLocker) + { + Func eq_func; + if (comparer == null) + { + eq_func = Equals; + } + else + { + eq_func = (x, y) => comparer.Compare(x, y) == 0; + } + var cursor = _startIndex; + if (_count > 0) + { + do + { + if (eq_func(_array[cursor], item)) + return true; + cursor = (cursor + 1) % _array.Length; + } while (cursor != _nextIndex); + } + } + return false; + } + + public long Count + { + get + { + return _count; + } + } + + public bool TryTake(out T t) + { + lock (_accessLocker) + { + if (_count > 0) + { + t = _array[_startIndex]; + _array[_startIndex] = default(T); + _startIndex = (_startIndex + 1) % _array.Length; + _count--; + return true; + } + } + t = default(T); + return false; + } + + public T Take() + { + T ret; + lock (_accessLocker) + { + if (_count > 0) + { + ret = _array[_startIndex]; + _array[_startIndex] = default(T); + _startIndex = (_startIndex + 1) % _array.Length; + _count--; + return ret; + } + } + throw new System.Exception("Collection is empty"); + } + + public IEnumerable Dump() + { + var dump = new List(); + lock (_accessLocker) + { + var cursor = _startIndex; + if (_count > 0) + { + do + { + dump.Add(_array[cursor]); + cursor = (cursor + 1) % _array.Length; + } while (cursor != _nextIndex); + } + } + return dump; + } + } +} diff --git a/ZeroLevel/Services/Collections/IEverythingStorage.cs b/ZeroLevel/Services/Collections/IEverythingStorage.cs new file mode 100644 index 0000000..c5343b7 --- /dev/null +++ b/ZeroLevel/Services/Collections/IEverythingStorage.cs @@ -0,0 +1,13 @@ +namespace ZeroLevel.Services.Collections +{ + public interface IEverythingStorage + { + bool TryAdd(string key, T value); + bool ContainsKey(string key); + bool TryRemove(string key); + void Add(string key, T value); + void AddOrUpdate(string key, T value); + void Remove(string key); + T Get(string key); + } +} diff --git a/ZeroLevel/Services/Collections/IFixSizeQueue.cs b/ZeroLevel/Services/Collections/IFixSizeQueue.cs new file mode 100644 index 0000000..0fb1aee --- /dev/null +++ b/ZeroLevel/Services/Collections/IFixSizeQueue.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace ZeroLevel.Services.Collections +{ + public interface IFixSizeQueue + { + void Push(T item); + long Count { get; } + bool TryTake(out T t); + T Take(); + IEnumerable Dump(); + bool Contains(T item, IComparer comparer = null); + } +} diff --git a/ZeroLevel/Services/Collections/RoundRobinCollection.cs b/ZeroLevel/Services/Collections/RoundRobinCollection.cs new file mode 100644 index 0000000..b51ac57 --- /dev/null +++ b/ZeroLevel/Services/Collections/RoundRobinCollection.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace ZeroLevel.Services.Collections +{ + public sealed class RoundRobinCollection : + IDisposable + { + private readonly List _collection = + new List(); + private int _index = -1; + private readonly ReaderWriterLockSlim _lock = + new ReaderWriterLockSlim(); + + public int Count { get { return _collection.Count; } } + + public void Add(T item) + { + _lock.EnterWriteLock(); + try + { + _collection.Add(item); + if (_index == -1) _index = 0; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void Remove(T item) + { + _lock.EnterWriteLock(); + try + { + _collection.Remove(item); + if (_index >= _collection.Count) + { + if (_collection.Count == 0) _index = -1; + else _index = 0; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool Contains(T item) + { + _lock.EnterReadLock(); + try + { + return _collection.Contains(item); + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool MoveNext() + { + _lock.EnterReadLock(); + try + { + if (_collection.Count > 0) + { + _index = Interlocked.Increment(ref _index) % _collection.Count; + return true; + } + } + finally + { + _lock.ExitReadLock(); + } + return false; + } + + public T Current + { + get + { + return _index == -1 ? default(T) : _collection[_index]; + } + } + + public IEnumerable Source { get { return _collection; } } + + public IEnumerable GetCurrentSeq() + { + _lock.EnterReadLock(); + try + { + var arr = new T[_collection.Count]; + int p = 0; + for (int i = _index; i < _collection.Count; i++, p++) + { + arr[p] = _collection[i]; + } + for (int i = 0; i < _index; i++, p++) + { + arr[p] = _collection[i]; + } + return arr; + } + finally + { + _lock.ExitReadLock(); + } + } + + public IEnumerable Find(Func selector) + { + return _collection.Where(selector); + } + + public void Clear() + { + _collection.Clear(); + _index = -1; + } + + public void Dispose() + { + _collection.Clear(); + _lock.Dispose(); + } + } +} diff --git a/ZeroLevel/Services/Collections/SparseIterator.cs b/ZeroLevel/Services/Collections/SparseIterator.cs new file mode 100644 index 0000000..b723d41 --- /dev/null +++ b/ZeroLevel/Services/Collections/SparseIterator.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ZeroLevel.Services.Collections +{ + /// + /// Циклический разреженный итератор + /// позволяет выполнять циклический обход массива, с возможностью отмечать элементы + /// которые требуется прпускать при следующих обходах. + /// + public class SparseIterator + { + private readonly T[] _array; + private readonly HashSet _removed = new HashSet(); + private int index = -1; + + public SparseIterator(IEnumerable items) + { + _array = items.ToArray(); + } + /// + /// Текущий элемент последовательности + /// + public T Current + { + get + { + if (index >= 0 && index < _array.Length) + { + return _array[index]; + } + throw new IndexOutOfRangeException(); + } + } + /// + /// Указывает на отсутствие элементов в последовательности или на + /// то что все элементы были отмечены для пропуска + /// + public bool IsEmpty + { + get + { + return _array.Length == 0 || _array.Length == _removed.Count; + } + } + /// + /// Сдвиг на следующий элемент, если достигнут конец последовательности, + /// переключается на первый неотмеченный для пропуска элемент + /// + /// вернет -1 если последовательность пуста, или если не осталось элементов не отмеченных для пропуска + public int MoveNext() + { + do + { + index++; + } while (_removed.Contains(index)); + if (index >= _array.Length) + { + if (IsEmpty) return -1; + index = -1; + do + { + index++; + } while (_removed.Contains(index)); + } + return index; + } + /// + /// Отмечает текущий элемент для пропуска при следующем обходе + /// + /// + public bool Exclude() + { + if (index >= 0) + { + return _removed.Add(index); + } + return false; + } + } +} diff --git a/ZeroLevel/Services/Config/BaseConfiguration.cs b/ZeroLevel/Services/Config/BaseConfiguration.cs new file mode 100644 index 0000000..9e5e811 --- /dev/null +++ b/ZeroLevel/Services/Config/BaseConfiguration.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using ZeroLevel.Services.Reflection; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.Services.Config +{ + /// + /// Упрощенная конфигурация, без разделения параметров по секциям + /// + internal sealed class BaseConfiguration : + IConfiguration + { + #region Private members + /// + /// Указывает на заморозку конфигурации, все изменения запрещены + /// + private bool _freezed = false; + /// + /// Указывает на перманентную заморозку конфигурации, разморозка запрещена + /// + private bool _permanentFreezed = false; + private readonly object _freezeLock = new object(); + /// + /// Список вида ключ-значение + /// + private readonly ConcurrentDictionary> _keyValues = new ConcurrentDictionary>(); + /// + /// Пустой список + /// + private static readonly IEnumerable EmptyValuesList = new List(0); + + private static string GetKey(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + return key.Trim().ToLower(CultureInfo.InvariantCulture); + } + #endregion + + #region Properties + /// + /// Список значений по ключу + /// + /// Ключ + /// Список значений + public IEnumerable this[string key] + { + get + { + key = key.ToLower(CultureInfo.CurrentCulture); + IList result; + if (_keyValues.TryGetValue(key, out result)) + { + return result; + } + return EmptyValuesList; + } + } + /// + /// Список ключей + /// + public IEnumerable Keys + { + get { return _keyValues.Keys; } + } + + public bool Freezed + { + get + { + return _freezed; + } + } + #endregion + + #region Public methods + + #region Get + /// + /// Получение списка значение соотвествующих указанному ключу + /// + /// Ключ + /// Список значений + public IEnumerable Items(string key) + { + return this[key]; + } + /// + /// Получение первого значения для указанного ключа + /// + /// Ключ + /// Первое значение, или null если ключ есть, но нет значений, или KeyNotFoundException если нет ключа + public string First(string key) + { + IList result; + if (_keyValues.TryGetValue(GetKey(key), out result)) + { + if (result.Count > 0) + return result[0]; + return null; + } + throw new KeyNotFoundException("Key not found: " + key); + } + + public void DoWithFirst(string key, Action action) + { + if (Contains(key)) + { + action(First(key)); + } + } + + public void DoWithFirst(string key, Action action) + { + if (Contains(key)) + { + action(First(key)); + } + } + + /// + /// Получение первого значения для указанного ключа, с попыткой преобразования в указанный тип + /// + /// Ожидаемый тип + /// Ключ + /// Первое значение, или default(T) если ключ есть, но нет значений, или KeyNotFoundException если нет ключа + public T First(string key) + { + IList result; + if (_keyValues.TryGetValue(GetKey(key), out result)) + { + if (result.Count > 0) + return (T)StringToTypeConverter.TryConvert(result[0], typeof(T)); + return default(T); + } + throw new KeyNotFoundException("Parameter not found: " + key); + } + /// + /// Получение первого значения для указанного ключа, или значения по умолчанию + /// + /// Ключ + /// Значение по умолчанию + /// Первое значение, или значение по умолчанию если нет значений или ключа + public string FirstOrDefault(string key, string defaultValue) + { + IList result; + if (_keyValues.TryGetValue(GetKey(key), out result)) + { + if (result.Count > 0) + return result[0]; + } + return defaultValue; + } + /// + /// Получение первого значения для указанного ключа, или значения по умолчанию, с попыткой преобразования в указанный тип + /// + /// Ожидаемый тип + /// Ключ + /// Первое значение, или default(T) если нет значений или ключа + public T FirstOrDefault(string key) + { + IList result; + if (_keyValues.TryGetValue(GetKey(key), out result)) + { + if (result.Count > 0) + return (T)StringToTypeConverter.TryConvert(result[0], typeof(T)); + } + return default(T); + } + /// + /// Получение первого значения для указанного ключа, или значения по умолчанию, с попыткой преобразования в указанный тип + /// + /// Ожидаемый тип + /// Ключ + /// Значение по умолчанию + /// Первое значение, или значение по умолчанию если нет значений или ключа + public T FirstOrDefault(string key, T defaultValue) + { + IList result; + if (_keyValues.TryGetValue(GetKey(key), out result)) + { + if (result.Count > 0) + return (T)StringToTypeConverter.TryConvert(result[0], typeof(T)); + } + return defaultValue; + } + /// + /// Проверка наличия ключа и непустого списка связанных с ним значений + /// + /// Ключ + /// true - если существует ключ и есть хотя бы одно значение + public bool Contains(string key) + { + key = GetKey(key); + return _keyValues.ContainsKey(key) && _keyValues[key].Count > 0; + } + /// + /// Проверка наличия одного из ключей + /// + public bool Contains(params string[] keys) + { + foreach (var key in keys) + if (Contains(key)) return true; + return false; + } + /// + /// Проверка наличия ключа и связанного с ним значения + /// + /// + /// + /// + public bool ContainsValue(string key, string value) + { + IList result; + if (_keyValues.TryGetValue(GetKey(key), out result)) + { + return result.Contains(value); + } + return false; + } + /// + /// Количество значений связанных с указанным ключом + /// + /// Ключ + /// Количество значений + public int Count(string key) + { + key = GetKey(key); + if (_keyValues.ContainsKey(key)) + { + return _keyValues[key].Count; + } + return 0; + } + #endregion + + /// + /// Добавление ключа и связанного с ним значения + /// + /// Ключ + /// Значение + public IConfiguration Append(string key, string value) + { + if (false == _freezed) + { + key = GetKey(key); + if (false == _keyValues.ContainsKey(key)) + { + _keyValues.TryAdd(key, new List()); + } + _keyValues[key].Add(value?.Trim() ?? null); + } + return this; + } + /// + /// Задает значение в единственном числе, + /// существующее значение будет перезаписано + /// + public IConfiguration SetUnique(string key, string value) + { + if (false == _freezed) + { + key = GetKey(key); + if (false == _keyValues.ContainsKey(key)) + { + _keyValues.TryAdd(key, new List()); + } + _keyValues[key].Clear(); + _keyValues[key].Add(value?.Trim() ?? null); + } + return this; + } + /// + /// Очистка связанного с ключом списка значений + /// + /// Ключ + public IConfiguration Clear(string key) + { + if (false == _freezed) + { + key = GetKey(key); + if (_keyValues.ContainsKey(key)) + { + _keyValues[key].Clear(); + } + } + return this; + } + /// + /// Очистка конфигурации + /// + public IConfiguration Clear() + { + if (false == _freezed) + { + _keyValues.Clear(); + } + return this; + } + /// + /// Удаление ключа и связанных с ним значений + /// + /// Ключ + public IConfiguration Remove(string key) + { + if (false == _freezed) + { + IList removed; + _keyValues.TryRemove(GetKey(key), out removed); + } + return this; + } + + public bool Freeze(bool permanent = false) + { + lock (_freezeLock) + { + if (false == _freezed) + { + _freezed = true; + _permanentFreezed = permanent; + return true; + } + else if (_permanentFreezed == false && permanent) + { + _permanentFreezed = true; + return true; + } + return false; + } + } + + public bool Unfreeze() + { + lock (_freezeLock) + { + if (_freezed && _permanentFreezed == false) + { + _freezed = false; + return true; + } + return false; + } + } + #endregion + + #region IEquatable + public bool Equals(IConfiguration other) + { + if (other == null) + { + return false; + } + if (this.Keys.NoOrderingEquals(other.Keys) == false) + { + return false; + } + foreach (var key in Keys) + { + if (this[key].NoOrderingEquals(other[key]) == false) + { + return false; + } + } + return true; + } + #endregion + + #region Binary Serializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteBoolean(this._freezed); + writer.WriteBoolean(this._permanentFreezed); + writer.WriteInt32(_keyValues.Count); + foreach (var pair in _keyValues) + { + writer.WriteString(pair.Key); + writer.WriteInt32(pair.Value.Count); + foreach (var value in pair.Value) + { + writer.WriteString(value); + } + } + } + + public void Deserialize(IBinaryReader reader) + { + this._freezed = reader.ReadBoolean(); + this._permanentFreezed = reader.ReadBoolean(); + var count = reader.ReadInt32(); + _keyValues.Clear(); + for (int i = 0; i < count; i++) + { + var key = reader.ReadString(); + var count_values = reader.ReadInt32(); + var list_values = new List(); + for (var k = 0; k < count_values; k++) + { + list_values.Add(reader.ReadString()); + } + _keyValues.TryAdd(key, list_values); + } + } + #endregion + } +} diff --git a/ZeroLevel/Services/Config/BaseConfigurationSet.cs b/ZeroLevel/Services/Config/BaseConfigurationSet.cs new file mode 100644 index 0000000..9890308 --- /dev/null +++ b/ZeroLevel/Services/Config/BaseConfigurationSet.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.Services.Config +{ + /// + /// Наборы именованых конфигураций (секций) + /// + internal sealed class BaseConfigurationSet : + IConfigurationSet + { + #region Private members + /// + /// Список секций + /// + private readonly ConcurrentDictionary _sections = new ConcurrentDictionary(); + + private static string GetKey(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException(nameof(key)); + } + return key.Trim().ToLower(CultureInfo.InvariantCulture); + } + #endregion + + #region Properties + public IConfiguration Default + { + get { return _sections[Configuration.DEFAULT_SECTION_NAME]; } + } + + public IConfiguration this[string sectionName] + { + get + { + return CreateSection(sectionName); + } + } + + public IEnumerable SectionNames + { + get { return _sections.Keys; } + } + + public IEnumerable Sections + { + get { return _sections.Values; } + } + + public bool SectionsFreezed + { + get + { + return _sectionsFreezed; + } + } + + #endregion + + #region Methods + public BaseConfigurationSet() + { + CreateSection(Configuration.DEFAULT_SECTION_NAME); + } + + public BaseConfigurationSet(IConfiguration defaultConfiguration) + { + _sections.TryAdd(Configuration.DEFAULT_SECTION_NAME, defaultConfiguration); + } + + public IConfiguration CreateSection(string sectionName) + { + var key = GetKey(sectionName); + IConfiguration exists; + if (_sections.TryGetValue(key, out exists)) + { + return exists; + } + else if (false == _sectionsFreezed) + { + _sections.TryAdd(key, new BaseConfiguration()); + return _sections[key]; + } + throw new Exception("Sections change freezed"); + } + + public IConfiguration GetSection(string sectionName) + { + var key = GetKey(sectionName); + IConfiguration exists; + if (_sections.TryGetValue(key, out exists)) + { + return exists; + } + throw new KeyNotFoundException("Section not found: " + sectionName); + } + + public bool ContainsSection(string sectionName) + { + var key = GetKey(sectionName); + return _sections.ContainsKey(key); + } + + public bool RemoveSection(string sectionName) + { + if (false == _sectionsFreezed) + { + var key = GetKey(sectionName); + if (_sections.ContainsKey(key)) + { + IConfiguration removed; + return _sections.TryRemove(key, out removed); + } + } + return false; + } + #endregion + + #region IEquatable + public bool Equals(IConfigurationSet other) + { + if (other == null) return false; + return this.SectionNames.NoOrderingEquals(other.SectionNames) && + this.Sections.NoOrderingEquals(other.Sections); + } + #endregion + + #region Freezing + private readonly object _freezeLock = new object(); + + public bool FreezeConfiguration(bool permanent = false) + { + bool result = false; + lock (_freezeLock) + { + foreach (var s in _sections) + { + result |= s.Value.Freeze(permanent); + } + } + return result || FreezeSections(permanent); + } + + public bool UnfreezeConfiguration() + { + bool result = false; + lock (_freezeLock) + { + foreach (var s in _sections) + { + result |= s.Value.Unfreeze(); + } + } + return result || UnfreezeSections(); + } + + private bool _sectionsFreezed = false; + private bool _permanentSectionsFreezed = false; + + public bool FreezeSections(bool permanent = false) + { + lock (_freezeLock) + { + if (false == _sectionsFreezed) + { + _sectionsFreezed = true; + _permanentSectionsFreezed = permanent; + return true; + } + else if (_permanentSectionsFreezed == false && permanent) + { + _permanentSectionsFreezed = true; + return true; + } + return false; + } + } + + public bool UnfreezeSections() + { + lock (_freezeLock) + { + if (_sectionsFreezed && _permanentSectionsFreezed == false) + { + _sectionsFreezed = false; + return true; + } + return false; + } + } + #endregion + + #region Binary Serializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteBoolean(this._sectionsFreezed); + writer.WriteBoolean(this._permanentSectionsFreezed); + writer.WriteInt32(_sections.Count); + foreach (var s in _sections) + { + writer.WriteString(s.Key); + writer.Write(s.Value); + } + } + + public void Deserialize(IBinaryReader reader) + { + this._sectionsFreezed = reader.ReadBoolean(); + this._permanentSectionsFreezed = reader.ReadBoolean(); + var count = reader.ReadInt32(); + _sections.Clear(); + for (int i = 0; i < count; i++) + { + var key = reader.ReadString(); + _sections.TryAdd(key, reader.Read()); + } + } + #endregion + } +} diff --git a/ZeroLevel/Services/Config/Configuration.cs b/ZeroLevel/Services/Config/Configuration.cs new file mode 100644 index 0000000..507fc3b --- /dev/null +++ b/ZeroLevel/Services/Config/Configuration.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using ZeroLevel.Services.Config; +using ZeroLevel.Services.Config.Implementation; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel +{ + public static class Configuration + { + /// + /// Путь к каталогу приложения + /// + public static string BaseDirectory = + Path.GetDirectoryName(Assembly.GetExecutingAssembly()?.CodeBase)?. + Replace("file:\\", string.Empty); + + public static string AppLocation = Assembly.GetEntryAssembly()?.Location; + + public const string DEFAULT_SECTION_NAME = "_defaultsection"; + + #region Ctor + static Configuration() + { + _empty = new BaseConfiguration(); + _emptySet = new BaseConfigurationSet(); + _empty.Freeze(true); + _emptySet.FreezeConfiguration(true); + } + #endregion + + #region Cachee + private static readonly IConfiguration _empty; + private static readonly IConfigurationSet _emptySet; + private static readonly ConcurrentDictionary _cachee = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _setCachee = new ConcurrentDictionary(); + + public static IConfiguration Empty { get { return _empty; } } + public static IConfigurationSet EmptySet { get { return _emptySet; } } + + public static IConfiguration Default { get; private set; } + public static IConfigurationSet DefaultSet { get; private set; } + + public static void Save(string name, IConfiguration configuration) + { + _cachee.AddOrUpdate(name, configuration, (oldKey, oldValue) => configuration); + } + + public static void Save(IConfiguration configuration) + { + if (Default == null) + { + Default = configuration; + } + else + { + throw new Exception("Default configuration set already"); + } + } + + public static void Save(string name, IConfigurationSet configurationSet) + { + _setCachee.AddOrUpdate(name, configurationSet, (oldKey, oldValue) => configurationSet); + } + + public static void Save(IConfigurationSet configuration) + { + if (DefaultSet == null) + { + DefaultSet = configuration; + } + else + { + throw new Exception("Default configurationset set already"); + } + } + + public static IConfiguration Get(string name) + { + IConfiguration result; + if (false == _cachee.TryGetValue(name, out result)) + { + throw new KeyNotFoundException(string.Format("Not found configuration '{0}'", name)); + } + return result; + } + + public static IConfigurationSet GetSet(string name) + { + IConfigurationSet result; + if (false == _setCachee.TryGetValue(name, out result)) + { + throw new KeyNotFoundException(string.Format("Not found configuration set '{0}'", name)); + } + return result; + } + #endregion + + #region Factory + public static IConfiguration Create() + { + return new BaseConfiguration(); + } + + public static IConfigurationSet CreateSet() + { + return new BaseConfigurationSet(); + } + + public static IConfigurationSet CreateSet(IConfiguration defaultConfiguration) + { + return new BaseConfigurationSet(defaultConfiguration); + } + #endregion + + #region Read configuration + /// + /// Создание конфигурации из секции AppSettings файла app.config или web.config + /// + /// Конфигурация + public static IConfiguration ReadFromApplicationConfig() { return new ApplicationConfigReader().ReadConfiguration(); } + /// + /// Создание конфигурации из секции AppSettings файла app.config или web.config, дополняется секцией 'ConnectionStrings' + /// + /// Конфигурация + public static IConfigurationSet ReadSetFromApplicationConfig() { return new ApplicationConfigReader().ReadConfigurationSet(); } + /// + /// Создание конфигурации из секции AppSettings файла app.config или web.config + /// + /// Конфигурация + public static IConfiguration ReadFromApplicationConfig(string configFilePath) { return new ApplicationConfigReader(configFilePath).ReadConfiguration(); } + /// + /// Создание конфигурации из секции AppSettings файла app.config или web.config, дополняется секцией 'ConnectionStrings' + /// + /// Конфигурация + public static IConfigurationSet ReadSetFromApplicationConfig(string configFilePath) { return new ApplicationConfigReader(configFilePath).ReadConfigurationSet(); } + /// + /// Создание конфигурации из ini файла + /// + /// Путь к ini-файлу + /// Конфигурация + public static IConfiguration ReadFromIniFile(string path) { return new IniFileReader(path).ReadConfiguration(); } + /// + /// Создание конфигурации из ini файла, с учетом секций + /// + /// Путь к ini-файлу + /// Конфигурация + public static IConfigurationSet ReadSetFromIniFile(string path) { return new IniFileReader(path).ReadConfigurationSet(); } + /// + /// Создание конфигурации из параметров командной строки + /// + /// Параметры командной строки + /// Конфигурация + public static IConfiguration ReadFromCommandLine(string[] args) { return new CommandLineReader(args).ReadConfiguration(); } + + public static IConfigurationSet ReadBinary(IBinaryReader reader) { return reader.Read(); } + #endregion + + #region Write configuration + /// + /// Запись простой конфигурации в ini-файл + /// + /// Конфигурация + /// Путь к ini-файлу + public static void WriteToIniFile(IConfiguration configuration, string path) { new IniFileWriter(path).WriteConfiguration(configuration); } + /// + /// Запись полной конфигурации в ini-файл + /// + /// Конфигурация + /// Путь к ini-файлу + public static void WriteSetToIniFile(IConfigurationSet configuration, string path) { new IniFileWriter(path).WriteConfigurationSet(configuration); } + #endregion + } +} diff --git a/ZeroLevel/Services/Config/IConfiguration.cs b/ZeroLevel/Services/Config/IConfiguration.cs new file mode 100644 index 0000000..2fcda9a --- /dev/null +++ b/ZeroLevel/Services/Config/IConfiguration.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel +{ + /// + /// Интерфейс конфигурационных данных + /// + public interface IConfiguration : + IEquatable, + IBinarySerializable + { + #region Properties + /// + /// Получение списка значений по ключу + /// + IEnumerable this[string key] { get; } + /// + /// Перечисление ключей + /// + IEnumerable Keys { get; } + /// + /// Указывает что конфигурация заблокирована на изменения + /// + bool Freezed { get; } + #endregion + + #region Methods + /// + /// Получение списка значений параметра по ключу + /// + /// Имя параметра + /// Список значений + IEnumerable Items(string key); + /// + /// Получение одного(первого) значения параметра по ключу + /// + string First(string key); + /// + /// Получить первое значение в виде объекта типа T + /// + T First(string key); + /// + /// Получить первое значение или значение по умолчанию + /// + string FirstOrDefault(string name, string defaultValue); + /// + /// Получить первое значение в виде объекта типа T или получить значение по умолчанию + /// + T FirstOrDefault(string name); + /// + /// Получить первое значение в виде объекта типа T или получить переданное значение по умолчанию + /// + T FirstOrDefault(string name, T defaultValue); + /// + /// Проверка наличия ключа + /// + bool Contains(string key); + /// + /// Проверка наличия одного из ключей + /// + bool Contains(params string[] keys); + /// + /// Проверка наличия значения по ключу + /// + bool ContainsValue(string key, string value); + /// + /// Количество значений параметра + /// + int Count(string key); + /// + /// Выполняет указанное действие только в случае если в конфигурации есть ключ + /// + void DoWithFirst(string key, Action action); + /// + /// Выполняет указанное действие только в случае если в конфигурации есть ключ + /// + void DoWithFirst(string key, Action action); + #endregion + + #region Create, Clean, Delete + /// + /// Очистка всей секции + /// + IConfiguration Clear(); + /// + /// Очистка значения ключа + /// + IConfiguration Clear(string key); + /// + /// Удаление ключа и значений + /// + IConfiguration Remove(string key); + /// + /// Добавление параметра + /// + IConfiguration Append(string key, string value); + /// + /// Задает значение в единственном числе, + /// существующее значение будет перезаписано + /// + IConfiguration SetUnique(string key, string value); + /// + /// Запрещает вносить какие-либо изменения в конфигурацию + /// + /// false - если уже установлен запрет + bool Freeze(bool permanent = false); + /// + /// Убирает запрет на внесение изменений в конфигурацию + /// + /// false - если запрет снят + bool Unfreeze(); + #endregion + } +} diff --git a/ZeroLevel/Services/Config/IConfigurationReader.cs b/ZeroLevel/Services/Config/IConfigurationReader.cs new file mode 100644 index 0000000..58a2885 --- /dev/null +++ b/ZeroLevel/Services/Config/IConfigurationReader.cs @@ -0,0 +1,8 @@ +namespace ZeroLevel.Services.Config +{ + public interface IConfigurationReader + { + IConfiguration ReadConfiguration(); + IConfigurationSet ReadConfigurationSet(); + } +} diff --git a/ZeroLevel/Services/Config/IConfigurationSet.cs b/ZeroLevel/Services/Config/IConfigurationSet.cs new file mode 100644 index 0000000..bc3a36b --- /dev/null +++ b/ZeroLevel/Services/Config/IConfigurationSet.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel +{ + /// + /// Интерфейс набора конфигурационных данных + /// + public interface IConfigurationSet : + IEquatable, + IBinarySerializable + { + #region Properties + /// + /// Получение конфигурации по умолчанию + /// + IConfiguration Default { get; } + /// + /// Получение конфигурации по имени + /// + IConfiguration this[string sectionName] { get; } + /// + /// Получение имен конфигураций + /// + IEnumerable SectionNames { get; } + /// + /// Получение всех конфигураций + /// + IEnumerable Sections { get; } + /// + /// Указывает, заблокирован или нет набор секций + /// + bool SectionsFreezed { get; } + #endregion + + #region Methods + /// + /// Создание секции параметров + /// + /// Название секции + IConfiguration CreateSection(string sectionName); + /// + /// Запрос секции данных по имени секции + /// + /// Название секции + /// Секция с данными + IConfiguration GetSection(string sectionName); + /// + /// Проверка наличия секции с указанным именем + /// + /// Название секции + /// true - секция существует + bool ContainsSection(string sectionName); + /// + /// Удаление секции + /// + /// Название секции + /// false - если секция уже удалена или не существует + bool RemoveSection(string sectionName); + /// + /// Запрещает вносить какие-либо изменения в существующую конфигурацию во всех секциях + /// а также менять набор секций + /// + /// false - если уже установлен запрет + bool FreezeConfiguration(bool permanent = false); + /// + /// Запрещает вносить какие-либо изменения в существующий набор секций + /// + /// false - если уже установлен запрет + bool FreezeSections(bool permanent = false); + /// + /// Убирает запрет на внесение изменений в существующую конфигурацию во всех секциях + /// а также разрешает менять набор секций + /// + /// false - если запрет снят + bool UnfreezeConfiguration(); + /// + /// Убирает запрет на внесение изменений в существующий набор секций + /// + /// false - если запрет снят + bool UnfreezeSections(); + #endregion + } +} diff --git a/ZeroLevel/Services/Config/IConfigurationWriter.cs b/ZeroLevel/Services/Config/IConfigurationWriter.cs new file mode 100644 index 0000000..18ff3ed --- /dev/null +++ b/ZeroLevel/Services/Config/IConfigurationWriter.cs @@ -0,0 +1,8 @@ +namespace ZeroLevel.Services.Config +{ + public interface IConfigurationWriter + { + void WriteConfiguration(IConfiguration configuration); + void WriteConfigurationSet(IConfigurationSet configuration); + } +} diff --git a/ZeroLevel/Services/Config/Implementation/AppWebConfigReader.cs b/ZeroLevel/Services/Config/Implementation/AppWebConfigReader.cs new file mode 100644 index 0000000..04bc4bd --- /dev/null +++ b/ZeroLevel/Services/Config/Implementation/AppWebConfigReader.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace ZeroLevel.Services.Config.Implementation +{ + internal sealed class AppWebConfigReader + { + private readonly string _configFilePath; + + internal AppWebConfigReader(string configFilePath = null) + { + if (configFilePath == null) + { + var appConfig = Path.Combine(Configuration.BaseDirectory, string.Format("{0}.config", System.AppDomain.CurrentDomain.FriendlyName)); + if (File.Exists(appConfig)) + { + _configFilePath = appConfig; + } + else + { + var webConfig = Path.Combine(Configuration.BaseDirectory, "web.config"); + if (File.Exists(webConfig)) + { + _configFilePath = webConfig; + } + else + { + _configFilePath = Directory.GetFiles(Configuration.BaseDirectory, "*.config").FirstOrDefault(); + } + } + } + else + { + if (configFilePath.IndexOf(':') < 0) + { + this._configFilePath = Path.Combine(Configuration.BaseDirectory, configFilePath); + } + else + { + this._configFilePath = configFilePath; + } + } + } + + internal IEnumerable GetSections() + { + if (_configFilePath != null) + { + var xdoc = XDocument.Load(_configFilePath); + var cs = xdoc.Descendants("connectionStrings"). + Select(x => x.Name.LocalName); + return xdoc.Descendants("section") + .Select(x => x.Attribute("name").Value).Union(cs); + } + return Enumerable.Empty(); + } + + internal IEnumerable> ReadSection(string sectionName) + { + if (_configFilePath != null) + { + var xdoc = XDocument.Load(_configFilePath); + return xdoc.Descendants(sectionName). + SelectMany(x => x.Nodes().Where(n => null != (n as XElement)).Select(n => + { + var xe = n as XElement; + return new Tuple(FindName(xe), FindValue(xe)); + })); + } + return Enumerable.Empty>(); + } + + private static string FindName(XElement n) + { + var attributes = n.Attributes(). + ToDictionary(i => i.Name.LocalName.ToLowerInvariant(), j => j.Value); + foreach (var v in new[] { "key", "name", "code", "id" }) + { + if (attributes.ContainsKey(v)) + return attributes[v]; + } + return n.Name.LocalName; + } + + private static string FindValue(XElement n) + { + var attributes = n.Attributes(). + ToDictionary(i => i.Name.LocalName.ToLowerInvariant(), j => j.Value); + foreach (var v in new[] { "value", "val", "file", "db", "connectionstring" }) + { + if (attributes.ContainsKey(v)) + return attributes[v]; + } + return n.Value; + } + + internal IEnumerable> ReadAppSettings() + { + return ReadSection("appSettings"); + } + } +} diff --git a/ZeroLevel/Services/Config/Implementation/ApplicationConfigReader.cs b/ZeroLevel/Services/Config/Implementation/ApplicationConfigReader.cs new file mode 100644 index 0000000..9279ae0 --- /dev/null +++ b/ZeroLevel/Services/Config/Implementation/ApplicationConfigReader.cs @@ -0,0 +1,39 @@ +namespace ZeroLevel.Services.Config.Implementation +{ + internal sealed class ApplicationConfigReader + : IConfigurationReader + { + private readonly AppWebConfigReader _reader; + + internal ApplicationConfigReader() { _reader = new AppWebConfigReader(); } + internal ApplicationConfigReader(string configFilePath) { _reader = new AppWebConfigReader(configFilePath); } + + public IConfiguration ReadConfiguration() + { + var result = Configuration.Create(); + foreach (var pair in _reader.ReadAppSettings()) + { + result.Append(pair.Item1, pair.Item2); + } + return result; + } + + public IConfigurationSet ReadConfigurationSet() + { + var result = Configuration.CreateSet(); + foreach (var pair in _reader.ReadAppSettings()) + { + result.Default.Append(pair.Item1, pair.Item2); + } + foreach (var section in _reader.GetSections()) + { + var sectionConfig = result.CreateSection(section); + foreach (var pair in _reader.ReadSection(section)) + { + sectionConfig.Append(pair.Item1, pair.Item2); + } + } + return result; + } + } +} diff --git a/ZeroLevel/Services/Config/Implementation/CommandLineReader.cs b/ZeroLevel/Services/Config/Implementation/CommandLineReader.cs new file mode 100644 index 0000000..7b8299b --- /dev/null +++ b/ZeroLevel/Services/Config/Implementation/CommandLineReader.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; + +namespace ZeroLevel.Services.Config.Implementation +{ + internal sealed class CommandLineReader + : IConfigurationReader + { + private readonly string[] _args; + + public CommandLineReader(string[] args) + { + _args = args; + } + + public IConfiguration ReadConfiguration() + { + var result = Configuration.Create(); + if (_args != null) + { + try + { + foreach (string arg in _args) + { + int index = arg.IndexOf('='); + string key; + string value; + if (index >= 0) + { + key = arg.Substring(0, index).TrimStart('/').Trim().ToLower(CultureInfo.CurrentCulture); + value = arg.Substring(index + 1, arg.Length - index - 1).Trim(' ', '"'); + } + else + { + key = arg.TrimStart('-', '/').Trim().ToLower(CultureInfo.CurrentCulture); + value = string.Empty; + } + result.Append(key, value); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("Не удалось создать конфигурацию из аргументов командной строки", ex); + } + } + return result; + } + + public IConfigurationSet ReadConfigurationSet() + { + return Configuration.CreateSet(ReadConfiguration()); + } + } +} diff --git a/ZeroLevel/Services/Config/Implementation/IniFileReader.cs b/ZeroLevel/Services/Config/Implementation/IniFileReader.cs new file mode 100644 index 0000000..cc636cc --- /dev/null +++ b/ZeroLevel/Services/Config/Implementation/IniFileReader.cs @@ -0,0 +1,125 @@ +using System; +using System.Globalization; +using System.IO; + +namespace ZeroLevel.Services.Config.Implementation +{ + /// + /// Чтение конфигурации из ini файла + /// + internal sealed class IniFileReader + : IConfigurationReader + { + private readonly string _iniPath; + + internal IniFileReader(string configPath) + { + if (String.IsNullOrWhiteSpace(configPath)) + throw new ArgumentNullException("configPath", "Не указан путь к конфигурационному файлу"); + if (!File.Exists(configPath)) + { + configPath = Path.Combine(Configuration.BaseDirectory, configPath); + if (!File.Exists(configPath)) + { + throw new FileNotFoundException("Не существует конфигурационный файл: " + configPath); + } + } + _iniPath = configPath; + } + + public IConfiguration ReadConfiguration() + { + var result = Configuration.Create(); + string sectionName = null; + foreach (var line in File.ReadAllLines(_iniPath)) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + int index = line.IndexOf('='); + string key; + string originalKey; + string value; + if (index >= 0) + { + originalKey = line.Substring(0, index).Trim(); + key = originalKey.ToLower(CultureInfo.InvariantCulture); + value = line.Substring(index + 1, line.Length - index - 1).Trim(); + } + else + { + originalKey = line.Trim(); + key = originalKey.ToLower(CultureInfo.InvariantCulture); + value = string.Empty; + } + if (key[0].Equals(';') || key[0].Equals('#')) + continue; + if (string.IsNullOrEmpty(value) && key[0].Equals('[') && key[key.Length - 1].Equals(']')) + { + sectionName = originalKey.Trim('[', ']'); + } + else + { + if (!string.IsNullOrEmpty(sectionName)) + { + result.Append(string.Format("{0}.{1}", sectionName, key), value); + } + else + { + result.Append(key, value); + } + } + } + return result; + } + + public IConfigurationSet ReadConfigurationSet() + { + var result = Configuration.CreateSet(); + string sectionName = null; + foreach (var line in File.ReadAllLines(_iniPath)) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + int index = line.IndexOf('='); + string key; + string originalKey; + string value; + if (index >= 0) + { + originalKey = line.Substring(0, index).Trim(); + key = originalKey.ToLower(CultureInfo.InvariantCulture); + value = line.Substring(index + 1, line.Length - index - 1).Trim(); + } + else + { + originalKey = line.Trim(); + key = originalKey.ToLower(CultureInfo.InvariantCulture); + value = string.Empty; + } + if (key[0].Equals(';') || key[0].Equals('#')) + continue; + if (string.IsNullOrEmpty(value) && key[0].Equals('[') && key[key.Length - 1].Equals(']')) + { + sectionName = originalKey.Trim('[', ']'); + } + else + { + if (!string.IsNullOrEmpty(sectionName)) + { + var currentSection = (false == result.ContainsSection(sectionName)) ? result.CreateSection(sectionName) : result.GetSection(sectionName); + currentSection.Append(key, value); + } + else + { + result.Default.Append(key, value); + } + } + } + return result; + } + } +} diff --git a/ZeroLevel/Services/Config/Implementation/IniFileWriter.cs b/ZeroLevel/Services/Config/Implementation/IniFileWriter.cs new file mode 100644 index 0000000..a57c033 --- /dev/null +++ b/ZeroLevel/Services/Config/Implementation/IniFileWriter.cs @@ -0,0 +1,79 @@ +using System.IO; + +namespace ZeroLevel.Services.Config.Implementation +{ + /// + /// Запись конфигурации в ini-файл + /// + public class IniFileWriter + : IConfigurationWriter + { + /// + /// Путь к ini-файлу + /// + private readonly string _iniPath; + /// + /// Инициализирует новый экземпляр класса + /// + /// Путь к ini-файлу + public IniFileWriter(string iniPath) + { + _iniPath = iniPath; + } + /// + /// Запись простой конфигурации + /// + /// Конфигурация + public void WriteConfiguration(IConfiguration configuration) + { + using (TextWriter writer = new StreamWriter(_iniPath, false)) + { + foreach (string key in configuration.Keys) + { + if (configuration.Count(key) > 0) + { + foreach (string value in configuration[key]) + { + writer.WriteLine(key.Trim() + "=" + value.Trim()); + } + } + else + { + writer.WriteLine(key.Trim()); + } + } + writer.Flush(); + } + } + /// + /// Запись конфигурации разбитой по секциям + /// + /// Конфигурация + public void WriteConfigurationSet(IConfigurationSet configuration) + { + using (TextWriter writer = new StreamWriter(_iniPath, false)) + { + foreach (string section in configuration.SectionNames) + { + if (false == section.Equals(Configuration.DEFAULT_SECTION_NAME, System.StringComparison.Ordinal)) + writer.WriteLine("[" + section + "]"); + foreach (string key in configuration[section].Keys) + { + if (configuration[section].Count(key) > 0) + { + foreach (string value in configuration[section][key]) + { + writer.WriteLine(key + "=" + value); + } + } + else + { + writer.WriteLine(key); + } + } + } + writer.Flush(); + } + } + } +} diff --git a/ZeroLevel/Services/DOM/Contracts/IContentReader.cs b/ZeroLevel/Services/DOM/Contracts/IContentReader.cs new file mode 100644 index 0000000..6dffcc4 --- /dev/null +++ b/ZeroLevel/Services/DOM/Contracts/IContentReader.cs @@ -0,0 +1,51 @@ +using ZeroLevel.DocumentObjectModel.Flow; + +namespace ZeroLevel.DocumentObjectModel +{ + public interface IContentReader + { + // Primitives + void ReadText(Text text); + void ReadQuote(Quote quote); + void ReadLink(Link link, int order); + void ReadImage(Image image, int order); + void ReadAudio(Audio audio, int order); + void ReadVideo(Video video, int order); + + // Containers + void EnterSection(Section section); + void LeaveSection(Section section); + + void EnterParagraph(Paragraph paragraph); + void LeaveParagraph(Paragraph paragraph); + + void EnterList(List list); + void EnterListItem(List list, IContentElement item, int order); + void LeaveListItem(List list, IContentElement item, int order); + void LeaveList(List list); + + void EnterTable(Table table); + void EnterColumns(Table table); + void ReadColumn(Table table, Column column, int order); + void LeaveColumns(Table table); + void EnterRow(Table table, Row row, int order); + void EnterRowCell(Table table, Row row, IContentElement cell, int order); + void LeaveRowCell(Table table, Row row, IContentElement cell, int order); + void LeaveRow(Table table, Row row, int order); + void LeaveTable(Table table); + + void EnterGallery(Gallery gallery); + void LeaveGallery(Gallery gallery); + + void EnterAudioplayer(Audioplayer player); + void LeaveAudioplayer(Audioplayer player); + + void EnterVideoplayer(Videoplayer player); + void LeaveVideoplayer(Videoplayer player); + + // Feature + void ReadForm(FormContent form); + + T Complete(); + } +} diff --git a/ZeroLevel/Services/DOM/Contracts/IMetadataReader.cs b/ZeroLevel/Services/DOM/Contracts/IMetadataReader.cs new file mode 100644 index 0000000..42c2103 --- /dev/null +++ b/ZeroLevel/Services/DOM/Contracts/IMetadataReader.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace ZeroLevel.DocumentObjectModel +{ + public interface IMetadataReader + { + void ReadId(Guid Id); + void ReadSummary(string summary); + void ReadHeader(string header); + + void EnterIdentifier(Identifier identifier); + void ReadVersion(int version); + void ReadTimestamp(string timestamp); + void ReadDateLabel(string datelabel); + void LeaveIdentifier(Identifier identifier); + + void EnterTagsBlock(TagMetadata tagBlock); + void EnterKeywords(IEnumerable keywords); + void ReadKeyword(string keyword, int order); + void LeaveKeywords(IEnumerable keywords); + void EnterPlaces(IEnumerable places); + void ReadPlace(Tag place, int order); + void LeavePlaces(IEnumerable places); + void EnterCompanies(IEnumerable companies); + void ReadCompany(Tag company, int order); + void LeaveCompanies(IEnumerable companies); + void EnterPersons(IEnumerable persons); + void ReadPerson(Tag person, int order); + void LeavePersons(IEnumerable persons); + void LeaveTagsBlock(TagMetadata tagBlock); + + void EnterDescriptioveBlock(DescriptiveMetadata metadata); + void ReadAuthors(string byline); + void ReadCopiright(string copyright); + void ReadCreated(DateTime created); + void ReadLanguage(string language); + void ReadPriority(Priority priority); + void ReadSource(Agency source); + void ReadPublisher(Agency publisher); + void ReadOriginal(Tag original); + void EnterHeaders(IEnumerable
headers); + void ReadHeader(Header header, int order); + void LeaveHeaders(IEnumerable
headers); + void LeaveDescriptioveBlock(DescriptiveMetadata metadata); + + void EnterAsides(IEnumerable asides); + void ReadAside(AsideContent aside, int order); + void LeaveAsides(IEnumerable asides); + + void EnterAssotiations(IEnumerable assotiations); + void ReadAssotiation(Assotiation assotiation, int order); + void LeaveAssotiations(IEnumerable assotiations); + + void EnterCategories(IEnumerable categories); + void ReadCategory(Category category, int order); + void LeaveCategories(IEnumerable categories); + + T Complete(); + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contexts/TBlockContext.cs b/ZeroLevel/Services/DOM/DSL/Contexts/TBlockContext.cs new file mode 100644 index 0000000..a23d7f0 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contexts/TBlockContext.cs @@ -0,0 +1,129 @@ +using DOM.DSL.Model; +using DOM.DSL.Services; +using DOM.DSL.Tokens; +using System; +using System.Collections.Generic; + +namespace DOM.DSL.Contexts +{ + internal class TBlockContext : + TContext + { + private readonly string _name; + private TToken _blockToken; + private List _tokens; + + public string Name { get { return _name; } } + + public TBlockContext(TContext parent, string name) + { + ParentContext = parent; + _name = name; + _tokens = new List(); + } + + public override void Read(TStringReader reader) + { + if (_name.Equals("if", StringComparison.OrdinalIgnoreCase) || + _name.Equals("for", StringComparison.OrdinalIgnoreCase)) + { + var spaces_count = reader.SkipSpaces(); + if (reader.Current.Equals(TChar.TokenStart)) + { + reader.Move(); + var name = reader.ReadIdentity(); + if (name.Equals(string.Format("end{0}", _name))) + { + reader.Move(name.Length); + return; + } + if (_elementNames.Contains(name)) + { + reader.Move(name.Length); + var elementContext = new TElementContext(this, name); + elementContext.Read(reader); + _blockToken = elementContext.Complete(); + var body = new TRootContext(this); + body.Read(reader); + _tokens.AddRange(body.Complete()); + } + else + { + _tokens.Add(new TTextToken { Text = "@" + _name + " @" + name }); + } + } + else + { + _tokens.Add(new TTextToken { Text = "@" + _name + new string(' ', spaces_count) }); + } + } + else if (_name.Equals("block", StringComparison.OrdinalIgnoreCase)) + { + /*----------------------------------------------------------------*/ + if (reader.Current.Equals(TChar.PropertyOrFuncStart)) + { + reader.Move(); + var name = reader.ReadIdentity(); + if (name.Equals(string.Format("end{0}", _name))) + { + reader.Move(name.Length); + return; + } + if (name.Equals("to", StringComparison.OrdinalIgnoreCase)) + { + reader.Move(name.Length); + if (reader.Current.Equals(TChar.FuncArgsStart)) + { + reader.Move(); + reader.SkipSpaces(); + var var_name = reader.ReadIdentity(); + if (string.IsNullOrWhiteSpace(var_name)) + { + return; + } + reader.Move(var_name.Length); + reader.SkipSpaces(); + if (reader.Current.Equals(TChar.FuncArgsEnd)) + { + reader.Move(); + reader.SkipSpaces(); + reader.SkipBreaks(); + _blockToken = new TTextToken { Text = var_name }; + var body = new TRootContext(this); + body.Read(reader); + _tokens.AddRange(body.Complete()); + } + } + } + } + /*----------------------------------------------------------------*/ + } + else if (_name.Equals("comm", StringComparison.OrdinalIgnoreCase)) + { + do + { + var offset = reader.FindOffsetTo(TChar.TokenStart); + if (offset == -1) return; + reader.Move(offset + 1); + var name = reader.ReadIdentity(); + if (name.Equals(string.Format("end{0}", _name))) + { + reader.Move(name.Length); + return; + } + } while (reader.EOF == false); + } + else + { + var body = new TRootContext(this); + body.Read(reader); + _tokens.AddRange(body.Complete()); + } + } + + public TToken Complete() + { + return new TBlockToken(_name, _blockToken, _tokens); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contexts/TContext.cs b/ZeroLevel/Services/DOM/DSL/Contexts/TContext.cs new file mode 100644 index 0000000..b54e058 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contexts/TContext.cs @@ -0,0 +1,27 @@ +using DOM.DSL.Services; +using System.Collections.Generic; + +namespace DOM.DSL.Contexts +{ + internal abstract class TContext + { + protected static HashSet _blockNames = new HashSet { "for", "if", "flow", "comm", "block" }; + protected static HashSet _endblockNames = new HashSet { "endfor", "endif", "endflow", "endcomm", "endblock" }; + + protected static HashSet _elementNames = new HashSet { "text", "now", "nowutc", "guid", + "id","summary","header","categories", "directions","author","copyright","created", + "lang","priority","source","publisher","meta","timestamp","datelabel", + "version","keywords","companies","persons","places", + "self", "order", "counter", "aside", "assotiations", + "list","listitem","text","link","image","quote", + "form","video","audio","section","paragraph","table", + "columns","column","tablerow","tablecell","videoplayer","audioplayer", + "gallery", "content", "system", "buf", "build", + "env", "var", "identifier", "desc", "descriptive", "headers", "tags", + "null", "empty", "utc" + }; + + public TContext ParentContext { get; protected set; } + public abstract void Read(TStringReader reader); + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contexts/TElementContext.cs b/ZeroLevel/Services/DOM/DSL/Contexts/TElementContext.cs new file mode 100644 index 0000000..d56e4ca --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contexts/TElementContext.cs @@ -0,0 +1,78 @@ +using DOM.DSL.Model; +using DOM.DSL.Services; +using DOM.DSL.Tokens; + +namespace DOM.DSL.Contexts +{ + internal class TElementContext : + TContext + { + private readonly string _name; + private TToken _next; + + public TElementContext(TContext parent, string name) + { + ParentContext = parent; + _name = name; + } + + public override void Read(TStringReader reader) + { + if (reader.EOF == false && reader.Current == TChar.PropertyOrFuncStart) + { + if (reader.Move()) + { + reader.SkipBreaks(); + var name = reader.ReadIdentity(); + if (false == string.IsNullOrWhiteSpace(name)) + { + reader.Move(name.Length); + if (this._name.Equals("system")) + { + // Like a '@system.ignorespace' + if (reader.Current == TChar.FuncArgsStart) + { + reader.Move(); + reader.SkipBreaks(); + var context = new TFunctionContext(this, name); + context.Read(reader); + _next = new TSystemToken { Command = name, Arg = context.Complete() }; + } + else + { + _next = new TSystemToken { Command = name, Arg = null }; + } + } + else + { + if (reader.Current == TChar.FuncArgsStart) + { + // Function '@now.format(dd-mm)' + reader.Move(); + var context = new TFunctionContext(this, name); + context.Read(reader); + _next = context.Complete(); + } + else + { + // Property '@now.year' + var context = new TPropertyContext(this, name); + context.Read(reader); + _next = context.Complete(); + } + } + } + else + { + _next = new TTextToken { Text = TChar.PropertyOrFuncStart + name }; + } + } + } + } + + public TToken Complete() + { + return new TElementToken { ElementName = _name, NextToken = _next?.Clone() }; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contexts/TFunctionContext.cs b/ZeroLevel/Services/DOM/DSL/Contexts/TFunctionContext.cs new file mode 100644 index 0000000..001785d --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contexts/TFunctionContext.cs @@ -0,0 +1,187 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using DOM.DSL.Tokens; +using DOM.DSL.Services; +using DOM.DSL.Model; + +namespace DOM.DSL.Contexts +{ + internal class TFunctionContext : + TContext + { + private string _name; + private List _argTokens; + private TToken _nextToken; + + public TFunctionContext(TContext parent, string name) + { + ParentContext = parent; + _argTokens = new List(); + _name = name; + } + + public override void Read(TStringReader reader) + { + var text = new StringBuilder(); + var argTokens = new List(); + var flushTextToken = new Action(() => + { + if (text.Length > 0) + { + argTokens.Add(new TTextToken { Text = text.ToString() }); + text.Clear(); + } + }); + var flushArgToken = new Action(() => + { + if (argTokens.Count > 0) + { + _argTokens.Add(new TBlockToken("", null, argTokens.Select(t => t.Clone()).ToArray())); + argTokens.Clear(); + } + }); + while (reader.EOF == false) + { + switch (reader.Current) + { + #region Ecsaping + case TChar.Escape: + { + switch (reader.Next) + { + case 's': + text.Append(' '); + reader.Move(2); + break; + case 'r': + text.Append(TChar.CaretReturn); + reader.Move(2); + break; + case 'n': + text.Append(TChar.Newline); + reader.Move(2); + break; + case 't': + text.Append(TChar.Tab); + reader.Move(2); + break; + case '@': + case '(': + case ')': + case '.': + case ',': + case '\\': + text.Append(reader.Next); + reader.Move(2); + break; + default: + text.Append(reader.Current); + reader.Move(); + break; + } + } + break; + #endregion + + case TChar.FuncArgsEnd: + { + flushTextToken(); + flushArgToken(); + if (reader.Next == TChar.PropertyOrFuncStart) + { + if (reader.Move(2)) + { + reader.SkipBreaks(); + var name = reader.ReadIdentity(); + if (false == string.IsNullOrWhiteSpace(name)) + { + reader.Move(name.Length); + if (reader.Current == TChar.FuncArgsStart) + { + // Function '@now.format(dd-mm)' + reader.Move(); + var context = new TFunctionContext(this, name); + context.Read(reader); + _nextToken = context.Complete(); + } + else + { + // Property '@now.year' + var context = new TPropertyContext(this, name); + context.Read(reader); + _nextToken = context.Complete(); + } + } + } + } + else + { + reader.Move(); + } + } + return; + + case TChar.FuncArgsSeparator: + flushTextToken(); + flushArgToken(); + reader.Move(); + break; + + case TChar.TokenStart: + { + if (reader.Move()) + { + var name = reader.ReadIdentity(); + if (_elementNames.Contains(name)) + { + flushTextToken(); + reader.Move(name.Length); + var elementContext = new TElementContext(this, name); + elementContext.Read(reader); + argTokens.Add(elementContext.Complete()); + } + else if (_blockNames.Contains(name)) + { + flushTextToken(); + reader.Move(name.Length); + var blockContext = new TBlockContext(this, name); + blockContext.Read(reader); + argTokens.Add(blockContext.Complete()); + } + else + { + text.Append(TChar.TokenStart); + text.Append(reader.Current); + } + } + else + { + text.Append(reader.Current); + } + } + break; + default: + { + text.Append(reader.Current); + reader.Move(); + } + break; + } + } + flushTextToken(); + flushArgToken(); + } + + public TToken Complete() + { + return new TFunctionToken + { + FunctionName = _name, + NextToken = _nextToken?.Clone(), + FunctionArgs = _argTokens?.Select(t => t.Clone()) + }; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contexts/TPropertyContext.cs b/ZeroLevel/Services/DOM/DSL/Contexts/TPropertyContext.cs new file mode 100644 index 0000000..aef059a --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contexts/TPropertyContext.cs @@ -0,0 +1,185 @@ +using DOM.DSL.Model; +using DOM.DSL.Services; +using DOM.DSL.Tokens; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DOM.DSL.Contexts +{ + internal class TPropertyContext : + TContext + { + private string _name; + private List _indexTokens; + private TToken _nextToken; + + public TPropertyContext(TContext parent, string name) + { + ParentContext = parent; + _name = name; + _indexTokens = new List(); + } + + public override void Read(TStringReader reader) + { + if (reader.EOF) return; + if (reader.Current == TChar.PropertyOrFuncStart) + { + if (reader.Move()) + { + reader.SkipBreaks(); + var name = reader.ReadIdentity(); + if (false == string.IsNullOrWhiteSpace(name)) + { + reader.Move(name.Length); + if (reader.Current == TChar.FuncArgsStart) + { + // Function '@now.format(dd-mm)' + reader.Move(); + var context = new TFunctionContext(this, name); + context.Read(reader); + _nextToken = context.Complete(); + } + else + { + // Property '@now.year' + var context = new TPropertyContext(this, name); + context.Read(reader); + _nextToken = context.Complete(); + } + } + } + } + else if (reader.Current == TChar.PropertyIndexStart) + { + var text = new StringBuilder(); + var flushTextToken = new Action(() => + { + if (text.Length > 0) + { + _indexTokens.Add(new TTextToken { Text = text.ToString() }); + text.Clear(); + } + }); + reader.Move(); + while (reader.EOF == false) + { + switch (reader.Current) + { + #region Ecsaping + case TChar.Escape: + { + switch (reader.Next) + { + case 's': + text.Append(' '); + reader.Move(2); + break; + case 'r': + text.Append(TChar.CaretReturn); + reader.Move(2); + break; + case 'n': + text.Append(TChar.Newline); + reader.Move(2); + break; + case 't': + text.Append(TChar.Tab); + reader.Move(2); + break; + case '@': + case '(': + case ')': + case '.': + case ',': + case '\\': + text.Append(reader.Next); + reader.Move(2); + break; + default: + text.Append(reader.Current); + reader.Move(); + break; + } + } + break; + #endregion + + case TChar.PropertyIndexEnd: + { + flushTextToken(); + if (reader.Next == TChar.PropertyOrFuncStart) + { + reader.Move(2); + reader.SkipBreaks(); + var name = reader.ReadIdentity(); + reader.Move(name.Length); + if (reader.Current.Equals(TChar.FuncArgsStart)) + { + reader.Move(); + var context = new TFunctionContext(this, name); + context.Read(reader); + _nextToken = context.Complete(); + } + else + { + text.Append(name); + } + } + else + { + reader.Move(); + } + } + return; + + case TChar.TokenStart: + { + if (reader.Move()) + { + var name = reader.ReadIdentity(); + if (_elementNames.Contains(name)) + { + flushTextToken(); + reader.Move(name.Length); + var elementContext = new TElementContext(this, name); + elementContext.Read(reader); + _indexTokens.Add(elementContext.Complete()); + } + else + { + text.Append(TChar.TokenStart); + text.Append(reader.Current); + } + } + else + { + text.Append(reader.Current); + } + } + break; + default: + { + text.Append(reader.Current); + reader.Move(); + } + break; + } + } + flushTextToken(); + } + } + + public TToken Complete() + { + return new TPropertyToken + { + PropertyName = _name, + PropertyIndex = new TBlockToken(_name, null, _indexTokens.Select(t => t.Clone()).ToArray()), + NextToken = _nextToken?.Clone() + }; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contexts/TRootContext.cs b/ZeroLevel/Services/DOM/DSL/Contexts/TRootContext.cs new file mode 100644 index 0000000..6ee28d8 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contexts/TRootContext.cs @@ -0,0 +1,146 @@ +using DOM.DSL.Model; +using DOM.DSL.Services; +using DOM.DSL.Tokens; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DOM.DSL.Contexts +{ + internal class TRootContext : + TContext + { + private readonly List _tokens; + + public TRootContext() + { + ParentContext = null; + _tokens = new List(); + } + + public TRootContext(TContext parent) + { + ParentContext = parent; + _tokens = new List(); + } + + public override void Read(TStringReader reader) + { + var text = new StringBuilder(); + var flushTextToken = new Action(() => + { + if (text.Length > 0) + { + _tokens.Add(new TTextToken { Text = text.ToString() }); + text.Clear(); + } + }); + while (reader.EOF == false) + { + switch (reader.Current) + { + #region Ecsaping + case TChar.Escape: + { + switch (reader.Next) + { + case 's': + text.Append(' '); + reader.Move(2); + break; + case 'r': + text.Append(TChar.CaretReturn); + reader.Move(2); + break; + case 'n': + text.Append(TChar.Newline); + reader.Move(2); + break; + case 't': + text.Append(TChar.Tab); + reader.Move(2); + break; + case '@': + case '(': + case ')': + case '.': + case ',': + case '\\': + text.Append(reader.Next); + reader.Move(2); + break; + default: + text.Append(reader.Current); + reader.Move(); + break; + } + } + break; + #endregion + + case TChar.TokenStart: + { + if (reader.Move()) + { + var name = reader.ReadIdentity(); + + if (_elementNames.Contains(name)) + { + flushTextToken(); + reader.Move(name.Length); + var elementContext = new TElementContext(this, name); + reader.SkipBreaks(); + elementContext.Read(reader); + _tokens.Add(elementContext.Complete()); + } + else if (_blockNames.Contains(name)) + { + flushTextToken(); + reader.Move(name.Length); + var blockContext = new TBlockContext(this, name); + blockContext.Read(reader); + _tokens.Add(blockContext.Complete()); + } + else if (ParentContext != null && ParentContext is TBlockContext && + name.Equals("end" + (ParentContext as TBlockContext).Name, StringComparison.OrdinalIgnoreCase)) + { + reader.Move(name.Length); + flushTextToken(); + return; + } + else + { + text.Append(TChar.TokenStart); + text.Append(name); + reader.Move(name.Length); + } + } + else + { + text.Append(TChar.TokenStart); + reader.Move(); + } + } + break; + case TChar.CaretReturn: + case TChar.Newline: + case TChar.Tab: + reader.Move(); + break; + default: + { + text.Append(reader.Current); + reader.Move(); + } + break; + } + } + flushTextToken(); + } + + public IEnumerable Complete() + { + return _tokens; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contracts/ISpecialTableBuilder.cs b/ZeroLevel/Services/DOM/DSL/Contracts/ISpecialTableBuilder.cs new file mode 100644 index 0000000..ae4f5fd --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contracts/ISpecialTableBuilder.cs @@ -0,0 +1,20 @@ +using System.Text; +using ZeroLevel.DocumentObjectModel.Flow; + +namespace DOM.DSL.Contracts +{ + public interface ISpecialTableBuilder + { + /// + /// Указывает что ожидается запись тела ячейки таблицы + /// + bool WaitCellBody { get; } + void WriteToCell(string part); + void EnterTable(Column[] colunmns); + void EnterRow(int count_columns); + void EnterCell(int order); + void LeaveCell(); + void LeaveRow(); + void FlushTable(StringBuilder builder); + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Contracts/TCloneable.cs b/ZeroLevel/Services/DOM/DSL/Contracts/TCloneable.cs new file mode 100644 index 0000000..cd5a6d3 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Contracts/TCloneable.cs @@ -0,0 +1,9 @@ +using DOM.DSL.Tokens; + +namespace DOM.DSL.Contracts +{ + public interface TCloneable + { + TToken Clone(); + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Model/DOMRenderElementCounter.cs b/ZeroLevel/Services/DOM/DSL/Model/DOMRenderElementCounter.cs new file mode 100644 index 0000000..152c8bc --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Model/DOMRenderElementCounter.cs @@ -0,0 +1,47 @@ +namespace DOM.DSL.Model +{ + internal sealed class DOMRenderElementCounter + { + public int SectionId { get; private set; } = -1; + public int ParagraphId { get; private set; } = -1; + public int ListId { get; private set; } = -1; + public int ListItemId { get; private set; } = -1; + public int TableId { get; private set; } = -1; + public int ColumnId { get; private set; } = -1; + public int RowId { get; private set; } = -1; + public int CellId { get; private set; } = -1; + + public int FormId { get; private set; } = -1; + public int LinkId { get; private set; } = -1; + public int QuoteId { get; private set; } = -1; + public int TextId { get; private set; } = -1; + + public int AudioplayerId { get; private set; } = -1; + public int AudioId { get; private set; } = -1; + + public int VideoplayerId { get; private set; } = -1; + public int VideoId { get; private set; } = -1; + + public int GalleryId { get; private set; } = -1; + public int ImageId { get; private set; } = -1; + + public void IncSectionId() { SectionId++; } + public void IncParagraphId() { ParagraphId++; } + public void IncListId() { ListId++; } + public void IncListItemId() { ListItemId++; } + public void IncTableId() { TableId++; } + public void IncColumnId() { ColumnId++; } + public void IncRowId() { RowId++; } + public void IncCellId() { CellId++; } + public void IncFormId() { FormId++; } + public void IncLinkId() { LinkId++; } + public void IncQuoteId() { QuoteId++; } + public void IncTextId() { TextId++; } + public void IncAudioplayerId() { AudioplayerId++; } + public void IncAudioId() { AudioId++; } + public void IncVideoplayerId() { VideoplayerId++; } + public void IncVideoId() { VideoId++; } + public void IncGalleryId() { GalleryId++; } + public void IncImageId() { ImageId++; } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Model/TChar.cs b/ZeroLevel/Services/DOM/DSL/Model/TChar.cs new file mode 100644 index 0000000..4e054ce --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Model/TChar.cs @@ -0,0 +1,17 @@ +namespace DOM.DSL.Model +{ + internal class TChar + { + public const char TokenStart = '@'; + public const char PropertyOrFuncStart = '.'; + public const char PropertyIndexStart = '['; + public const char PropertyIndexEnd = ']'; + public const char FuncArgsStart = '('; + public const char FuncArgsEnd = ')'; + public const char FuncArgsSeparator = ','; + public const char Escape = '\\'; + public const char CaretReturn = '\r'; + public const char Newline = '\n'; + public const char Tab = '\t'; + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Model/TContentElement.cs b/ZeroLevel/Services/DOM/DSL/Model/TContentElement.cs new file mode 100644 index 0000000..428083a --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Model/TContentElement.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ZeroLevel.DocumentObjectModel; +using ZeroLevel.DocumentObjectModel.Flow; + +namespace DOM.DSL.Model +{ + internal class TContentElement + { + private readonly Document _document; + + public TContentElement(Document document) + { + _document = document; + } + + private static void TraversElement(IContentElement element, ContentElementType type, Action handler) + { + if (element.Type == type) + { + handler(element); + } + switch (element.Type) + { + // Containers + case ContentElementType.Section: + var section = (element as Section); + foreach (var item in section.Parts) + { + TraversElement(item, type, handler); + } + break; + case ContentElementType.Paragraph: + var paragraph = (element as Paragraph); + foreach (var item in paragraph.Parts) + { + TraversElement(item, type, handler); + } + break; + case ContentElementType.List: + var list = (element as List); + foreach (var item in list.Items) + { + TraversElement(item, type, handler); + } + break; + case ContentElementType.Gallery: + var gallery = (element as Gallery); + foreach (var item in gallery.Images) + { + TraversElement(item, type, handler); + } + break; + case ContentElementType.Audioplayer: + var audioplayer = (element as Audioplayer); + foreach (var item in audioplayer.Tracks) + { + TraversElement(item, type, handler); + } + break; + case ContentElementType.Videoplayer: + var videoplayer = (element as Videoplayer); + foreach (var item in videoplayer.Playlist) + { + TraversElement(item, type, handler); + } + break; + case ContentElementType.Table: + var table = (element as Table); + foreach (var column in table.Columns) + { + TraversElement(column, type, handler); + } + foreach (var row in table.Rows) + { + TraversElement(row, type, handler); + foreach (var cell in row.Cells) + { + TraversElement(cell, type, handler); + } + } + break; + } + } + + private ContentElementType ParseContentElementType(string element_name) + { + switch (element_name) + { + case "section": + return ContentElementType.Section; + case "paragraph": + return ContentElementType.Paragraph; + case "link": + return ContentElementType.Link; + case "list": + return ContentElementType.List; + case "table": + return ContentElementType.Table; + case "audio": + return ContentElementType.Audio; + case "audioplayer": + return ContentElementType.Audioplayer; + case "form": + return ContentElementType.Form; + case "gallery": + return ContentElementType.Gallery; + case "image": + return ContentElementType.Image; + case "video": + return ContentElementType.Video; + case "videoplayer": + return ContentElementType.Videoplayer; + case "quote": + return ContentElementType.Quote; + case "text": + return ContentElementType.Text; + case "column": + return ContentElementType.Column; + case "row": + return ContentElementType.Row; + } + return ContentElementType.Unknown; + } + + public IEnumerable Find(string elementName, string index) + { + var type = ParseContentElementType(elementName); + if (type == ContentElementType.Unknown) return Enumerable.Empty(); + var list = new List(); + foreach (var section in _document.Content.Sections) + { + TraversElement(section, type, e=>list.Add(e)); + } + return list; + } + + public override string ToString() + { + return "Content"; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Model/TEnvironment.cs b/ZeroLevel/Services/DOM/DSL/Model/TEnvironment.cs new file mode 100644 index 0000000..88307d3 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Model/TEnvironment.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DOM.DSL.Model +{ + public sealed class TEnvironment + { + public int Delay { get; set; } = 0; + public string FileName { get; set; } = null; + public Encoding Encoding { get; set; } = null; + public string ContractName { get; set; } = null; + public string SubscriptionName { get; set; } = null; + public Guid SubscriptionId { get; set; } = Guid.Empty; + public IDictionary CustomVariables { get; } + + public TEnvironment() + { + CustomVariables = new Dictionary(); + } + + public void AddCustomVar(string name, object value) + { + CustomVariables.Add(name.ToLowerInvariant().Trim(), value); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Model/TFlowRules.cs b/ZeroLevel/Services/DOM/DSL/Model/TFlowRules.cs new file mode 100644 index 0000000..42d828b --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Model/TFlowRules.cs @@ -0,0 +1,426 @@ +using DOM.DSL.Contracts; +using DOM.DSL.Services; +using DOM.DSL.Tokens; + +namespace DOM.DSL.Model +{ + internal sealed class TFlowRules + { + #region Rules + public TBlockToken ListPrefix; + public TBlockToken ListPostfix; + public TBlockToken ListItemPrefix; + public TBlockToken ListItemPostfix; + public TBlockToken TextPrefix; + public TBlockToken TextTemplate; + public TBlockToken TextPostfix; + public TBlockToken LinkPrefix; + public TBlockToken LinkTemplate; + public TBlockToken LinkPostfix; + public TBlockToken ImagePrefix; + public TBlockToken ImageTemplate; + public TBlockToken ImagePostfix; + public TBlockToken QuotePrefix; + public TBlockToken QuoteTemplate; + public TBlockToken QuotePostfix; + public TBlockToken VideoPrefix; + public TBlockToken VideoTemplate; + public TBlockToken VideoPostfix; + public TBlockToken AudioPrefix; + public TBlockToken AudioTemplate; + public TBlockToken AudioPostfix; + public TBlockToken TablePrefix; + public TBlockToken TablePostfix; + public TBlockToken SectionPrefix; + public TBlockToken SectionPostfix; + public TBlockToken ParagraphPrefix; + public TBlockToken ParagraphPostfix; + public TBlockToken ColumnsPrefix; + public TBlockToken ColumnsPostfix; + public TBlockToken ColumnPrefix; + public TBlockToken ColumnTemplate; + public TBlockToken ColumnPostfix; + public TBlockToken RowPrefix; + public TBlockToken RowPostfix; + public TBlockToken FirstRowCellPrefix; + public TBlockToken FirstRowCellPostfix; + public TBlockToken CellPrefix; + public TBlockToken CellPostfix; + public TBlockToken FormPrefix; + public TBlockToken FormTemplate; + public TBlockToken FormPostfix; + public TBlockToken AudioplayerPrefix; + public TBlockToken AudioplayerPostfix; + public TBlockToken VideoplayerPrefix; + public TBlockToken VideoplayerPostfix; + public TBlockToken GalleryPrefix; + public TBlockToken GalleryPostfix; + #endregion + + #region Special table builder + public bool UseSpecialTableBuilder = false; + public ISpecialTableBuilder SpecialTableBuilder; + #endregion + + public void Bootstrap() + { + if (null == SectionPrefix) SectionPrefix = null; + if (null == SectionPostfix) SectionPostfix = null; + if (null == ParagraphPrefix) ParagraphPrefix = null; + if (null == ParagraphPostfix) ParagraphPostfix = null; + if (null == ListPrefix) ListPrefix = null; + if (null == ListPostfix) ListPostfix = null; + if (null == ListItemPrefix) ListItemPrefix = null; + if (null == ListItemPostfix) ListItemPostfix = null; + if (null == TablePrefix) TablePrefix = null; + if (null == TablePostfix) TablePostfix = null; + if (null == ColumnsPrefix) ColumnsPrefix = null; + if (null == ColumnsPostfix) ColumnsPostfix = null; + if (null == ColumnPrefix) ColumnPrefix = null; + if (null == ColumnTemplate) ColumnTemplate = null; + if (null == ColumnPostfix) ColumnPostfix = null; + if (null == RowPrefix) RowPrefix = null; + if (null == RowPostfix) RowPostfix = null; + if (null == CellPrefix) CellPrefix = null; + if (null == CellPostfix) CellPostfix = null; + if (null == FirstRowCellPrefix) FirstRowCellPrefix = null; + if (null == FirstRowCellPostfix) FirstRowCellPostfix = null; + if (null == AudioplayerPrefix) AudioplayerPrefix = null; + if (null == AudioplayerPostfix) AudioplayerPostfix = null; + if (null == VideoplayerPrefix) VideoplayerPrefix = null; + if (null == VideoplayerPostfix) VideoplayerPostfix = null; + if (null == GalleryPrefix) GalleryPrefix = null; + if (null == GalleryPostfix) GalleryPostfix = null; + if (null == FormPrefix) FormPrefix = null; + if (null == FormTemplate) FormTemplate = null; + if (null == FormPostfix) FormPostfix = null; + if (null == VideoPrefix) VideoPrefix = null; + if (null == VideoTemplate) VideoTemplate = null; + if (null == VideoPostfix) VideoPostfix = null; + if (null == AudioPrefix) AudioPrefix = null; + if (null == AudioTemplate) AudioTemplate = null; + if (null == AudioPostfix) AudioPostfix = null; + if (null == ImagePrefix) ImagePrefix = null; + if (null == ImageTemplate) ImageTemplate = null; + if (null == ImagePostfix) ImagePostfix = null; + if (null == LinkPrefix) LinkPrefix = null; + if (null == LinkTemplate) LinkTemplate = null; + if (null == LinkPostfix) LinkPostfix = null; + if (null == QuotePrefix) QuotePrefix = null; + if (null == QuoteTemplate) QuoteTemplate = new TBlockToken(new[] { new TElementToken { ElementName = "self" } }); + if (null == QuotePostfix) QuotePostfix = null; + if (null == TextPrefix) TextPrefix = null; + if (null == TextTemplate) TextTemplate = new TBlockToken(new[] { new TElementToken { ElementName = "self" } }); + if (null == TextPostfix) TextPostfix = null; + } + + public void UpdateRule(string elementName, string functionName, TBlockToken rule_token, string special) + { + switch (elementName) + { + case "list": + switch (functionName) + { + case "prefix": + ListPrefix = rule_token; + break; + case "postfix": + ListPostfix = rule_token; + break; + case "ignore": + ListPostfix = ListPrefix = null; + break; + } + break; + case "listitem": + switch (functionName) + { + case "prefix": + ListItemPrefix = rule_token; + break; + case "postfix": + ListItemPostfix = rule_token; + break; + case "ignore": + ListItemPrefix = ListItemPostfix = null; + break; + } + break; + case "text": + switch (functionName) + { + case "prefix": + TextPrefix = rule_token; + break; + case "template": + TextTemplate = rule_token; + break; + case "postfix": + TextPostfix = rule_token; + break; + case "ignore": + TextPrefix = TextTemplate = TextPostfix = null; + break; + } + break; + case "link": + switch (functionName) + { + case "prefix": + LinkPrefix = rule_token; + break; + case "template": + LinkTemplate = rule_token; + break; + case "postfix": + LinkPostfix = rule_token; + break; + case "ignore": + LinkPrefix = LinkTemplate = LinkPostfix = null; + break; + } + break; + case "image": + switch (functionName) + { + case "prefix": + ImagePrefix = rule_token; + break; + case "template": + ImageTemplate = rule_token; + break; + case "postfix": + ImagePostfix = rule_token; + break; + case "ignore": + ImagePrefix = ImageTemplate = ImagePostfix = null; + break; + } + break; + case "quote": + switch (functionName) + { + case "prefix": + QuotePrefix = rule_token; + break; + case "template": + QuoteTemplate = rule_token; + break; + case "postfix": + QuotePostfix = rule_token; + break; + case "ignore": + QuotePrefix = QuoteTemplate = QuotePostfix = null; + break; + } + break; + case "form": + switch (functionName) + { + case "prefix": + FormPrefix = rule_token; + break; + case "template": + FormTemplate = rule_token; + break; + case "postfix": + FormPostfix = rule_token; + break; + case "ignore": + FormPrefix = FormTemplate = FormPostfix = null; + break; + } + break; + case "video": + switch (functionName) + { + case "prefix": + VideoPrefix = rule_token; + break; + case "template": + VideoTemplate = rule_token; + break; + case "postfix": + VideoPostfix = rule_token; + break; + case "ignore": + VideoPrefix = VideoTemplate = VideoPostfix = null; + break; + } + break; + case "audio": + switch (functionName) + { + case "prefix": + AudioPrefix = rule_token; + break; + case "template": + AudioTemplate = rule_token; + break; + case "postfix": + AudioPostfix = rule_token; + break; + case "ignore": + AudioPrefix = AudioTemplate = AudioPostfix = null; + break; + } + break; + case "section": + switch (functionName) + { + case "prefix": + SectionPrefix = rule_token; + break; + case "postfix": + SectionPostfix = rule_token; + break; + case "ignore": + SectionPrefix = SectionPostfix = null; + break; + } + break; + case "paragraph": + switch (functionName) + { + case "prefix": + ParagraphPrefix = rule_token; + break; + case "postfix": + ParagraphPostfix = rule_token; + break; + case "ignore": + ParagraphPrefix = ParagraphPostfix = null; + break; + } + break; + case "table": + switch (functionName) + { + case "prefix": + TablePrefix = rule_token; + break; + case "postfix": + TablePostfix = rule_token; + break; + case "ignore": + TablePrefix = TablePostfix = null; + break; + case "special": // Использование захардкоженного преобразования таблицы + //TablePrefix = TablePostfix = null; + ColumnsPrefix = ColumnsPostfix = null; + ColumnPrefix = ColumnTemplate = ColumnPostfix = null; + RowPrefix = RowPostfix = null; + CellPrefix = CellPostfix = null; + // Аргументы: (style, paddings l-t-r-b, maxcellwidth, maxtablewidth) + UseSpecialTableBuilder = true; + SpecialTableBuilder = SpecialTableBuilderFactory.CreateSpecialTableBuilder(special); + if (SpecialTableBuilder == null) UseSpecialTableBuilder = false; + break; + } + break; + case "columns": + switch (functionName) + { + case "prefix": + ColumnsPrefix = rule_token; + break; + case "postfix": + ColumnsPostfix = rule_token; + break; + case "ignore": + ColumnsPrefix = ColumnsPostfix = null; + break; + } + break; + case "column": + switch (functionName) + { + case "prefix": + ColumnPrefix = rule_token; + break; + case "template": + ColumnTemplate = rule_token; + break; + case "postfix": + ColumnPostfix = rule_token; + break; + case "ignore": + ColumnPrefix = ColumnTemplate = ColumnPostfix = null; + break; + + } + break; + case "tablerow": + switch (functionName) + { + case "prefix": + RowPrefix = rule_token; + break; + case "postfix": + RowPostfix = rule_token; + break; + case "ignore": + RowPrefix = RowPostfix = null; + break; + } + break; + case "tablecell": + switch (functionName) + { + case "prefix": + CellPrefix = rule_token; + break; + case "postfix": + CellPostfix = rule_token; + break; + case "ignore": + CellPrefix = CellPostfix = null; + break; + } + break; + case "videoplayer": + switch (functionName) + { + case "prefix": + VideoplayerPrefix = rule_token; + break; + case "postfix": + VideoplayerPostfix = rule_token; + break; + case "ignore": + VideoplayerPrefix = VideoplayerPostfix = null; + break; + } + break; + case "audioplayer": + switch (functionName) + { + case "prefix": + AudioplayerPrefix = rule_token; + break; + case "postfix": + AudioplayerPostfix = rule_token; + break; + case "ignore": + AudioplayerPrefix = AudioplayerPostfix = null; + break; + } + break; + case "gallery": + switch (functionName) + { + case "prefix": + GalleryPrefix = rule_token; + break; + case "postfix": + GalleryPostfix = rule_token; + break; + case "ignore": + GalleryPrefix = GalleryPostfix = null; + break; + } + break; + } + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Model/TRenderOptions.cs b/ZeroLevel/Services/DOM/DSL/Model/TRenderOptions.cs new file mode 100644 index 0000000..1fa29ed --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Model/TRenderOptions.cs @@ -0,0 +1,89 @@ +using DOM.DSL.Services; +using System.Linq; +using System.Text; + +namespace DOM.DSL.Model +{ + /// + /// Feature + /// + internal class TRenderOptions + { + public int MaxStringWidth { get; set; } = -1; + public bool ValidateAsJson { get; set; } = false; + public bool ValidateAsHtml { get; set; } = false; + public bool ValidateAsXml { get; set; } = false; + } + + internal static class TRenderUtils + { + public static string SplitOn(string initial, int max) + { + var text = new StringBuilder(); + var reader = new TStringReader(initial); + var current_max = 0; + while (reader.EOF == false) + { + if (char.IsLetterOrDigit(reader.Current)) + { + var word = reader.ReadWord(); + if ((current_max + word.Length) < max) + { + text.Append(word); + current_max += word.Length; + } + else if (word.Length >= max) + { + var lines = Enumerable.Range(0, word.Length / max) + .Select(i => word.Substring(i * max, max)). + ToArray(); + int k = 0; + if(current_max > 0) text.Append("\r\n"); + for (; k < lines.Length - 1; k++) + { + text.Append(lines[k]); + text.Append("\r\n"); + } + text.Append(lines[k]); + current_max = lines[k].Length; + } + else + { + text.Append("\r\n"); + current_max = 0; + text.Append(word); + current_max = word.Length; + } + reader.Move(word.Length); + } + else if (reader.Current == '\n') + { + current_max = 0; + text.Append(reader.Current); + reader.Move(); + } + else + { + text.Append(reader.Current); + current_max++; + if (current_max >= max) + { + if (reader.Next == '\r' && + reader.FindOffsetTo('\n') == 2) + { + text.Append("\r\n"); + reader.Move(2); + } + else if (reader.Next != '\n') + { + text.Append("\r\n"); + } + current_max = 0; + } + reader.Move(); + } + } + return text.ToString(); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Services/PlainTextTableBuilder.cs b/ZeroLevel/Services/DOM/DSL/Services/PlainTextTableBuilder.cs new file mode 100644 index 0000000..a9e839c --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Services/PlainTextTableBuilder.cs @@ -0,0 +1,110 @@ +using DOM.DSL.Contracts; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ZeroLevel.DocumentObjectModel.Flow; +using ZeroLevel.Services.PlainTextTables; + +namespace DOM.DSL.Services +{ + internal sealed class PlainTextTableBuilder : ISpecialTableBuilder + { + private class TextTableMeta + { + public TextTableMeta(Column[] columns) + { + Data = new TextTableData(columns.Length); + Data.SetColumnsHeaders(columns.Select(c => c.Caption).ToArray()); + } + + public void FlushRow() + { + if (RowCells != null) + { + Data.AppendRow(RowCells); + RowCells = null; + } + } + + public TextTableData Data; + public string[] RowCells; + public int RowCellIndex = -1; + + private StringBuilder _cellBody = new StringBuilder(); + public void FlushCell() + { + if (RowCellIndex >= 0) + { + RowCells[RowCellIndex] = _cellBody.ToString(); + _cellBody.Clear(); + RowCellIndex = -1; + } + } + + public void WriteCell(string part) + { + _cellBody.Append(part); + } + + public string Complete(TextTableRenderOptions options) + { + return TextTableRender.Render(Data, options); + } + } + private readonly TextTableRenderOptions _options; + private Stack _textTables = new Stack(); + + public PlainTextTableBuilder(TextTableRenderOptions options) + { + _options = options; + } + + public bool WaitCellBody + { + get + { + return _textTables.Count > 0 && + _textTables.Peek().RowCellIndex >= 0; + } + } + + public void EnterTable(Column[] columns) + { + _textTables.Push(new TextTableMeta(columns)); + } + + public void EnterRow(int count_columns) + { + _textTables.Peek().RowCells = new string[count_columns]; + } + + public void EnterCell(int order) + { + _textTables.Peek().RowCellIndex = order; + } + + public void LeaveCell() + { + _textTables.Peek().FlushCell(); + } + + public void LeaveRow() + { + _textTables.Peek().FlushRow(); + } + + public void FlushTable(StringBuilder builder) + { + if (_textTables.Count > 0) + { + var meta = _textTables.Pop(); + builder.Append(meta.Complete(this._options)); + } + } + + public void WriteToCell(string part) + { + _textTables.Peek().WriteCell(part); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Services/SpecialTableBuilderFactory.cs b/ZeroLevel/Services/DOM/DSL/Services/SpecialTableBuilderFactory.cs new file mode 100644 index 0000000..3d008a8 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Services/SpecialTableBuilderFactory.cs @@ -0,0 +1,64 @@ +using DOM.DSL.Contracts; +using System; +using System.Linq; +using ZeroLevel.Services.PlainTextTables; + +namespace DOM.DSL.Services +{ + internal static class SpecialTableBuilderFactory + { + public static ISpecialTableBuilder CreateSpecialTableBuilder(string command) + { + if (string.IsNullOrWhiteSpace(command)) return null; + ISpecialTableBuilder result = null; + var args = command.Split(',').Select(s => s.Trim()).ToArray(); + switch (args[0]) + { + case "plaintext": + // (Borders, 1-0-1-0, 0, 96) + var options = new TextTableRenderOptions(); + if (args.Length > 1) // Стиль + { + if (Enum.TryParse(args[1], out options.Style) == false) + options.Style = TextTableStyle.Borders; + } + if (args.Length > 2) // Паддинги + { + var paddings = args[2].Split(' '); + int buffer; + for (int i = 0; i < paddings.Length; i++) + { + if (true == int.TryParse(paddings[i].Trim(), out buffer)) + { + switch (i) + { + case 0: options.PaddingLeft = buffer; break; + case 1: options.PaddingTop = buffer; break; + case 2: options.PaddingRight = buffer; break; + case 3: options.PaddingBottom = buffer; break; + } + } + } + } + if (args.Length > 3) // Ширина ячейки + { + int buffer; + if (true == int.TryParse(args[3].Trim(), out buffer)) + { + options.MaxCellWidth = buffer; + } + } + if (args.Length > 4) // Ширина таблицы + { + int buffer; + if (true == int.TryParse(args[4].Trim(), out buffer)) + { + options.MaxTableWidth = buffer; + } + } + return new PlainTextTableBuilder(options); + } + return result; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Services/TContainer.cs b/ZeroLevel/Services/DOM/DSL/Services/TContainer.cs new file mode 100644 index 0000000..f9bb8ff --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Services/TContainer.cs @@ -0,0 +1,2643 @@ +using DOM.DSL.Model; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security; +using System.Text; +using System.Threading; +using ZeroLevel.DocumentObjectModel; +using ZeroLevel.DocumentObjectModel.Flow; +using ZeroLevel.Services.FileSystem; +using ZeroLevel.Services.Reflection; +using ZeroLevel.Services.Serialization; +using ZeroLevel.Services.Web; + +namespace DOM.DSL.Services +{ + internal class TContainer + { + #region Private classes + private class TDList + { + private IList _list; + private Type _elementType; + + public int Count => _list.Count; + + public void Append(object _item) + { + if (_item == null) return; + object item; + if (_item is TContainer) + { + item = ((TContainer)_item).Current; + } + else + { + item = _item; + } + if (_list == null) + { + _elementType = item.GetType(); + _list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(_elementType)); + _list.Add(item); + } + else if (_elementType == typeof(string)) + { + _list.Add(item.ToString()); + } + else + { + if (item.GetType() == _elementType) + { + _list.Add(item); + } + else + { + // Добавляются элементы разных типов, в этом случае все элементы приводим к string + var list = new List(); + foreach (var i in _list) list.Add(i.ToString()); + _elementType = typeof(string); + _list.Clear(); + _list = null; + _list = list; + list.Add(item.ToString()); + } + } + } + + public object First() + { + return (_list.Count > 0) ? _list[0] : null; + } + + public IList Complete() + { + return _list; + } + } + #endregion + + private readonly TContainerFactory _factory; + private readonly TRender _render; + + private object _current; + public int Index { get; set; } + + public object Current + { + get { return _current; } + } + + public bool IsNumeric + { + get + { + return _current != null && + (_current is byte || + _current is int || + _current is uint || + _current is long || + _current is ulong || + _current is short || + _current is ushort || + _current is float || + _current is double || + _current is decimal); + } + } + + public bool IsString + { + get + { + return _current != null && _current is string; + } + } + + public bool IsBoolean + { + get + { + return _current != null && _current is Boolean; + } + } + + public bool IsEnumerable + { + get + { + return _current != null && false == (_current is string) && _current is IEnumerable; + } + } + + public TContainer(TContainerFactory factory, TRender render) + { + this._factory = factory; + this._render = render; + } + + public void Reset(object value) + { + _current = value; + Index = 0; + } + + public void Copy(TContainer container) + { + _current = container.Current; + Index = container.Index; + } + + public void MoveToProperty(string propertyName, string propertyIndex) + { + if (propertyName.Equals("order", StringComparison.OrdinalIgnoreCase)) { Reset(Index); return; } + if (_current == null) return; + + var buff_val = _current; + var buff_index = Index; + try + { + if (_current is string) SelectProperty((string)_current, propertyName, propertyIndex); + else if (_current is DateTime) SelectProperty((DateTime)_current, propertyName, propertyIndex); + else if (_current is TimeSpan) SelectProperty((TimeSpan)_current, propertyName, propertyIndex); + else if (_current is Text) SelectProperty((Text)_current, propertyName, propertyIndex); + else if (_current is Image) SelectProperty((Image)_current, propertyName, propertyIndex); + else if (_current is Link) SelectProperty((Link)_current, propertyName, propertyIndex); + else if (_current is Agency) SelectProperty((Agency)_current, propertyName, propertyIndex); + else if (_current is Category) SelectProperty((Category)_current, propertyName, propertyIndex); + else if (_current is Header) SelectProperty((Header)_current, propertyName, propertyIndex); + else if (_current is Tag) SelectProperty((Tag)_current, propertyName, propertyIndex); + else if (_current is AsideContent) SelectProperty((AsideContent)_current, propertyName, propertyIndex); + else if (_current is Assotiation) SelectProperty((Assotiation)_current, propertyName, propertyIndex); + else if (_current is List
) SelectProperty((List
)_current, propertyName, propertyIndex); + else if (_current is Identifier) SelectProperty((Identifier)_current, propertyName, propertyIndex); + else if (_current is TagMetadata) SelectProperty((TagMetadata)_current, propertyName, propertyIndex); + else if (_current is DescriptiveMetadata) SelectProperty((DescriptiveMetadata)_current, propertyName, propertyIndex); + else if (_current is TEnvironment) SelectProperty((TEnvironment)_current, propertyName, propertyIndex); + else if (_current is DOMRenderElementCounter) SelectProperty((DOMRenderElementCounter)_current, propertyName, propertyIndex); + else if (_current is IDictionary) SelectProperty((IDictionary)_current, propertyName, propertyIndex); + else if (_current is Audio) SelectProperty((Audio)_current, propertyName, propertyIndex); + else if (_current is Table) SelectProperty((Table)_current, propertyName, propertyIndex); + else if (_current is FormContent) SelectProperty((FormContent)_current, propertyName, propertyIndex); + else if (_current is Quote) SelectProperty((Quote)_current, propertyName, propertyIndex); + else if (_current is Video) SelectProperty((Video)_current, propertyName, propertyIndex); + else if (_current is Paragraph) SelectProperty((Paragraph)_current, propertyName, propertyIndex); + else if (_current is Section) SelectProperty((Section)_current, propertyName, propertyIndex); + else if (_current is Row) SelectProperty((Row)_current, propertyName, propertyIndex); + else if (_current is Column) SelectProperty((Column)_current, propertyName, propertyIndex); + else if (_current is List) SelectProperty((List)_current, propertyName, propertyIndex); + else if (_current is Audioplayer) SelectProperty((Audioplayer)_current, propertyName, propertyIndex); + else if (_current is Gallery) SelectProperty((Gallery)_current, propertyName, propertyIndex); + else if (_current is Videoplayer) SelectProperty((Videoplayer)_current, propertyName, propertyIndex); + else if (_current is TContentElement) SelectProperty((TContentElement)_current, propertyName, propertyIndex); + else if (_current is TextStyle) SelectProperty((TextStyle)_current, propertyName, propertyIndex); + } + catch + { + _current = buff_val; + Index = buff_index; + } + } + + public void ApplyFunction(string functionName, Func args_getter) + { + var buff_val = _current; + var buff_index = Index; + TContainer[] args = null; + try + { + switch (GetFunctionType(functionName)) + { + case FunctionType.String: + args = args_getter(this); + ApplyStringFunction(functionName, args); + break; + case FunctionType.Condition: + args = args_getter(this); + ApplyConditionFunction(functionName, args); + break; + case FunctionType.Extract: + unchecked + { + ApplyExtractionFunction(functionName, args_getter, out args); + } + break; + case FunctionType.Unknown: + break; + } + } + catch + { + _current = buff_val; + Index = buff_index; + } + if (args != null) + { + foreach (var a in args) + _factory.Release(a); + } + } + + #region As + public T As() + { + if (_current == null) return default(T); + if (_current is T) return (T)_current; + var type = typeof(T); + if (_current is string) + { + return ConvertTo((string)_current); + } + try + { + return (T)Convert.ChangeType(_current, type); + } + catch + { + return default(T); + } + } + + public object As(Type type) + { + if (_current == null) return TypeHelpers.CreateDefaultState(type); + if (_current.GetType().IsAssignableFrom(type)) return _current; + if (_current is string) + { + return ConvertTo((string)_current, type); + } + try + { + return Convert.ChangeType(_current, type); + } + catch + { + return TypeHelpers.CreateDefaultState(type); + } + } + #endregion + + #region Detect function type + private enum FunctionType + { + Extract, + String, + Condition, + Unknown + } + private static FunctionType GetFunctionType(string function) + { + switch (function) + { + case "tolower": + case "toupper": + case "totitle": + case "trim": + case "insert": + case "replace": + case "remove": + case "padleft": + case "padright": + case "substr": + case "index": + case "lastindex": + case "joinright": + case "joinleft": + case "map_mc": + case "map": + case "xmlescape": + case "htmlescape": + case "jsonescape": + case "join": + case "format": + case "tofilename": + case "topath": + return FunctionType.String; + case "contains": + case "nocontains": + case "any": + case "empty": + case "is": + case "isnt": + case "contains_mc": + case "nocontains_mc": + case "any_mc": + case "is_mc": + case "isnt_mc": + case "lt": + case "gt": + case "lte": + case "gte": + case "lt_mc": + case "gt_mc": + case "lte_mc": + case "gte_mc": + case "isnum": + case "isstring": + case "isbool": + case "isenum": + case "isbox": + return FunctionType.Condition; + } + return FunctionType.Extract; + } + #endregion + + #region Properties + + #region Flow + private void SelectProperty(TextStyle style, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "format": + case "formatting": + Reset(style.Formatting); + break; + case "size": + Reset(style.Size); + break; + case "color": + case "foreground": + Reset(style.HexColor); + break; + case "background": + Reset(style.HexMarkerColor); + break; + } + } + + private void SelectProperty(Column column, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "name": + case "title": + case "caption": + Reset(column.Caption); + break; + case "type": + Reset(column.Type); + break; + default: + Reset(column.Caption); + break; + } + } + + private void SelectProperty(Audio audio, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "id": + case "identity": + case "identifier": + Reset(audio.Identifier); + break; + case "title": + case "name": + Reset(audio.Title); + break; + case "type": + Reset(audio.Type); + break; + case "source": + Reset(audio.Source); + break; + } + } + + private void SelectProperty(Table table, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "summary": + case "abstract": + case "lead": + Reset(table.Abstract); + break; + case "columns": + Reset(table.Columns); + break; + case "name": + case "title": + Reset(table.Name); + break; + case "rows": + Reset(table.Rows); + break; + case "type": + Reset(table.Type); + break; + } + } + + private void SelectProperty(FormContent form, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "id": + case "identity": + case "identifier": + Reset(form.Identifier); + break; + case "title": + case "name": + Reset(form.Title); + break; + case "type": + Reset(form.Type); + break; + case "source": + Reset(form.Source); + break; + } + } + + private void SelectProperty(Video video, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "id": + case "identity": + case "identifier": + Reset(video.Identifier); + break; + case "title": + case "name": + Reset(video.Title); + break; + case "type": + Reset(video.Type); + break; + case "source": + Reset(video.Source); + break; + } + } + + private void SelectProperty(Image image, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "id": + case "identity": + case "identifier": + Reset(image.Identifier); + break; + case "title": + case "name": + Reset(image.Title); + break; + case "type": + Reset(image.Type); + break; + case "source": + Reset(image.Source); + break; + } + } + + private void SelectProperty(Quote quote, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "style": + Reset(quote.Style); + break; + case "value": + case "text": + Reset(quote.Value); + break; + case "type": + Reset(quote.Type); + break; + } + } + + private void SelectProperty(Text text, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "style": + Reset(text.Style); + break; + case "value": + case "text": + Reset(text.Value); + break; + case "type": + Reset(text.Type); + break; + } + } + + private void SelectProperty(Link link, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "link": + case "href": + case "url": + Reset(link.Href); + break; + case "value": + case "text": + Reset(link.Value); + break; + case "type": + Reset(link.Type); + break; + } + } + + private void SelectProperty(Paragraph paragraph, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "parts": + case "items": + Reset(paragraph.Parts); + break; + case "type": + Reset(paragraph.Type); + break; + } + } + + private void SelectProperty(Section section, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "parts": + case "items": + Reset(section.Parts); + break; + case "type": + Reset(section.Type); + break; + } + } + + private void SelectProperty(Row row, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "cells": + case "items": + Reset(row.Cells); + break; + case "type": + Reset(row.Type); + break; + } + } + + private void SelectProperty(List list, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "items": + Reset(list.Items); + break; + case "type": + Reset(list.Type); + break; + } + } + + private void SelectProperty(Audioplayer audioplayer, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "name": + case "title": + Reset(audioplayer.Title); + break; + case "items": + case "tracks": + Reset(audioplayer.Tracks); + break; + case "type": + Reset(audioplayer.Type); + break; + } + } + + private void SelectProperty(Gallery gallery, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "name": + case "title": + Reset(gallery.Title); + break; + case "items": + case "images": + Reset(gallery.Images); + break; + case "type": + Reset(gallery.Type); + break; + } + } + + private void SelectProperty(Videoplayer videoplayer, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "name": + case "title": + Reset(videoplayer.Title); + break; + case "items": + case "playlist": + Reset(videoplayer.Playlist); + break; + case "type": + Reset(videoplayer.Type); + break; + } + } + #endregion + + private void SelectProperty(TimeSpan time, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "days": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(time.TotalDays); + } + else + { + Reset(time.TotalDays.ToString(propertyIndex)); + } + break; + case "hours": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(time.TotalHours); + } + else + { + Reset(time.TotalHours.ToString(propertyIndex)); + } + break; + case "minutes": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(time.TotalMinutes); + } + else + { + Reset(time.TotalMinutes.ToString(propertyIndex)); + } + break; + case "seconds": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(time.TotalSeconds); + } + else + { + Reset(time.TotalSeconds.ToString(propertyIndex)); + } + break; + case "milliseconds": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(time.TotalMilliseconds); + } + else + { + Reset(time.TotalMilliseconds.ToString(propertyIndex)); + } + break; + case "ticks": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(time.Ticks); + } + else + { + Reset(time.Ticks.ToString(propertyIndex)); + } + break; + } + } + + private void SelectProperty(TContentElement content, string property, string propertyIndex) + { + Reset(content.Find(property, propertyIndex)); + } + + private void SelectProperty(DOMRenderElementCounter counter, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "list": Reset(counter.ListId); break; + case "listitem": Reset(counter.ListItemId); break; + case "text": Reset(counter.TextId); break; + case "link": Reset(counter.LinkId); break; + case "image": Reset(counter.ImageId); break; + case "quote": Reset(counter.QuoteId); break; + case "video": Reset(counter.VideoId); break; + case "audio": Reset(counter.AudioId); break; + case "form": Reset(counter.FormId); break; + case "section": Reset(counter.SectionId); break; + case "paragraph": Reset(counter.ParagraphId); break; + case "table": Reset(counter.TableId); break; + case "column": Reset(counter.ColumnId); break; + case "tablerow": Reset(counter.RowId); break; + case "tablecell": Reset(counter.CellId); break; + case "videoplayer": Reset(counter.VideoplayerId); break; + case "audioplayer": Reset(counter.AudioplayerId); break; + case "gallery": Reset(counter.GalleryId); break; + } + } + + private void SelectProperty(TEnvironment env, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "user": + case "username": + Reset(Environment.UserName); + break; + case "domain": + case "domainname": + Reset(Environment.UserDomainName); + break; + case "host": + case "hostname": + case "machine": + case "machinename": + Reset(Environment.MachineName); + break; + case "tick": + case "tickcount": + Reset(Environment.TickCount); + break; + + case "subs": + Reset(env.SubscriptionName); + break; + case "subsid": + case "subscriptionid": + Reset(env.SubscriptionId); + break; + case "file": + case "filename": + Reset(env.FileName); + break; + case "delay": + Reset(env.Delay); + break; + case "contract": + Reset(env.ContractName); + break; + + case "encoding": + Reset(env.Encoding.HeaderName); + break; + case "encodingfullname": + Reset(env.Encoding.EncodingName); + break; + case "encodingpage": + case "encodingcode": + case "encodingcodepage": + Reset(env.Encoding.CodePage); + break; + } + } + + private void SelectProperty(Identifier identifier, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "version": + Reset(identifier.Version); + break; + case "time": + case "timestamp": + Reset(identifier.Timestamp); + break; + case "date": + case "datelabel": + Reset(identifier.DateLabel); + break; + } + } + + private void SelectProperty(TagMetadata tags, string property, string propertyIndex) + { + IList enumerable = null; + switch (property.Trim().ToLowerInvariant()) + { + case "places": + enumerable = tags.Places; + break; + case "companies": + enumerable = tags.Companies; + break; + case "persons": + enumerable = tags.Persons; + break; + case "keywords": + enumerable = tags.Keywords; + break; + } + + if (enumerable != null) + { + int index; + if (int.TryParse(propertyIndex, out index)) + { + if (index < 0) index = 0; + if (index >= enumerable.Count) index = enumerable.Count - 1; + Reset(enumerable[index]); + } + else + { + Reset(enumerable); + } + } + } + + private void SelectProperty(DescriptiveMetadata descriptive, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "byline": + case "author": + Reset(descriptive.Byline); + break; + case "copyright": + Reset(descriptive.CopyrightNotice); + break; + case "created": + case "date": + case "time": + case "datetime": + Reset(descriptive.Created); + break; + case "lang": + case "language": + Reset(descriptive.Language); + break; + case "original": + Reset(descriptive.Original); + break; + case "priority": + Reset(descriptive.Priority); + break; + case "publisher": + Reset(descriptive.Publisher); + break; + case "source": + Reset(descriptive.Source); + break; + case "headers": + { + int index; + if (int.TryParse(propertyIndex, out index)) + { + if (index < 0) index = 0; + if (index >= descriptive.Headers.Count) index = descriptive.Headers.Count - 1; + Reset(descriptive.Headers[index]); + } + else + { + Reset(descriptive.Headers); + } + } + break; + } + } + + private void SelectProperty(string line, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "length": + case "count": + Reset(line.Length); + break; + } + } + + private void SelectProperty(DateTime dt, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "day": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Day); + } + else + { + Reset(dt.Day.ToString(propertyIndex)); + } + break; + case "year": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Year); + } + else + { + Reset(dt.Year.ToString(propertyIndex)); + } + break; + case "month": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Month); + } + else + { + Reset(dt.Month.ToString(propertyIndex)); + } + break; + case "date": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Date); + } + else + { + Reset(dt.Date.ToString(propertyIndex)); + } + break; + case "hour": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Hour); + } + else + { + Reset(dt.Hour.ToString(propertyIndex)); + } + break; + case "minute": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Minute); + } + else + { + Reset(dt.Minute.ToString(propertyIndex)); + } + break; + case "second": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Second); + } + else + { + Reset(dt.Second.ToString(propertyIndex)); + } + break; + case "millisecond": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Millisecond); + } + else + { + Reset(dt.Millisecond.ToString(propertyIndex)); + } + break; + case "ticks": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.Ticks); + } + else + { + Reset(dt.Ticks.ToString(propertyIndex)); + } + break; + case "time": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.TimeOfDay); + } + else + { + Reset(dt.TimeOfDay.ToString(propertyIndex)); + } + break; + case "dayofweek": + if (string.IsNullOrWhiteSpace(propertyIndex) == false) + { + try + { + Reset(CultureInfo.GetCultureInfo(propertyIndex).DateTimeFormat.GetDayName(dt.DayOfWeek)); + } + catch { } + } + else + { + Reset(dt.DayOfWeek.ToString()); + } + break; + case "dayofyear": + if (string.IsNullOrWhiteSpace(propertyIndex)) + { + Reset(dt.DayOfYear); + } + else + { + Reset(dt.DayOfYear.ToString(propertyIndex)); + } + break; + } + } + + private void SelectProperty(Agency agency, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "title": + Reset(agency.Title); + break; + case "url": + Reset(agency.Url); + break; + case "description": + Reset(agency.Description); + break; + default: + Reset(agency.Title); + break; + } + } + + private void SelectProperty(Category category, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "title": + Reset(category.Title); + break; + case "code": + Reset(category.Code); + break; + case "description": + Reset(category.Description); + break; + case "direction": + Reset(category.DirectionCode); + break; + case "system": + Reset(category.IsSystem); + break; + default: + Reset(category.Title); + break; + } + } + + private void SelectProperty(Header header, string property, string propertyIndex) + { + switch (property) + { + case "name": + Reset(header.Name); + break; + case "value": + Reset(header.Value); + break; + case "type": + Reset(header.Type); + break; + case "tag": + Reset(header.Tag); + break; + default: + Reset(header.Value); + break; + } + } + + private void SelectProperty(Tag tag, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "name": + Reset(tag.Name); + break; + case "value": + Reset(tag.Value); + break; + default: + Reset(tag.Value); + break; + } + } + + private void SelectProperty(AsideContent aside, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "caption": + Reset(aside.Caption); + break; + case "contenttype": + Reset(aside.ContentType); + break; + case "identity": + Reset(aside.Identity); + break; + case "summary": + Reset(aside.Summary); + break; + default: + Reset(aside.Caption); + break; + } + } + + private void SelectProperty(Assotiation assotiation, string property, string propertyIndex) + { + switch (property.Trim().ToLowerInvariant()) + { + case "title": + Reset(assotiation.Title); + break; + case "documentid": + Reset(assotiation.DocumentId); + break; + case "relation": + Reset(assotiation.Relation); + break; + case "description": + Reset(assotiation.Description); + break; + default: + Reset(assotiation.Title); + break; + } + } + + private void SelectProperty(List
header_list, string property, string propertyIndex) + { + var headers = header_list. + Where(h => h.Name.ToLowerInvariant(). + Equals(property.Trim().ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)). + ToList(); + if (headers.Any()) + { + int index; + if (int.TryParse(propertyIndex, out index)) + { + if (index < 0) index = 0; + if (index >= headers.Count) index = headers.Count - 1; + Reset(headers[index]); + } + else + { + if (headers.Count == 0) + { + Reset(null); + } + else if (headers.Count == 1) + { + Reset(headers[0]); + } + else + { + Reset(headers); + } + } + } + else + { + Reset(null); + } + } + + private void SelectProperty(IDictionary dictionary, string property, string propertyIndex) + { + var typing = dictionary.GetType().GetGenericArguments(); + Type keyType = typing[0]; + Reset(dictionary[ConvertTo(property, keyType)]); + } + #endregion + + #region Functions + private static string HtmlEncode(string text) + { + return HtmlUtility.EncodeHtmlEntities(text); + } + + private static Func MakeMapFunc(string[] args, bool ignorecase) + { + var map_dict = new Dictionary(); + foreach (var arg in args) + { + var map_args = arg?.Split(new[] { "->" }, StringSplitOptions.RemoveEmptyEntries); + if (map_args != null && map_args.Length == 2) + { + if (ignorecase) + { + map_dict.Add(map_args[0].Trim().ToLowerInvariant(), map_args[1]); + } + else + { + map_dict.Add(map_args[0].Trim(), map_args[1]); + } + } + } + return new Func(s => + { + var key = s.Trim(); + if (ignorecase) key = key.ToLowerInvariant(); + if (map_dict.ContainsKey(key)) + { + return map_dict[key]; + } + return s; + }); + } + + private void ApplyStringFunction(string function, TContainer[] args) + { + if (_current == null) + { + args = null; + return; + } + //args = args_getter(this); + switch (function) + { + case "tofilename": + { + Reset(FSUtils.FileNameCorrection(_current.ToString())); + break; + } + case "topath": + { + Reset(FSUtils.PathCorrection(_current.ToString())); + break; + } + case "join": + if (_current is IEnumerable && false == (_current is string)) + { + if (args != null && args.Any()) + { + var separator = args[0].ToString(); + StringBuilder result = new StringBuilder(); + foreach (var i in ((IEnumerable)_current)) + { + var container = _factory.Get(i); + if (args.Length > 1) + { + container.MoveToProperty(args[1].ToString(), null); + } + if (result.Length > 0) result.Append(separator); + result.Append(container.ToString()); + _factory.Release(container); + } + Reset(result.ToString()); + } + } + else if (_current is string) + { + var text = _current.ToString() + string.Join(string.Empty, args.Select(a => a.ToString())); + Reset(text); + } + break; + case "format": + if (_current is DateTime) + { + var format = (args != null && args.Length > 0) ? args[0].ToString() : null; + var culture = (args != null && args.Length > 1) ? args[1].ToString() : null; + Reset(FormattedDateTime((DateTime)_current, format, culture)); + } + break; + case "tolower": Reset(this.ToString().ToLowerInvariant()); break; + case "toupper": Reset(this.ToString().ToUpperInvariant()); break; + case "xmlescape": Reset(XmlEscape(this.ToString())); break; + case "jsonescape": Reset(JsonEscape(this.ToString())); break; + case "htmlescape": Reset(HtmlEncode(this.ToString())); break; + case "totitle": + { + CultureInfo cultureInfo = Thread.CurrentThread.CurrentCulture; + TextInfo textInfo = cultureInfo.TextInfo; + Reset(textInfo.ToTitleCase(this.ToString())); + break; + } + case "trim": Reset(this.ToString().Trim()); break; + case "joinright": + if (args.Length > 0) + { + Reset(string.Concat(this.ToString(), string.Join(string.Empty, args.Select(a => a.ToString())))); + } + break; + case "joinleft": + if (args.Length > 0) + { + Reset(string.Concat(string.Join(string.Empty, args.Select(a => a.ToString())), this.ToString())); + } + break; + case "insert": + { + int position; + string str; + if (args.Length == 1) + { + position = 0; + str = args[0].ToString(); + Reset(this.ToString().Insert(position, str)); + } + else if (args.Length == 2) + { + str = args[1].ToString(); + int pos = args[0].As(); + var line = this.ToString(); + if (pos >= 0 && pos < line.Length) + { + Reset(line.Insert(pos, str)); + } + else + { + if (pos > line.Length) + { + Reset(line.Insert(line.Length, str)); + } + else + { + Reset(line.Insert(0, str)); + } + } + } + } + break; + case "replace": + { + if (args.Length == 2) + { + Reset(this.ToString().Replace(args[0].ToString(), args[1].ToString())); + } + } + break; + case "remove": + { + int start; + int count; + var line = this.ToString(); + if (args.Length == 1) + { + start = args[0].As(); + if (start < line.Length) + Reset(line.Remove(start)); + else + Reset(line); + } + else if (args.Length == 2) + { + start = args[0].As(); + count = args[1].As(); + start = args[0].As(); + if (start < line.Length) + { + Reset(line.Remove(start, count)); + } + else + Reset(line); + } + } + break; + case "index": + if (args.Length == 1) + { + Reset(this.ToString().IndexOf(args[0].ToString(), StringComparison.OrdinalIgnoreCase)); + } + else if (args.Length == 2) + { + Reset(this.ToString().IndexOf(args[0].ToString(), args[1].As(), StringComparison.OrdinalIgnoreCase)); + } + else + { + Reset(-1); + } + break; + case "lastindex": + if (args.Length == 1) + { + Reset(this.ToString().LastIndexOf(args[0].ToString(), StringComparison.OrdinalIgnoreCase)); + } + else if (args.Length == 2) + { + Reset(this.ToString().LastIndexOf(args[0].ToString(), args[1].As(), StringComparison.OrdinalIgnoreCase)); + } + else + { + Reset(-1); + } + break; + case "map_mc": + if (args != null && args.Any()) + { + string[] _args = args.Select(a => a.ToString()).ToArray(); + Reset(MakeMapFunc(_args, false)(this.ToString())); + } + break; + case "map": + if (args != null && args.Any()) + { + string[] _args = args.Select(a => a.ToString()).ToArray(); + Reset(MakeMapFunc(_args, true)(this.ToString())); + } + break; + case "padleft": + { + if (args.Length > 0) + { + int pad = args[0].As(); + string sym = args[1].ToString(); + if (args.Length == 2 && sym.Length > 0) + { + Reset(this.ToString().PadLeft(pad, sym[0])); + } + else if (args.Length == 1) + { + Reset(this.ToString().PadLeft(pad)); + } + else + { + Reset(this.ToString()); + } + } + else + { + Reset(this.ToString()); + } + } + break; + case "padright": + { + if (args.Length > 0) + { + int pad = args[0].As(); + string sym = args[1].ToString(); + if (args.Length == 2 && sym.Length > 0) + { + Reset(this.ToString().PadRight(pad, sym[0])); + } + else if (args.Length == 1) + { + Reset(this.ToString().PadRight(pad)); + } + else + { + Reset(this.ToString()); + } + } + else + { + Reset(this.ToString()); + } + } + break; + case "substr": + { + if (args.Length == 1) + { + var line = this.ToString(); + var start = args[0].As(); + if (start < 0) start = 0; + if (start >= line.Length) start = line.Length - 1; + Reset(this.ToString().Substring(start)); + } + else if (args.Length == 2) + { + var line = this.ToString(); + var start = args[0].As(); + if (start < 0) start = 0; + if (start >= line.Length) start = line.Length - 1; + var end = args[1].As(); + if (end < start) end = start; + if (end >= line.Length) end = line.Length; + Reset(this.ToString().Substring(start, end)); + } + else + { + Reset(this.ToString()); + } + } + break; + } + } + + private void ApplyConditionFunction(string function, TContainer[] args) + { + switch (function) + { + case "isnum": Reset(IsNumeric); break; + case "isstring": Reset(IsString); break; + case "isbool": Reset(IsBoolean); break; + case "isenum": Reset(IsEnumerable); break; + case "isbox": Reset(_current is TContainer); break; + + case "empty": Reset(IsEmpty()); break; + case "contains": Reset(Contains(args, true)); break; + case "contains_mc": Reset(Contains(args, false)); break; + case "nocontains": Reset(NoContains(args, true)); break; + case "nocontains_mc": Reset(NoContains(args, false)); break; + case "any": Reset(Any(args, true)); break; + case "any_mc": Reset(Any(args, false)); break; + case "is": + if (args?.Length > 0) + { + Reset(Is(args[0], true)); + } + break; + case "is_mc": + if (args?.Length > 0) + { + Reset(Is(args[0], false)); + } + break; + case "isnt": + if (args?.Length > 0) + { + Reset(IsNot(args[0], true)); + } + break; + case "isnt_mc": + if (args?.Length > 0) + { + Reset(IsNot(args[0], false)); + } + break; + + case "lt": + if (args?.Length > 0) + { + Reset(LessThan(args[0], true)); + } + break; + case "gt": + if (args?.Length > 0) + { + Reset(MoreThan(args[0], true)); + } + break; + case "lte": + if (args?.Length > 0) + { + Reset(LessOrEq(args[0], true)); + } + break; + case "gte": + if (args?.Length > 0) + { + Reset(MoreOrEq(args[0], true)); + } + break; + case "lt_mc": + if (args?.Length > 0) + { + Reset(LessThan(args[0], false)); + } + break; + case "gt_mc": + if (args?.Length > 0) + { + Reset(MoreThan(args[0], false)); + } + break; + case "lte_mc": + if (args?.Length > 0) + { + Reset(LessOrEq(args[0], false)); + } + break; + case "gte_mc": + if (args?.Length > 0) + { + Reset(MoreOrEq(args[0], false)); + } + break; + } + } + + private void ApplyExtractionFunction(string function, Func args_getter, out TContainer[] args) + { + if (_current == null) + { + if (function.Equals("append", StringComparison.OrdinalIgnoreCase)) + { + var list = new List(); + args = args_getter(this); + foreach (var i in args) + list.Add(i); + Reset(list); + } + if (function.Equals("to", StringComparison.OrdinalIgnoreCase)) + { + args = args_getter(this); + if (args?.Length > 0) + { + var key = args[0].ToString(); + if (_render.BufferDictionary.ContainsKey(key) == false) + { + _render.BufferDictionary.Add(key, this._current); + } + else + { + _render.BufferDictionary[key] = this._current; + } + Reset(null); + } + } + args = null; + return; + } + if (function.Equals("where", StringComparison.OrdinalIgnoreCase)) + { + if (args_getter != null) + { + if (IsEnumerable) + { + var list = new TDList(); + int index = 0; + foreach (var i in ((IEnumerable)_current)) + { + if (i == null) continue; + var container = _factory.Get(i, index); + var conditions = args_getter(container); + if (conditions != null) + { + bool success = conditions.Any(); + foreach (var c in conditions) + { + success &= c.IsBoolean && c.As(); + _factory.Release(c); + } + if (success) + { + list.Append(i); + } + } + _factory.Release(container); + index++; + } + Reset(list.Complete()); + } + } + args = null; + return; + } + else + { + args = args_getter(this); + switch (function) + { + case "to": + if (args?.Length > 0) + { + var key = args[0].ToString(); + if (_render.BufferDictionary.ContainsKey(key) == false) + { + _render.BufferDictionary.Add(key, this._current); + } + else + { + _render.BufferDictionary[key] = this._current; + } + Reset(null); + } + break; + + case "unbox": if (_current != null && _current is TContainer) Reset(((TContainer)_current).Current); break; + + case "max": Max(); break; + case "min": Min(); break; + case "sort": Sort(args); break; + case "reverse": Reverse(); break; + + case "inc": if (args?.Length > 0) Increment(args[0]); break; + case "dec": if (args?.Length > 0) Decrement(args[0]); break; + case "mul": if (args?.Length > 0) Multiply(args[0]); break; + case "div": if (args?.Length > 0) Divide(args[0]); break; + case "mod": if (args?.Length > 0) Mod(args[0]); break; + + case "adddays": if (args?.Length > 0) ChangeDateTime(args[0], ChangeDateTimeType.Days); break; + case "addhours": if (args?.Length > 0) ChangeDateTime(args[0], ChangeDateTimeType.Hours); break; + case "addminutes": if (args?.Length > 0) ChangeDateTime(args[0], ChangeDateTimeType.Minutes); break; + case "addseconds": if (args?.Length > 0) ChangeDateTime(args[0], ChangeDateTimeType.Seconds); break; + case "addmonths": if (args?.Length > 0) ChangeDateTime(args[0], ChangeDateTimeType.Months); break; + case "addyears": if (args?.Length > 0) ChangeDateTime(args[0], ChangeDateTimeType.Years); break; + case "addmilliseconds": if (args?.Length > 0) ChangeDateTime(args[0], ChangeDateTimeType.Milliseconds); break; + + case "count": + { + if (_current is IList) + { + Reset(((IList)_current).Count); + } + else if (_current is string) + { + var line = (string)_current; + Reset(line.Length); + } + else if (_current is IEnumerable) + { + int _i = 0; + foreach (var i in ((IEnumerable)_current)) _i++; + Reset(_i); + } + else if (_current is IDictionary) + { + Reset(((IDictionary)_current).Count); + } + else + { + Reset(_current == null ? 0 : 1); + } + } + break; + case "get": + { + if (args != null && args.Any()) + { + int index = args[0].As(); + if (_current is IList) + { + var list = (IList)_current; + if (index >= 0 && index < list.Count) + { + Reset(list[index]); + } + else + { + Reset(null); + } + } + else if (_current is string) + { + var line = (string)_current; + if (index >= 0 && index < line.Length) + { + Reset(line[index]); + } + else + { + Reset(null); + } + } + else if (_current is IEnumerable) + { + int _i = 0; bool found = false; + foreach (var i in ((IEnumerable)_current)) + { + if (_i == index) + { + found = true; + Reset(i); break; + } + _i++; + } + if (found == false) Reset(null); + } + } + } + break; + case "select": + { + if (args?.Length > 0) + { + var property = args[0].ToString(); + var property_index = args.Length > 1 ? args[1].ToString() : null; + if (_current is IEnumerable && false == (_current is string)) + { + var list = new TDList(); + foreach (var i in ((IEnumerable)_current)) + { + if (i == null) continue; + var container = _factory.Get(i); + container.MoveToProperty(property, property_index); + list.Append(container.Current); + _factory.Release(container); + } + Reset(list.Complete()); + } + else + { + var container = _factory.Get(_current); + container.MoveToProperty(property, property_index); + Reset(container.Current); + _factory.Release(container); + } + } + } + break; + case "apply": + { + if (args?.Length > 0) + { + var functionName = args[0].ToString(); + var functionArgs = args.Skip(1); + var functionType = GetFunctionType(functionName); + if (_current is IEnumerable && false == (_current is string)) + { + var list = new TDList(); + foreach (var i in ((IEnumerable)_current)) + { + if (i == null) continue; + var container = _factory.Get(i); + switch (functionType) + { + case FunctionType.String: + container.ApplyStringFunction(functionName, functionArgs.ToArray()); + break; + case FunctionType.Condition: + container.ApplyConditionFunction(functionName, functionArgs.ToArray()); + break; + } + list.Append(container.Current); + _factory.Release(container); + } + Reset(list.Complete()); + } + else + { + var container = _factory.Get(_current); + switch (functionType) + { + case FunctionType.String: + container.ApplyStringFunction(functionName, functionArgs.ToArray()); + break; + case FunctionType.Condition: + container.ApplyConditionFunction(functionName, functionArgs.ToArray()); + break; + } + Reset(container.Current); + _factory.Release(container); + } + } + } + break; + case "distinct": + { + if (_current is IEnumerable) + { + Type elementType = ((IEnumerable)_current).GetType().GetGenericArguments()[0]; + var genericList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)); + foreach (var i in ((IEnumerable)_current)) + { + if (false == genericList.Contains(i)) + { + genericList.Add(i); + } + } + Reset(genericList); + } + } + break; + case "utc": + if (_current != null && (_current is DateTime)) + { + Reset(((DateTime)_current).ToUniversalTime()); + } + break; + + case "tonum": + case "tonumber": + if (_current != null) + { + var buf = _current.ToString(); + int num; + if (int.TryParse(buf, out num)) + { + Reset(num); + } + else + { + Reset(0); + } + } + break; + case "append": + if (_current is List) + { + var list = _current as List; + foreach (var i in args) + list.Add(i); + Reset(list); + args = null; + } + break; + } + } + } + #endregion + + #region Math + private void Multiply(TContainer value) + { + if (_current is byte) Reset(((byte)_current) * value.As()); + else if (_current is int) Reset(((int)_current) * value.As()); + else if (_current is uint) Reset(((uint)_current) * value.As()); + else if (_current is long) Reset(((long)_current) * value.As()); + else if (_current is ulong) Reset(((ulong)_current) * value.As()); + else if (_current is short) Reset(((short)_current) * value.As()); + else if (_current is ushort) Reset(((ushort)_current) * value.As()); + else if (_current is float) Reset(((float)_current) * value.As()); + else if (_current is double) Reset(((double)_current) * value.As()); + else if (_current is decimal) Reset(((decimal)_current) * value.As()); + } + private void Divide(TContainer value) + { + if (_current is byte) Reset(((byte)_current) / value.As()); + else if (_current is int) Reset(((int)_current) / value.As()); + else if (_current is uint) Reset(((uint)_current) / value.As()); + else if (_current is long) Reset(((long)_current) / value.As()); + else if (_current is ulong) Reset(((ulong)_current) / value.As()); + else if (_current is short) Reset(((short)_current) / value.As()); + else if (_current is ushort) Reset(((ushort)_current) / value.As()); + else if (_current is float) Reset(((float)_current) / value.As()); + else if (_current is double) Reset(((double)_current) / value.As()); + else if (_current is decimal) Reset(((decimal)_current) / value.As()); + } + private void Increment(TContainer value) + { + if (_current is byte) Reset(((byte)_current) + value.As()); + else if (_current is char) Reset((char)(((char)_current) + value.As())); + else if (_current is int) Reset(((int)_current) + value.As()); + else if (_current is uint) Reset(((uint)_current) + value.As()); + else if (_current is long) Reset(((long)_current) + value.As()); + else if (_current is ulong) Reset(((ulong)_current) + value.As()); + else if (_current is short) Reset(((short)_current) + value.As()); + else if (_current is ushort) Reset(((ushort)_current) + value.As()); + else if (_current is float) Reset(((float)_current) + value.As()); + else if (_current is double) Reset(((double)_current) + value.As()); + else if (_current is decimal) Reset(((decimal)_current) + value.As()); + else if (_current is DateTime) Reset(new DateTime(((DateTime)_current).Ticks + value.As())); + } + private void Decrement(TContainer value) + { + if (_current is byte) Reset(((byte)_current) - value.As()); + else if (_current is int) Reset(((int)_current) - value.As()); + else if (_current is uint) Reset(((uint)_current) - value.As()); + else if (_current is long) Reset(((long)_current) - value.As()); + else if (_current is ulong) Reset(((ulong)_current) - value.As()); + else if (_current is short) Reset(((short)_current) - value.As()); + else if (_current is ushort) Reset(((ushort)_current) - value.As()); + else if (_current is float) Reset(((float)_current) - value.As()); + else if (_current is double) Reset(((double)_current) - value.As()); + else if (_current is decimal) Reset(((decimal)_current) - value.As()); + else if (_current is DateTime) Reset(new DateTime(((DateTime)_current).Ticks - value.As())); + } + private void Mod(TContainer value) + { + if (_current is byte) Reset(((byte)_current) % value.As()); + else if (_current is int) Reset(((int)_current) % value.As()); + else if (_current is uint) Reset(((uint)_current) % value.As()); + else if (_current is long) Reset(((long)_current) % value.As()); + else if (_current is ulong) Reset(((ulong)_current) % value.As()); + else if (_current is short) Reset(((short)_current) % value.As()); + else if (_current is ushort) Reset(((ushort)_current) % value.As()); + else if (_current is float) Reset(((float)_current) % value.As()); + else if (_current is double) Reset(((double)_current) % value.As()); + else if (_current is decimal) Reset(((decimal)_current) % value.As()); + } + private void Max() + { + if (IsEnumerable) + { + Type elementType = ((IEnumerable)_current).GetType().GetGenericArguments()[0]; + if (elementType == typeof(byte)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(int)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(uint)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(long)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(ulong)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(short)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(ushort)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(float)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(double)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(decimal)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(string)) + { + Reset(((IEnumerable)_current).Max()); + } + else if (elementType == typeof(DateTime)) + { + Reset(((IEnumerable)_current).Max()); + } + } + } + private void Min() + { + if (IsEnumerable) + { + Type elementType = ((IEnumerable)_current).GetType().GetGenericArguments()[0]; + if (elementType == typeof(byte)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(int)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(uint)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(long)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(ulong)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(short)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(ushort)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(float)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(double)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(decimal)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(string)) + { + Reset(((IEnumerable)_current).Min()); + } + else if (elementType == typeof(DateTime)) + { + Reset(((IEnumerable)_current).Min()); + } + } + } + + private void Sort(TContainer[] args) + { + if (IsEnumerable) + { + Type elementType = ((IEnumerable)_current).GetType().GetGenericArguments()[0]; + if (elementType == typeof(byte)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(int)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(uint)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(long)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(ulong)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(short)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(ushort)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(float)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(double)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(decimal)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(string)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(DateTime)) + { + Reset(((IEnumerable)_current).OrderBy(x => x)); + } + else if (elementType == typeof(Category)) + { + string key = "title"; + if (args?.Length == 1) + { + switch (args[0].ToString()) + { + case "title": key = "title"; break; + case "code": key = "code"; break; + case "description": key = "description"; break; + case "direction": key = "direction"; break; + case "system": key = "system"; break; + default: key = "title"; break; + } + } + switch (key) + { + case "title": Reset(((IEnumerable)_current).OrderBy(x => x.Title)); break; + case "code": Reset(((IEnumerable)_current).OrderBy(x => x.Code)); break; + case "description": Reset(((IEnumerable)_current).OrderBy(x => x.Description)); break; + case "direction": Reset(((IEnumerable)_current).OrderBy(x => x.DirectionCode)); break; + case "system": Reset(((IEnumerable)_current).OrderBy(x => x.IsSystem)); break; + } + } + else if (elementType == typeof(Header)) + { + string key = "value"; + if (args?.Length == 1) + { + switch (args[0].ToString()) + { + case "name": key = "name"; break; + case "value": key = "value"; break; + case "type": key = "type"; break; + case "tag": key = "tag"; break; + default: key = "value"; break; + } + } + switch (key) + { + case "name": Reset(((IEnumerable
)_current).OrderBy(x => x.Name)); break; + case "value": Reset(((IEnumerable
)_current).OrderBy(x => x.Value)); break; + case "type": Reset(((IEnumerable
)_current).OrderBy(x => x.Type)); break; + case "tag": Reset(((IEnumerable
)_current).OrderBy(x => x.Tag)); break; + } + } + else if (elementType == typeof(Tag)) + { + string key = "name"; + if (args?.Length == 1) + { + switch (args[0].ToString()) + { + case "name": key = "name"; break; + case "value": key = "value"; break; + default: key = "name"; break; + } + } + switch (key) + { + case "name": Reset(((IEnumerable)_current).OrderBy(x => x.Name)); break; + case "value": Reset(((IEnumerable)_current).OrderBy(x => x.Value)); break; + } + } + else if (elementType == typeof(Agency)) + { + string key = "title"; + if (args?.Length == 1) + { + switch (args[0].ToString()) + { + case "title": key = "title"; break; + case "url": key = "url"; break; + case "description": key = "description"; break; + default: key = "title"; break; + } + } + switch (key) + { + case "title": Reset(((IEnumerable)_current).OrderBy(x => x.Title)); break; + case "url": Reset(((IEnumerable)_current).OrderBy(x => x.Url)); break; + case "description": Reset(((IEnumerable)_current).OrderBy(x => x.Description)); break; + } + } + } + } + + private void Reverse() + { + if (IsEnumerable) + { + Type elementType = ((IEnumerable)_current).GetType().GetGenericArguments()[0]; + var genericList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)); + foreach (var i in ((IEnumerable)_current)) + { + if (false == genericList.Contains(i)) + { + genericList.Insert(0, i); + } + } + Reset(genericList); + } + } + + private enum ChangeDateTimeType + { + Days, + Hours, + Minutes, + Seconds, + Milliseconds, + Months, + Years + } + private void ChangeDateTime(TContainer value, ChangeDateTimeType type) + { + if (_current == null) return; + if (_current is DateTime) + { + var dt = (DateTime)_current; + switch (type) + { + case ChangeDateTimeType.Days: + Reset(dt.AddDays(value.As())); + break; + case ChangeDateTimeType.Hours: + Reset(dt.AddHours(value.As())); + break; + case ChangeDateTimeType.Minutes: + Reset(dt.AddMinutes(value.As())); + break; + case ChangeDateTimeType.Seconds: + Reset(dt.AddSeconds(value.As())); + break; + + case ChangeDateTimeType.Milliseconds: + Reset(dt.AddMilliseconds(value.As())); + break; + case ChangeDateTimeType.Months: + Reset(dt.AddMonths(value.As())); + break; + case ChangeDateTimeType.Years: + Reset(dt.AddYears(value.As())); + break; + } + } + } + #endregion + + #region Conditions + public bool Any(TContainer[] set = null, bool ignoreCase = true) + { + if (_current == null) return false; + if (set.Any()) + { + if (_current is IEnumerable && false == (_current is string)) + { + foreach (var c in (IEnumerable)_current) + { + foreach (var t in set) + { + if (CompareWith(c, t, ignoreCase) == 0) return true; + } + } + } + else + { + foreach (var t in set) + { + if (CompareWith(t, ignoreCase) == 0) return true; + } + } + return false; + } + return string.IsNullOrWhiteSpace(this.ToString()) == false; + } + + public bool Contains(TContainer[] set, bool ignoreCase) + { + if (_current == null) return false; + if (set == null || set?.Length == 0) return false; + if (_current is IEnumerable && false == (_current is string)) + { + foreach (var t in set) + { + bool contains = false; + foreach (var c in (IEnumerable)_current) + { + if (CompareWith(c, t, ignoreCase) == 0) + { + contains = true; + } + } + if (contains == false) return false; + } + return true; + } + else if (_current is string) + { + var line = (string)_current; + foreach (var t in set) + { + if (line.IndexOf(t.ToString(), StringComparison.OrdinalIgnoreCase) == -1) return false; + } + return true; + } + else + { + foreach (var t in set) + { + if (CompareWith(t, ignoreCase) != 0) return false; + } + } + return true; + } + + public bool NoContains(TContainer[] test, bool ignoreCase) + { + if (_current == null) return false; + if (_current == null) return false; + if (_current is IEnumerable && false == (_current is string)) + { + foreach (var c in (IEnumerable)_current) + { + foreach (var t in test) + { + if (CompareWith(c, t, ignoreCase) == 0) return false; + } + } + } + else + { + foreach (var t in test) + { + if (CompareWith(t, ignoreCase) == 0) return false; + } + } + return true; + } + + public bool IsEmpty() + { + if (_current == null) return true; + return String.IsNullOrWhiteSpace(_current.ToString()); + } + + public bool Is(TContainer test, bool ignoreCase) + { + if (_current == null) return test.Current == null; + return CompareWith(test, ignoreCase) == 0; + } + + public bool IsNot(TContainer test, bool ignoreCase) + { + if (_current == null) return test.Current != null; + return CompareWith(test, ignoreCase) != 0; + } + + public bool LessThan(TContainer test, bool ignoreCase) + { + if (_current == null) return false; + return CompareWith(test, ignoreCase) < 0; + } + + public bool MoreThan(TContainer test, bool ignoreCase) + { + if (_current == null) return false; + return CompareWith(test, ignoreCase) > 0; + } + + public bool LessOrEq(TContainer test, bool ignoreCase) + { + if (_current == null) return false; + return CompareWith(test, ignoreCase) <= 0; + } + + public bool MoreOrEq(TContainer test, bool ignoreCase) + { + if (_current == null) return false; + return CompareWith(test, ignoreCase) >= 0; + } + + private int CompareWith(TContainer test, bool ignoreCase) + { + return CompareWith(_current, test, ignoreCase); + } + + private static int CompareWith(object val, TContainer test, bool ignoreCase) + { + if (val is string) + { + return string.Compare((string)val, test.As(), ignoreCase); + } + else if (val is DateTime) + { + return DateTime.Compare((DateTime)val, test.As()); + } + else if (val is bool) + { + var t = test.As(); + switch ((bool)val) + { + case true: + if (t) return 0; + return 1; + case false: + if (!t) return 0; + return -1; + } + } + else if (val is int) + { + return ((int)val).CompareTo(test.As()); + } + else if (val is long) + { + return ((long)val).CompareTo(test.As()); + } + else if (val is byte) + { + return ((byte)val).CompareTo(test.As()); + } + else if (val is short) + { + return ((short)val).CompareTo(test.As()); + } + else if (val is uint) + { + return ((uint)val).CompareTo(test.As()); + } + else if (val is ulong) + { + return ((ulong)val).CompareTo(test.As()); + } + else if (val is ushort) + { + return ((ushort)val).CompareTo(test.As()); + } + else if (val is float) + { + return ((float)val).CompareTo(test.As()); + } + else if (val is double) + { + return ((double)val).CompareTo(test.As()); + } + else if (val is decimal) + { + return ((decimal)val).CompareTo(test.As()); + } + else if (val is Guid) + { + return ((Guid)val).CompareTo(test.As()); + } + return string.Compare(val.ToString(), test.ToString(), ignoreCase); + } + + private static T ConvertTo(string line) + { + return (T)StringToTypeConverter.TryConvert(line, typeof(T)); + } + private static object ConvertTo(string line, Type type) + { + return StringToTypeConverter.TryConvert(line, type); + } + #endregion + + #region Helpers + private static string XmlEscape(string unescaped) + { + return SecurityElement.Escape(unescaped); + } + + private static string JsonEscape(string s) + { + return JsonEscaper.EscapeString(s); + } + private const string DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + private static string FormattedDateTime(DateTime dt, string format = null, string culture = null) + { + CultureInfo ci; + if (culture != null) + { + try + { + ci = CultureInfo.GetCultureInfo(culture); + } + catch + { + ci = CultureInfo.CurrentCulture; + } + } + else + { + ci = CultureInfo.CurrentCulture; + } + if (false == string.IsNullOrWhiteSpace(format)) + { + try + { + return dt.ToString(format, ci); + } + catch { } + } + return dt.ToString(DEFAULT_DATETIME_FORMAT, ci); + } + #endregion + + public override string ToString() + { + if (_current == null) return string.Empty; + + if (_current is string) return (string)_current; + else if (_current is DateTime) return FormattedDateTime((DateTime)_current); + else if (_current is TimeSpan) return ((TimeSpan)_current).ToString(); + else if (_current is Agency) return ((Agency)_current).Title ?? string.Empty; + else if (_current is Category) return ((Category)_current).Title ?? string.Empty; + else if (_current is Header) return ((Header)_current).Value ?? string.Empty; + else if (_current is Tag) return ((Tag)_current).Name ?? string.Empty; + else if (_current is AsideContent) return ((AsideContent)_current).Caption ?? string.Empty; + else if (_current is Assotiation) return ((Assotiation)_current).Title ?? string.Empty; + else if (_current is List
) return string.Join("; ", ((List
)_current).Select(h => h.Name)); + else if (_current is Identifier) return string.Empty; + else if (_current is TagMetadata) return string.Empty; + else if (_current is DescriptiveMetadata) return string.Empty; + else if (_current is TEnvironment) return string.Empty; + else if (_current is CustomBlocks) return string.Empty; + else if (_current is DOMRenderElementCounter) return string.Empty; + else if (_current is Audio) return ((Audio)_current).Title ?? string.Empty; + else if (_current is Table) return ((Table)_current).Name ?? string.Empty; + else if (_current is FormContent) return ((FormContent)_current).Title ?? string.Empty; + else if (_current is Image) return ((Image)_current).Title ?? string.Empty; + else if (_current is Link) return ((Link)_current).Value ?? string.Empty; + else if (_current is Quote) return ((Quote)_current).Value ?? string.Empty; + else if (_current is Text) return ((Text)_current).Value ?? string.Empty; + else if (_current is Video) return ((Video)_current).Title ?? string.Empty; + else if (_current is Paragraph) return "Paragraph"; + else if (_current is Section) return "Section"; + else if (_current is Row) return "Row"; + else if (_current is Column) return ((Column)_current).Caption ?? string.Empty; + else if (_current is List) return "List"; + else if (_current is Audioplayer) return ((Audioplayer)_current).Title.Value ?? string.Empty; + else if (_current is Gallery) return ((Gallery)_current).Title.Value ?? string.Empty; + else if (_current is Videoplayer) return ((Videoplayer)_current).Title.Value ?? string.Empty; + else if (_current is TextStyle) return string.Empty; + + if (_current is IEnumerable) + { + var separator = "; "; + StringBuilder result = new StringBuilder(); + foreach (var i in ((IEnumerable)_current)) + { + var container = (i is TContainer) ? (TContainer)i : _factory.Get(i); + if (result.Length > 0) result.Append(separator); + result.Append(container.ToString()); + if (!(i is TContainer)) + _factory.Release(container); + } + return result.ToString(); + } + return _current?.ToString() ?? string.Empty; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Services/TContainerFactory.cs b/ZeroLevel/Services/DOM/DSL/Services/TContainerFactory.cs new file mode 100644 index 0000000..20a4a2c --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Services/TContainerFactory.cs @@ -0,0 +1,45 @@ +using System.Threading; +using ZeroLevel.Services.Pools; + +namespace DOM.DSL.Services +{ + public class TContainerFactory + { + private readonly ObjectPool _pool; + + private static int _get_count = 0; + private static int _release_count = 0; + + internal TContainerFactory(TRender render) + { + _pool = new ObjectPool(() => new TContainer(this, render), 64); + } + + internal TContainer Get(object value) + { + Interlocked.Increment(ref _get_count); + var c = _pool.Allocate(); + c.Reset(value); + return c; + } + internal TContainer Get(object value, int index) + { + Interlocked.Increment(ref _get_count); + var c = _pool.Allocate(); + c.Reset(value); + c.Index = index; + return c; + } + internal void Release(TContainer container) + { + if (container != null) + { + Interlocked.Increment(ref _release_count); + _pool.Free(container); + } + } + + public static int GetsCount() => _get_count; + public static int ReleasesCount() => _release_count; + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Services/TContentToStringConverter.cs b/ZeroLevel/Services/DOM/DSL/Services/TContentToStringConverter.cs new file mode 100644 index 0000000..1317cf1 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Services/TContentToStringConverter.cs @@ -0,0 +1,409 @@ +using DOM.DSL.Contracts; +using DOM.DSL.Model; +using DOM.DSL.Tokens; +using System.Text; +using ZeroLevel.DocumentObjectModel; +using ZeroLevel.DocumentObjectModel.Flow; + +namespace DOM.DSL.Services +{ + internal sealed class TContentToStringConverter : + IContentReader + { + private readonly StringBuilder _builder; + private readonly TRender _render; + private readonly TFlowRules _transformRules; + private readonly ISpecialTableBuilder _specialTableBuilder; + + private bool _useSpecialTableBuilder = false; + + public TContentToStringConverter(TRender render, TFlowRules rules) + { + _render = render; + _transformRules = rules; + _specialTableBuilder = rules.SpecialTableBuilder; + _useSpecialTableBuilder = rules.UseSpecialTableBuilder && rules.SpecialTableBuilder != null; + + _builder = new StringBuilder(); + } + + public string Complete() + { + return _builder.ToString(); + } + + private void WriteText(string text) + { + if (_useSpecialTableBuilder && _specialTableBuilder.WaitCellBody) + { + _specialTableBuilder.WriteToCell(text); + } + else + { + _builder.Append(text); + } + } + + private string Resolve(TBlockToken token, object value, int order) + { + StringBuilder text = new StringBuilder(); + var self = _render.Factory.Get(value, order); + _render.Resolve(token, c => text.Append(c.ToString()), true, self); + _render.Factory.Release(self); + return text.ToString(); + } + + #region Primitives + public void ReadAudio(Audio audio, int order) + { + _render.Counter.IncAudioId(); + if (_transformRules.AudioPrefix != null) + { + WriteText(Resolve(_transformRules.AudioPrefix, audio, order)); + } + if (_transformRules.AudioTemplate != null) + { + WriteText(Resolve(_transformRules.AudioTemplate, audio, order)); + } + if (_transformRules.AudioPostfix != null) + { + WriteText(Resolve(_transformRules.AudioPostfix, audio, order)); + } + } + + public void ReadColumn(Table table, Column column, int order) + { + _render.Counter.IncColumnId(); + if (_transformRules.ColumnPrefix != null) + { + WriteText(Resolve(_transformRules.ColumnPrefix, column, order)); + } + if (_transformRules.ColumnTemplate != null) + { + WriteText(Resolve(_transformRules.ColumnTemplate, column, order)); + } + if (_transformRules.ColumnPostfix != null) + { + WriteText(Resolve(_transformRules.ColumnPostfix, column, order)); + } + } + + public void ReadForm(FormContent form) + { + _render.Counter.IncFormId(); + if (_transformRules.FormPrefix != null) + { + WriteText(Resolve(_transformRules.FormPrefix, form, 0)); + } + if (_transformRules.FormTemplate != null) + { + WriteText(Resolve(_transformRules.FormTemplate, form, 0)); + } + if (_transformRules.FormPostfix != null) + { + WriteText(Resolve(_transformRules.FormPostfix, form, 0)); + } + } + + public void ReadImage(Image image, int order) + { + _render.Counter.IncImageId(); + if (_transformRules.ImagePrefix != null) + { + WriteText(Resolve(_transformRules.ImagePrefix, image, order)); + } + if (_transformRules.ImageTemplate != null) + { + WriteText(Resolve(_transformRules.ImageTemplate, image, order)); + } + if (_transformRules.ImagePostfix != null) + { + WriteText(Resolve(_transformRules.ImagePostfix, image, order)); + } + } + + public void ReadLink(Link link, int order) + { + _render.Counter.IncLinkId(); + if (_transformRules.LinkPrefix != null) + { + WriteText(Resolve(_transformRules.LinkPrefix, link, order)); + } + if (_transformRules.LinkTemplate != null) + { + WriteText(Resolve(_transformRules.LinkTemplate, link, order)); + } + if (_transformRules.LinkPostfix != null) + { + WriteText(Resolve(_transformRules.LinkPostfix, link, order)); + } + } + + public void ReadQuote(Quote quote) + { + _render.Counter.IncQuoteId(); + if (_transformRules.QuotePrefix != null) + { + WriteText(Resolve(_transformRules.QuotePrefix, quote, 0)); + } + if (_transformRules.QuoteTemplate != null) + { + WriteText(Resolve(_transformRules.QuoteTemplate, quote, 0)); + } + if (_transformRules.QuotePostfix != null) + { + WriteText(Resolve(_transformRules.QuotePostfix, quote, 0)); + } + } + + public void ReadText(Text text) + { + _render.Counter.IncTextId(); + if (_transformRules.TextPrefix != null) + { + WriteText(Resolve(_transformRules.TextPrefix, text, 0)); + } + if (_transformRules.TextTemplate != null) + { + WriteText(Resolve(_transformRules.TextTemplate, text, 0)); + } + if (_transformRules.TextPostfix != null) + { + WriteText(Resolve(_transformRules.TextPostfix, text, 0)); + } + } + + public void ReadVideo(Video video, int order) + { + _render.Counter.IncVideoId(); + if (_transformRules.VideoPrefix != null) + { + WriteText(Resolve(_transformRules.VideoPrefix, video, order)); + } + if (_transformRules.VideoTemplate != null) + { + WriteText(Resolve(_transformRules.VideoTemplate, video, order)); + } + if (_transformRules.VideoPostfix != null) + { + WriteText(Resolve(_transformRules.VideoPostfix, video, order)); + } + } + #endregion + + #region Containers + public void EnterParagraph(Paragraph paragraph) + { + _render.Counter.IncParagraphId(); + if (_transformRules.ParagraphPrefix != null) + { + WriteText(Resolve(_transformRules.ParagraphPrefix, paragraph, _render.Counter.ParagraphId)); + } + } + public void LeaveParagraph(Paragraph paragraph) + { + if (_transformRules.ParagraphPostfix != null) + { + WriteText(Resolve(_transformRules.ParagraphPostfix, paragraph, _render.Counter.ParagraphId)); + } + } + public void EnterSection(Section section) + { + _render.Counter.IncSectionId(); + if (_transformRules.SectionPrefix != null) + { + WriteText(Resolve(_transformRules.SectionPrefix, section, _render.Counter.SectionId)); + } + } + public void LeaveSection(Section section) + { + if (_transformRules.SectionPostfix != null) + { + WriteText(Resolve(_transformRules.SectionPostfix, section, _render.Counter.SectionId)); + } + } + #endregion + + #region Table + public void EnterTable(Table table) + { + _render.Counter.IncTableId(); + if (_transformRules.TablePrefix != null) + { + _builder.Append(Resolve(_transformRules.TablePrefix, table, _render.Counter.TableId)); + } + if (_useSpecialTableBuilder) + { + _builder.AppendLine(); + _specialTableBuilder.EnterTable(table.Columns.ToArray()); + } + } + public void EnterColumns(Table table) + { + if (_useSpecialTableBuilder == false && _transformRules.ColumnsPrefix != null) + { + _builder.Append(Resolve(_transformRules.ColumnsPrefix, table.Columns, 0)); + } + } + public void EnterRow(Table table, Row row, int order) + { + _render.Counter.IncRowId(); + if (_useSpecialTableBuilder) + { + _specialTableBuilder.EnterRow(row.Cells.Count); + } + else if (_transformRules.RowPrefix != null) + { + _builder.Append(Resolve(_transformRules.RowPrefix, row, order)); + } + } + public void EnterRowCell(Table table, Row row, IContentElement cell, int order) + { + _render.Counter.IncCellId(); + if (_useSpecialTableBuilder) + { + _specialTableBuilder.EnterCell(order); + } + else + { + if (order == 0 && _transformRules.FirstRowCellPrefix != null) + { + _builder.Append(Resolve(_transformRules.FirstRowCellPrefix, cell, order)); + } + else if (_transformRules.CellPrefix != null) + { + _builder.Append(Resolve(_transformRules.CellPrefix, cell, order)); + } + } + } + public void LeaveColumns(Table table) + { + if (_useSpecialTableBuilder == false && _transformRules.ColumnsPostfix != null) + { + _builder.Append(Resolve(_transformRules.ColumnsPostfix, table.Columns, 0)); + } + } + public void LeaveRow(Table table, Row row, int order) + { + if (_useSpecialTableBuilder) + { + _specialTableBuilder.LeaveRow(); + } + else if (_transformRules.RowPostfix != null) + { + _builder.Append(Resolve(_transformRules.RowPostfix, row, order)); + } + } + public void LeaveRowCell(Table table, Row row, IContentElement cell, int order) + { + if (_useSpecialTableBuilder) + { + _specialTableBuilder.LeaveCell(); + } + else + { + if (order == 0 && _transformRules.FirstRowCellPostfix != null) + { + _builder.Append(Resolve(_transformRules.FirstRowCellPostfix, cell, order)); + } + else if (_transformRules.CellPostfix != null) + { + _builder.Append(Resolve(_transformRules.CellPostfix, cell, order)); + } + } + } + public void LeaveTable(Table table) + { + if (_useSpecialTableBuilder) + { + _specialTableBuilder.FlushTable(_builder); + } + if (_transformRules.TablePostfix != null) + { + _builder.Append(Resolve(_transformRules.TablePostfix, table, _render.Counter.TableId)); + } + } + #endregion + + #region List + public void EnterList(List list) + { + _render.Counter.IncListId(); + if (_transformRules.ListPrefix != null) + { + WriteText(Resolve(_transformRules.ListPrefix, list, 0)); + } + } + public void EnterListItem(List list, IContentElement item, int order) + { + _render.Counter.IncListItemId(); + if (_transformRules.ListItemPrefix != null) + { + WriteText(Resolve(_transformRules.ListItemPrefix, item, order)); + } + } + public void LeaveList(List list) + { + if (_transformRules.ListPostfix != null) + { + WriteText(Resolve(_transformRules.ListPostfix, list, 0)); + } + } + public void LeaveListItem(List list, IContentElement item, int order) + { + if (_transformRules.ListItemPostfix != null) + { + WriteText(Resolve(_transformRules.ListItemPostfix, item, order)); + } + } + #endregion + + #region Media + public void EnterAudioplayer(Audioplayer player) + { + _render.Counter.IncAudioplayerId(); + if (_transformRules.AudioplayerPrefix != null) + { + WriteText(Resolve(_transformRules.AudioplayerPrefix, player, 0)); + } + } + public void EnterGallery(Gallery gallery) + { + _render.Counter.IncGalleryId(); + if (_transformRules.GalleryPrefix != null) + { + WriteText(Resolve(_transformRules.GalleryPrefix, gallery, 0)); + } + } + public void EnterVideoplayer(Videoplayer player) + { + _render.Counter.IncVideoplayerId(); + if (_transformRules.VideoplayerPrefix != null) + { + WriteText(Resolve(_transformRules.VideoplayerPrefix, player, 0)); + } + } + public void LeaveAudioplayer(Audioplayer player) + { + if (_transformRules.AudioplayerPostfix != null) + { + WriteText(Resolve(_transformRules.AudioplayerPostfix, player, 0)); + } + } + public void LeaveGallery(Gallery gallery) + { + if (_transformRules.GalleryPostfix != null) + { + WriteText(Resolve(_transformRules.GalleryPostfix, gallery, 0)); + } + } + public void LeaveVideoplayer(Videoplayer player) + { + if (_transformRules.VideoplayerPostfix != null) + { + WriteText(Resolve(_transformRules.VideoplayerPostfix, player, 0)); + } + } + #endregion + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Services/TRender.cs b/ZeroLevel/Services/DOM/DSL/Services/TRender.cs new file mode 100644 index 0000000..3172752 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Services/TRender.cs @@ -0,0 +1,435 @@ +using DOM.DSL.Model; +using DOM.DSL.Tokens; +using DOM.Services; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ZeroLevel; +using ZeroLevel.DocumentObjectModel; + +namespace DOM.DSL.Services +{ + internal class CustomBlocks + { + private readonly IDictionary> _blocks = + new Dictionary>(); + + public void Append(string name, IEnumerable tokens) + { + if (false == _blocks.ContainsKey(name)) + _blocks.Add(name, tokens); + else + { + _blocks[name] = null; + _blocks[name] = tokens; + } + } + + public IEnumerable Get(string name) + { + if (_blocks.ContainsKey(name)) + return _blocks[name]; + return Enumerable.Empty(); + } + + public bool Exists(string name) + { + return _blocks.ContainsKey(name); + } + } + + internal class TRender + { + private readonly Document _document; + private readonly TEnvironment _environment; + private readonly DOMRenderElementCounter _counter; + private readonly TRenderOptions _options = new TRenderOptions(); + private readonly TContainerFactory _factory; + private readonly IDictionary _buffer = + new Dictionary(); + + private readonly CustomBlocks _blocks = new CustomBlocks(); + + internal TContainerFactory Factory + { + get + { + return _factory; + } + } + + internal IDictionary BufferDictionary + { + get + { + return _buffer; + } + } + + internal DOMRenderElementCounter Counter + { + get + { + return _counter; + } + } + + internal TRenderOptions Options + { + get + { + return _options; + } + } + + public TRender(Document document, TEnvironment environment) + { + _document = document; + _environment = environment; + _counter = new DOMRenderElementCounter(); + _factory = new TContainerFactory(this); + } + + public void Resolve(TToken token, Action handler, bool release = true, TContainer self = null) + { + var self_copy = self == null ? null : _factory.Get(self.Current, self.Index); + try + { + if (token is TTextToken) + { + var container = _factory.Get(token.AsTextToken().Text); + handler(container); + if (release) _factory.Release(container); + } + else if (token is TElementToken) + { + var containers = ResolveElementToken(token.AsElementToken(), self_copy); + foreach (var c in containers) + { + handler(c); + if (release) _factory.Release(c); + } + } + else if (token is TBlockToken) + { + var containers = ResolveBlockToken(token.AsBlockToken(), self_copy); + foreach (var c in containers) + { + handler(c); + if (release) _factory.Release(c); + } + } + } + finally + { + _factory.Release(self_copy); + } + } + + private TContainer[] ResolveElementToken(TElementToken token, TContainer self = null) + { + TContainer container = null; + switch (token.ElementName.Trim().ToLowerInvariant()) + { + // External + case "now": container = _factory.Get(DateTime.Now); break; + case "utc": container = _factory.Get(DateTime.Now.ToUniversalTime()); break; + case "guid": container = _factory.Get(Guid.NewGuid()); break; + case "nowutc": container = _factory.Get(DateTime.UtcNow); break; + + // Document + case "id": container = _factory.Get(_document.Id); break; + case "summary": container = _factory.Get(_document.Summary); break; + case "header": container = _factory.Get(_document.Header); break; + case "categories": container = _factory.Get(_document.Categories); break; + case "directions": + container = _factory.Get(_document.Categories. + Select(c => c.DirectionCode).Distinct().ToList()); break; + + // Descriptive + case "desc": + case "descriptive": container = _factory.Get(_document.DescriptiveMetadata); break; + case "author": container = _factory.Get(_document.DescriptiveMetadata.Byline); break; + case "copyright": container = _factory.Get(_document.DescriptiveMetadata.CopyrightNotice); break; + case "created": container = _factory.Get(_document.DescriptiveMetadata.Created); break; + case "lang": container = _factory.Get(_document.DescriptiveMetadata.Language); break; + case "priority": container = _factory.Get(_document.DescriptiveMetadata.Priority); break; + case "source": container = _factory.Get(_document.DescriptiveMetadata.Source); break; + case "publisher": container = _factory.Get(_document.DescriptiveMetadata.Publisher); break; + case "meta": + case "headers": container = _factory.Get(_document.DescriptiveMetadata.Headers); break; + + // Identifier + case "identifier": container = _factory.Get(_document.Identifier); break; + case "timestamp": container = _factory.Get(_document.Identifier.Timestamp); break; + case "datelabel": container = _factory.Get(_document.Identifier.DateLabel); break; + case "version": container = _factory.Get(_document.Identifier.Version); break; + + // Tags + case "tags": container = _factory.Get(_document.TagMetadata); break; + case "keywords": container = _factory.Get(_document.TagMetadata.Keywords); break; + case "companies": container = _factory.Get(_document.TagMetadata.Companies); break; + case "persons": container = _factory.Get(_document.TagMetadata.Persons); break; + case "places": container = _factory.Get(_document.TagMetadata.Places); break; + + case "var": container = _factory.Get(_environment.CustomVariables); break; + case "buf": container = _factory.Get(_buffer); break; + case "env": container = _factory.Get(_environment); break; + case "counter": container = _factory.Get(_counter); break; + case "self": container = _factory.Get(self.Current, self.Index); break; + case "content": container = _factory.Get(new TContentElement(_document)); break; + case "aside": container = _factory.Get(_document.Aside); break; + case "assotiations": container = _factory.Get(_document.Assotiations); break; + case "null": container = _factory.Get(null); break; + case "empty": container = _factory.Get(string.Empty); break; + + // Blocks + case "build": + { + if (token.NextToken is TPropertyToken) + { + var block = new TBlockToken(_blocks.Get(token.NextToken.AsPropertyToken().PropertyName)); + var result = ResolveBlockToken(block, self); + container = _factory.Get(result.Where(c => c.Current != null).Select(c => c.Current).ToList()); + foreach (var c in result) + _factory.Release(c); + } + break; + } + } + + if (container == null) container = _factory.Get(null); + + if (token.NextToken is TPropertyToken) + { + return new[] { ResolvePropertyToken(token.NextToken.AsPropertyToken(), container) }; + } + else if (token.NextToken is TFunctionToken) + { + return new[] { ResolveFunctionToken(token.NextToken.AsFunctionToken(), container) }; + } + else if (token.NextToken is TSystemToken) + { + ApplyRenderCommand(token.NextToken.AsSystemToken()); + } + return new[] { container }; + } + + private TContainer ResolvePropertyToken(TPropertyToken token, TContainer container) + { + string property_index = null; + Resolve(token.PropertyIndex, c => property_index = c.ToString()); + container.MoveToProperty(token.PropertyName, property_index); + if (token.NextToken is TPropertyToken) + { + return ResolvePropertyToken(token.NextToken.AsPropertyToken(), container); + } + else if (token.NextToken is TFunctionToken) + { + return ResolveFunctionToken(token.NextToken.AsFunctionToken(), container); + } + return container; + } + + private TContainer[] CalculateArguments(TFunctionToken token, TContainer self) + { + List args = new List(); + foreach (var a in token?.FunctionArgs ?? Enumerable.Empty()) + { + Resolve(a, c => args.Add(c), false, self); + } + return args.ToArray(); + } + + private TContainer ResolveFunctionToken(TFunctionToken token, TContainer container) + { + var args_calc = new Func(self => CalculateArguments(token, self)); + container.ApplyFunction(token.FunctionName, args_calc); + if (token.NextToken is TPropertyToken) + { + container = ResolvePropertyToken(token.NextToken.AsPropertyToken(), container); + } + else if (token.NextToken is TFunctionToken) + { + container = ResolveFunctionToken(token.NextToken.AsFunctionToken(), container); + } + return container; + } + + private IEnumerable ResolveBlockToken(TBlockToken blockToken, TContainer self_parent = null) + { + switch (blockToken.Name) + { + case "block": + { + string name = null; + Resolve(blockToken.Condition, c => name = c.ToString(), true); + if (false == string.IsNullOrWhiteSpace(name)) + { + _blocks.Append(name, blockToken.Body); + return Enumerable.Empty(); + } + } + break; + case "flow": + { + return new List + { + ResolveFlowBlockToken(blockToken.Body) + }; + } + case "if": + { + bool success = false; + Resolve(blockToken.Condition, c => success = c.As(), true, self_parent); + List result; + if (success) + { + var ls = self_parent == null ? null : _factory.Get(self_parent.Current, self_parent.Index); + result = ResolveSimpleBlockToken(blockToken, ls); + _factory.Release(ls); + } + else + { + result = new List { _factory.Get(null) }; + } + return result; + } + case "for": + { + var list = new List(); + TContainer self_container = null; + Resolve(blockToken.Condition, c => self_container = c, false, self_parent); + if (self_container != null) + { + if (self_container.IsEnumerable) + { + int index = 0; + foreach (var t in (IEnumerable)self_container.Current) + { + var self = _factory.Get(t, index); + foreach (var bt in blockToken.Body) + { + Resolve(bt, c => list.Add(c), false, self); + } + _factory.Release(self); + index++; + } + } + else + { + foreach (var bt in blockToken.Body) + { + Resolve(bt, c => list.Add(c), false, self_container); + } + } + } + _factory.Release(self_container); + return list; + } + } + return ResolveSimpleBlockToken(blockToken, self_parent); + } + + private List ResolveSimpleBlockToken(TBlockToken token, TContainer self = null) + { + var block = new List(); + foreach (var t in token.Body) + { + Resolve(t, c => block.Add(c), false, self); + } + return block; + } + + private TContainer ResolveFlowBlockToken(IEnumerable tokens) + { + var rules = new TFlowRules(); + rules.Bootstrap(); + foreach (var token in tokens) + { + if (token is TElementToken) + { + var function = token.AsElementToken()?.NextToken?.AsFunctionToken(); + var elementName = token.AsElementToken()?.ElementName; + var functionName = function?.FunctionName; + var rule_token = function?.FunctionArgs == null ? + null : + new TBlockToken(function.FunctionArgs.Select(a => a.Clone())); + string special = null; + if (functionName.Equals("special", StringComparison.OrdinalIgnoreCase)) + { + var args = new List(); + foreach (var a in function.FunctionArgs) + { + Resolve(a, c => args.Add(c), false); + } + special = string.Join(",", args.Select(a => a.ToString())); + foreach (var a in args) + _factory.Release(a); + } + rules.UpdateRule(elementName, functionName, rule_token, special); + } + } + return _factory.Get(DocumentContentReader.ReadAs(_document, new TContentToStringConverter(this, rules))); + } + + private void ApplyRenderCommand(TSystemToken token) + { + List _args = new List(); + foreach (var a in token.Arg?.AsFunctionToken()?.FunctionArgs ?? Enumerable.Empty()) + { + Resolve(a, c => _args.Add(c), false); + } + var args = _args.ToArray(); + switch (token.Command) + { + case "log": + if (args?.Length > 0) + { + Log.Debug(args[0].ToString(), args.Skip(1).ToArray()); + } + break; + case "validate": + if (args?.Length == 1) + { + switch (args[0].ToString().Trim().ToLowerInvariant()) + { + case "xml": + _options.ValidateAsXml = true; + break; + case "html": + _options.ValidateAsHtml = true; + break; + case "json": + _options.ValidateAsJson = true; + break; + } + } + break; + case "fixwidth": + { + if (args?.Length == 1) + { + int width; + if (int.TryParse(args[0].ToString(), out width)) + { + _options.MaxStringWidth = width; + } + else + { + _options.MaxStringWidth = -1; + } + } + else + { + _options.MaxStringWidth = -1; + } + } + break; + } + foreach (var a in args) + _factory.Release(a); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Services/TStringReader.cs b/ZeroLevel/Services/DOM/DSL/Services/TStringReader.cs new file mode 100644 index 0000000..e1ea38f --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Services/TStringReader.cs @@ -0,0 +1,104 @@ +namespace DOM.DSL.Services +{ + public class TStringReader + { + private int _position; + private readonly string _template; + + public bool EOF => _position >= _template?.Length; + public bool StartPosition => _position == 0; + public bool LastPosition => _position == _template?.Length - 1; + public char Current => EOF ? char.MinValue : _template[_position]; + public char Next => EOF || LastPosition ? char.MinValue : _template[_position + 1]; + public char Preview => StartPosition ? char.MinValue : _template[_position - 1]; + + public TStringReader(string template) + { + _template = template; + _position = 0; + } + + public bool Move(int offset = 1) + { + if (EOF) return false; + if (LastPosition) { _position = _template.Length; return false; } + _position += offset; + if (_position >= _template.Length) + { + _position = _template.Length; + } + return true; + } + + public int SkipSpaces() + { + int count = 0; + while (EOF == false && char.IsWhiteSpace(Current)) { Move(); count++; } + return count; + } + + public void SkipBreaks() + { + while (EOF == false && char.IsWhiteSpace(Current)) Move(); + } + + public bool MoveBack() + { + _position = _position - 1; + if (_position < 0) + { + _position = 0; + return false; + } + return true; + } + + public int FindOffsetTo(char symbol) + { + if (_position == -1 || EOF || LastPosition) return -1; + var search_position = _position; + var sym = _template[search_position]; + while (search_position < _template.Length && false == sym.Equals(symbol)) + { + search_position++; + sym = _template[search_position]; + } + return sym.Equals(symbol) ? search_position - _position : -1; + } + + public bool Test(char sym, int offset = 0) + { + var index = _position + offset; + if (index < 0 || index >= _template.Length) return false; + return _template[index].Equals(sym); + } + + public string ReadIdentity() + { + string identity = string.Empty; + var offset = _position; + if (offset < _template.Length && char.IsLetter(_template[offset])) + { + var index = offset + 1; + while (index < _template.Length && (char.IsLetterOrDigit(_template[index]) || _template[index] == '_' || _template[index] == '-')) + index++; + identity = _template.Substring(offset, index - offset); + } + return identity.ToLowerInvariant(); + } + + public string ReadWord() + { + string identity = string.Empty; + var offset = _position; + if (offset < _template.Length && char.IsLetterOrDigit(_template[offset])) + { + var index = offset + 1; + while (index < _template.Length && char.IsLetterOrDigit(_template[index])) + index++; + identity = _template.Substring(offset, index - offset); + } + return identity; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/TEngine.cs b/ZeroLevel/Services/DOM/DSL/TEngine.cs new file mode 100644 index 0000000..fa76c46 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/TEngine.cs @@ -0,0 +1,81 @@ +using DOM.DSL.Contexts; +using DOM.DSL.Model; +using DOM.DSL.Services; +using DOM.DSL.Tokens; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ZeroLevel; +using ZeroLevel.DocumentObjectModel; + +namespace DOM.DSL +{ + public static class TEngine + { + public static IEnumerable Parse(string template) + { + if (false == string.IsNullOrEmpty(template)) + { + IEnumerable tokens; + try + { + var reader = new TStringReader(template); + var context = new TRootContext(); + context.Read(reader); + tokens = context.Complete(); + return tokens; + } + catch (Exception ex) + { + Log.SystemError(ex, "Fault parse template '{0}' to token list", template); + } + } + return Enumerable.Empty(); + } + + public static string Apply(Document document, IEnumerable tokens, TEnvironment environment, bool ignore_fault = true) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + if (tokens == null) + { + throw new ArgumentNullException(nameof(tokens)); + } + var text = new StringBuilder(); + try + { + var resolver = new TRender(document, environment); + foreach (var token in tokens) + { + resolver.Resolve(token, c => + { + var line = c.ToString(); + if (string.IsNullOrEmpty(line) == false) + { + if (resolver.Options.MaxStringWidth > 0) + { + text.Append(TRenderUtils.SplitOn(line, resolver.Options.MaxStringWidth)); + } + else + { + text.Append(line); + } + } + }); + } + } + catch (Exception ex) + { + Log.Debug("Fault transform document '{0} by token list'. {1}", document.Id, ex); + if (ignore_fault == false) + { + throw ex; + } + } + return text.ToString(); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Tokens/TBlockToken.cs b/ZeroLevel/Services/DOM/DSL/Tokens/TBlockToken.cs new file mode 100644 index 0000000..539bd6c --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Tokens/TBlockToken.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DOM.DSL.Tokens +{ + public class TBlockToken : + TToken + { + public string Name { get; } + public TToken Condition { get; } + public IEnumerable Body { get; } + + public TBlockToken(string name, + TToken condition, + IEnumerable body) + { + Name = name; + Condition = condition?.Clone(); + Body = body.Select(b => b.Clone()).ToArray(); + } + + public TBlockToken(IEnumerable body) + { + Name = string.Empty; + Condition = null; + Body = body.Select(b => b.Clone()).ToArray(); + } + + public override TToken Clone() + { + return new TBlockToken(this.Name, this.Condition?.Clone(), this.Body?.Select(b => b.Clone()).ToArray()); + } + + public override TToken CloneLocal() + { + return Clone(); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Tokens/TElementToken.cs b/ZeroLevel/Services/DOM/DSL/Tokens/TElementToken.cs new file mode 100644 index 0000000..7cb580c --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Tokens/TElementToken.cs @@ -0,0 +1,36 @@ +namespace DOM.DSL.Tokens +{ + /// + /// Токен ссылающийся на элемент документа + /// + public class TElementToken : + TToken + { + /// + /// Имя элемента + /// + public string ElementName; + /// + /// Опционально, при наличии свойств и/или функций для текущего элемента + /// + public TToken NextToken; + + public override TToken Clone() + { + return new TElementToken + { + ElementName = this.ElementName, + NextToken = this.NextToken?.Clone() + }; + } + + public override TToken CloneLocal() + { + return new TElementToken + { + ElementName = this.ElementName, + NextToken = null + }; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Tokens/TFunctionToken.cs b/ZeroLevel/Services/DOM/DSL/Tokens/TFunctionToken.cs new file mode 100644 index 0000000..c968f0b --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Tokens/TFunctionToken.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DOM.DSL.Tokens +{ + public class TFunctionToken : + TToken + { + public string FunctionName; + public IEnumerable FunctionArgs; + public TToken NextToken; + + public override TToken Clone() + { + return new TFunctionToken + { + FunctionArgs = FunctionArgs.Select(a => a.Clone()).ToArray(), + FunctionName = this.FunctionName, + NextToken = this.NextToken?.Clone() + }; + } + + public override TToken CloneLocal() + { + return new TFunctionToken + { + FunctionArgs = FunctionArgs.Select(a => a.Clone()).ToArray(), + FunctionName = this.FunctionName, + NextToken = null + }; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Tokens/TPropertyToken.cs b/ZeroLevel/Services/DOM/DSL/Tokens/TPropertyToken.cs new file mode 100644 index 0000000..ef84922 --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Tokens/TPropertyToken.cs @@ -0,0 +1,30 @@ +namespace DOM.DSL.Tokens +{ + public class TPropertyToken : + TToken + { + public string PropertyName; + public TToken PropertyIndex; + public TToken NextToken; + + public override TToken Clone() + { + return new TPropertyToken + { + PropertyIndex = this.PropertyIndex, + PropertyName = this.PropertyName, + NextToken = this.NextToken?.Clone() + }; + } + + public override TToken CloneLocal() + { + return new TPropertyToken + { + PropertyIndex = this.PropertyIndex, + PropertyName = this.PropertyName, + NextToken = null + }; + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Tokens/TSystemToken.cs b/ZeroLevel/Services/DOM/DSL/Tokens/TSystemToken.cs new file mode 100644 index 0000000..a28d2ac --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Tokens/TSystemToken.cs @@ -0,0 +1,23 @@ +namespace DOM.DSL.Tokens +{ + public class TSystemToken : + TToken + { + public string Command; + public TToken Arg; + + public override TToken Clone() + { + return new TSystemToken + { + Command = this.Command, + Arg = this.Arg?.Clone() + }; + } + + public override TToken CloneLocal() + { + return Clone(); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Tokens/TTextToken.cs b/ZeroLevel/Services/DOM/DSL/Tokens/TTextToken.cs new file mode 100644 index 0000000..eb6c05c --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Tokens/TTextToken.cs @@ -0,0 +1,24 @@ +namespace DOM.DSL.Tokens +{ + /// + /// Текстовый токен + /// + public class TTextToken : + TToken + { + /// + /// Текст в шаблоне, переносимый в результат без обработки + /// + public string Text; + + public override TToken Clone() + { + return new TTextToken { Text = this.Text }; + } + + public override TToken CloneLocal() + { + return Clone(); + } + } +} diff --git a/ZeroLevel/Services/DOM/DSL/Tokens/TToken.cs b/ZeroLevel/Services/DOM/DSL/Tokens/TToken.cs new file mode 100644 index 0000000..64b97ee --- /dev/null +++ b/ZeroLevel/Services/DOM/DSL/Tokens/TToken.cs @@ -0,0 +1,23 @@ +using DOM.DSL.Contracts; + +namespace DOM.DSL.Tokens +{ + /// + /// Абстрактная единица шаблона + /// + public abstract class TToken : TCloneable + { + public abstract TToken Clone(); + /// + /// Копия с установкой NextToken в null, для предотвращения циклических расчетов + /// + /// + public abstract TToken CloneLocal(); + public TElementToken AsElementToken() => this as TElementToken; + public TFunctionToken AsFunctionToken() => this as TFunctionToken; + public TTextToken AsTextToken() => this as TTextToken; + public TBlockToken AsBlockToken() => this as TBlockToken; + public TPropertyToken AsPropertyToken() => this as TPropertyToken; + public TSystemToken AsSystemToken() => this as TSystemToken; + } +} diff --git a/ZeroLevel/Services/DOM/Model/Agency.cs b/ZeroLevel/Services/DOM/Model/Agency.cs new file mode 100644 index 0000000..0fcf263 --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/Agency.cs @@ -0,0 +1,42 @@ +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel +{ + public class Agency : IBinarySerializable + { + /// + /// Название источника + /// + public string Title; + /// + /// Ссылка на источник + /// + public string Url; + /// + /// Описание источника + /// + public string Description; + + public Agency() { } + public Agency(IBinaryReader reader) { Deserialize(reader); } + public Agency(string title) { this.Title = title; } + public Agency(string title, string url) { this.Title = title; this.Url = url; } + public Agency(string title, string url, string description) { this.Title = title; this.Url = url; this.Description = description; } + + #region IBinarySerializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteString(this.Title); + writer.WriteString(this.Url); + writer.WriteString(this.Description); + } + + public void Deserialize(IBinaryReader reader) + { + this.Title = reader.ReadString(); + this.Url = reader.ReadString(); + this.Description = reader.ReadString(); + } + #endregion + } +} diff --git a/ZeroLevel/Services/DOM/Model/AsideContent.cs b/ZeroLevel/Services/DOM/Model/AsideContent.cs new file mode 100644 index 0000000..87910b5 --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/AsideContent.cs @@ -0,0 +1,55 @@ +using System; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel +{ + public sealed class AsideContent + : IBinarySerializable + { + /// + /// Идентификатор + /// + public string Identity; + /// + /// Название содержимого + /// + public string Caption; + /// + /// Описание (опционально) + /// + public string Summary; + /// + /// Тип содержимого + /// + public ContentType ContentType; + /// + /// Содержимое в бинарном представлении + /// + public byte[] Payload; + + public AsideContent() { } + public AsideContent(IBinaryReader reader) { Deserialize(reader); } + public AsideContent(string identity, string caption, string description) + { Identity = identity; Summary = description; Caption = caption; } + + #region IBinarySerializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteString(this.Identity); + writer.WriteString(this.Caption); + writer.WriteString(this.Summary); + writer.WriteInt32((Int32)this.ContentType); + writer.WriteBytes(this.Payload); + } + + public void Deserialize(IBinaryReader reader) + { + this.Identity = reader.ReadString(); + this.Caption = reader.ReadString(); + this.Summary = reader.ReadString(); + this.ContentType = (ContentType)reader.ReadInt32(); + this.Payload = reader.ReadBytes(); + } + #endregion + } +} diff --git a/ZeroLevel/Services/DOM/Model/Assotiation.cs b/ZeroLevel/Services/DOM/Model/Assotiation.cs new file mode 100644 index 0000000..03a5cbb --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/Assotiation.cs @@ -0,0 +1,46 @@ +using System; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel +{ + public sealed class Assotiation : + IBinarySerializable + { + #region Fields + /// + /// Заголовок + /// + public string Title; + /// + /// Описание (например, что было изменено по сравнению с прошлой версией) + /// + public string Description; + /// + /// Ссылка на связанный документ + /// + public Guid DocumentId; + /// + /// Тип связи + /// + public AssotiationRelation Relation; + #endregion + + #region IBinarySerializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteString(this.Title); + writer.WriteString(this.Description); + writer.WriteGuid(this.DocumentId); + writer.WriteInt32((Int32)this.Relation); + } + + public void Deserialize(IBinaryReader reader) + { + this.Title = reader.ReadString(); + this.Description = reader.ReadString(); + this.DocumentId = reader.ReadGuid(); + this.Relation = (AssotiationRelation)reader.ReadInt32(); + } + #endregion + } +} diff --git a/ZeroLevel/Services/DOM/Model/AssotiationRelation.cs b/ZeroLevel/Services/DOM/Model/AssotiationRelation.cs new file mode 100644 index 0000000..066b6ff --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/AssotiationRelation.cs @@ -0,0 +1,28 @@ +using System; + +namespace ZeroLevel.DocumentObjectModel +{ + public enum AssotiationRelation : Int32 + { + /// + /// Тип отношения не определен + /// + Uncknown = 0, + /// + /// Упоминается + /// + Mentions = 1, + /// + /// Рассказывается о + /// + About = 2, + /// + /// Обновление предыдущей версии + /// + UpdateOf = 3, + /// + /// Основано на + /// + BasedOn = 4 + } +} diff --git a/ZeroLevel/Services/DOM/Model/Category.cs b/ZeroLevel/Services/DOM/Model/Category.cs new file mode 100644 index 0000000..a790eb0 --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/Category.cs @@ -0,0 +1,137 @@ +using System; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel +{ + public sealed class Category : + IBinarySerializable, + IEquatable, + ICloneable + { + #region Ctors + + public Category() + { + } + + public Category(string title, string code, string direction_code, string description = null, + bool is_system = false) + { + this.Title = title; + this.Description = description; + this.Code = code; + this.DirectionCode = direction_code; + this.IsSystem = is_system; + } + + public Category(Category other) + { + this.Title = other.Title; + this.Description = other.Description; + this.Code = other.Code; + this.DirectionCode = other.DirectionCode; + this.IsSystem = other.IsSystem; + } + + public Category(IBinaryReader reader) + { + Deserialize(reader); + } + + #endregion + + #region Fields + + /// + /// Название + /// + public string Title { get; set; } + + /// + /// Описание + /// + public string Description { get; set; } + + /// + /// Код категории + /// + public string Code { get; set; } + + /// + /// Код направления + /// + public string DirectionCode { get; set; } + + /// + /// Указывает на принадлежность категории внутренней части системы + /// Категория не предназначенна для публичного доступа + /// + public bool IsSystem { get; set; } + + #endregion + + #region IBinarySerializable + + public void Serialize(IBinaryWriter writer) + { + writer.WriteString(this.Title); + writer.WriteString(this.Code); + writer.WriteString(this.Description); + writer.WriteString(this.DirectionCode); + writer.WriteBoolean(this.IsSystem); + } + + public void Deserialize(IBinaryReader reader) + { + this.Title = reader.ReadString(); + this.Code = reader.ReadString(); + this.Description = reader.ReadString(); + this.DirectionCode = reader.ReadString(); + this.IsSystem = reader.ReadBoolean(); + } + + #endregion + + #region ICloneable + + public object Clone() + { + return new Category(this); + } + + #endregion + + #region IEquatable + + public bool Equals(Category other) + { + if (other == null) return false; + if (string.Compare(this.Title, other.Title, StringComparison.Ordinal) != 0) return false; + if (string.Compare(this.Code, other.Code, StringComparison.Ordinal) != 0) return false; + if (string.Compare(this.Description, other.Description, StringComparison.Ordinal) != 0) return false; + if (string.Compare(this.DirectionCode, other.DirectionCode, StringComparison.Ordinal) != 0) return false; + if (this.IsSystem != other.IsSystem) return false; + return true; + } + + #endregion + + #region Equals & Hash + + public override bool Equals(object obj) + { + return this.Equals(obj as Category); + } + + public override int GetHashCode() + { + return Title?.GetHashCode() ?? 0 ^ + Description?.GetHashCode() ?? 0 ^ + Code?.GetHashCode() ?? 0 ^ + DirectionCode?.GetHashCode() ?? 0 ^ + IsSystem.GetHashCode(); + } + + #endregion + } +} \ No newline at end of file diff --git a/ZeroLevel/Services/DOM/Model/ContentType.cs b/ZeroLevel/Services/DOM/Model/ContentType.cs new file mode 100644 index 0000000..b998b6a --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/ContentType.cs @@ -0,0 +1,17 @@ +using System; + +namespace ZeroLevel.DocumentObjectModel +{ + public enum ContentType: Int32 + { + Raw = 0, + Text = 1, + Audio = 2, + Video = 3, + Image = 4, + Docx = 5, + Xlsx = 6, + Zip = 7, + Pdf = 8 + } +} diff --git a/ZeroLevel/Services/DOM/Model/DescriptiveMetadata.cs b/ZeroLevel/Services/DOM/Model/DescriptiveMetadata.cs new file mode 100644 index 0000000..a07cf71 --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/DescriptiveMetadata.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel +{ + public sealed class DescriptiveMetadata : + IBinarySerializable + { + public DescriptiveMetadata() { Initialize(); } + public DescriptiveMetadata(IBinaryReader reader) { Deserialize(reader); } + + private void Initialize() + { + Created = DateTime.Now; + Headers = new List
(); + Priority = Priority.Normal; + Source = new Agency(); + Publisher = new Agency(); + Original = new Tag(); + Language = "ru"; + } + + #region Fields + /// + /// Авторы (подпись) + /// + public string Byline; + /// + /// Копирайт + /// + public string CopyrightNotice; + /// + /// Дата создания + /// + public DateTime Created; + /// + /// Основной язык + /// + public string Language; + /// + /// Важность + /// + public Priority Priority; + /// + /// Источник документа (например, информационное агентство) + /// + public Agency Source; + /// + /// Издатель (Агентство) + /// + public Agency Publisher; + /// + /// Ссылка на оригинальную новость, если текущая создана на ее основе + /// + public Tag Original; + /// + /// Опциональные заголовки + /// + public List
Headers; + #endregion + + #region IBinarySerializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteString(this.Byline); + writer.WriteString(this.CopyrightNotice); + writer.WriteDateTime(this.Created); + writer.WriteCollection
(this.Headers); + writer.WriteString(this.Language); + this.Original.Serialize(writer); + writer.WriteInt32((Int32)this.Priority); + this.Publisher.Serialize(writer); + this.Source.Serialize(writer); + } + + public void Deserialize(IBinaryReader reader) + { + this.Byline = reader.ReadString(); + this.CopyrightNotice = reader.ReadString(); + this.Created = reader.ReadDateTime().Value; + this.Headers = reader.ReadCollection
(); + this.Language = reader.ReadString(); + this.Original = new Tag(); + this.Original.Deserialize(reader); + this.Priority = (Priority)reader.ReadInt32(); + this.Publisher = new Agency(); + this.Publisher.Deserialize(reader); + this.Source = new Agency(); + this.Source.Deserialize(reader); + } + #endregion + } +} diff --git a/ZeroLevel/Services/DOM/Model/Document.cs b/ZeroLevel/Services/DOM/Model/Document.cs new file mode 100644 index 0000000..4c22f08 --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/Document.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel +{ + public class Document + : IBinarySerializable + { + private static readonly Document _empty = new Document(); + public static Document Empty + { + get + { + var data = MessageSerializer.Serialize(_empty); + return MessageSerializer.Deserialize(data); + } + } + + public Document() { Id = Guid.NewGuid(); Initialize(); } + public Document(Guid id) { Id = id; Initialize(); } + public Document(IBinaryReader reader) { Deserialize(reader); } + public Document(Document other) + { + var data = MessageSerializer.Serialize(other); + using (var reader = new MemoryStreamReader(data)) + { + Deserialize(reader); + } + } + + private void Initialize() + { + Identifier = new Identifier(); + Content = new FlowContent(); + TagMetadata = new TagMetadata(); + DescriptiveMetadata = new DescriptiveMetadata(); + Aside = new List(); + Assotiations = new List(); + Categories = new List(); + } + + /// + /// Идентификатор документа + /// + public Guid Id; + /// + /// Краткое описание, лид + /// + public string Summary; + /// + /// Заголовок + /// + public string Header; + /// + /// Дополнительные идентификаторы + /// + public Identifier Identifier; + /// + /// Содержимое документа + /// + public FlowContent Content; + /// + /// Теги + /// + public TagMetadata TagMetadata; + /// + /// Метаданные документа + /// + public DescriptiveMetadata DescriptiveMetadata; + /// + /// Вложенные документы + /// + public List Aside; + /// + /// Связанные документы + /// + public List Assotiations; + /// + /// Категории + /// + public List Categories; + + + #region IBinarySerializable + public void Serialize(IBinaryWriter writer) + { + writer.WriteGuid(this.Id); + writer.WriteString(this.Summary); + writer.WriteString(this.Header); + + writer.Write(this.Identifier); + writer.Write(this.Content); + writer.Write(this.TagMetadata); + writer.Write(this.DescriptiveMetadata); + + writer.WriteCollection(this.Aside); + writer.WriteCollection(this.Assotiations); + writer.WriteCollection(this.Categories); + } + + public void Deserialize(IBinaryReader reader) + { + this.Id = reader.ReadGuid(); + this.Summary = reader.ReadString(); + this.Header = reader.ReadString(); + + this.Identifier = reader.Read(); + this.Content = reader.Read(); + this.TagMetadata = reader.Read(); + this.DescriptiveMetadata = reader.Read(); + + this.Aside = reader.ReadCollection(); + this.Assotiations = reader.ReadCollection(); + this.Categories = reader.ReadCollection(); + } + #endregion + } +} diff --git a/ZeroLevel/Services/DOM/Model/Flow/Audio.cs b/ZeroLevel/Services/DOM/Model/Flow/Audio.cs new file mode 100644 index 0000000..672179e --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/Flow/Audio.cs @@ -0,0 +1,43 @@ +using System; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel.Flow +{ + public sealed class Audio : + ContentElement + { + public SourceType Source { get; set; } + /// + /// Название + /// + public string Title; + /// + /// Ссылка или идентификатор вложения + /// + public string Identifier; + + public Audio() : base(ContentElementType.Audio) + { + } + + public Audio(IBinaryReader reader) : + base(ContentElementType.Audio) + { + Deserialize(reader); + } + + public override void Deserialize(IBinaryReader reader) + { + Source = (SourceType)reader.ReadInt32(); + Title = reader.ReadString(); + Identifier = reader.ReadString(); + } + + public override void Serialize(IBinaryWriter writer) + { + writer.WriteInt32((Int32)Source); + writer.WriteString(Title); + writer.WriteString(Identifier); + } + } +} diff --git a/ZeroLevel/Services/DOM/Model/Flow/Audioplayer.cs b/ZeroLevel/Services/DOM/Model/Flow/Audioplayer.cs new file mode 100644 index 0000000..e994763 --- /dev/null +++ b/ZeroLevel/Services/DOM/Model/Flow/Audioplayer.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.DocumentObjectModel.Flow +{ + public sealed class Audioplayer : + ContentElement + { + public Text Title; + public List