From 3a2f80250093fa19fe6eb1990ee21f3cdfe1d376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9C=B0=E5=B9=B3=E7=BA=BF?= <22250530@qq.com> Date: Wed, 11 Mar 2015 17:49:20 +0800 Subject: [PATCH] --- bin/shutdown.bat | 7 + bin/shutdown.sh | 16 + bin/start.bat | 7 + bin/start.sh | 22 + conf/application.xml | 10 + conf/logging.properties | 21 + conf/persistence.xml | 15 + lib/javax.persistence_2.1.0.jar | Bin 0 -> 164637 bytes src/META-INF/application-template.xml | 144 ++ src/META-INF/logging-template.properties | 16 + src/com/wentch/redkale/boot/Application.java | 512 ++++ src/com/wentch/redkale/boot/ClassFilter.java | 319 +++ .../wentch/redkale/boot/LogFileHandler.java | 230 ++ .../wentch/redkale/boot/NodeHttpServer.java | 83 + src/com/wentch/redkale/boot/NodeServer.java | 250 ++ .../wentch/redkale/boot/NodeSncpServer.java | 50 + .../wentch/redkale/convert/AnyEncoder.java | 40 + .../wentch/redkale/convert/ArrayDecoder.java | 79 + .../wentch/redkale/convert/ArrayEncoder.java | 75 + .../redkale/convert/CollectionDecoder.java | 69 + .../redkale/convert/CollectionEncoder.java | 65 + src/com/wentch/redkale/convert/Convert.java | 26 + .../wentch/redkale/convert/ConvertColumn.java | 44 + .../redkale/convert/ConvertColumnEntry.java | 67 + .../redkale/convert/ConvertColumns.java | 24 + .../redkale/convert/ConvertException.java | 28 + .../wentch/redkale/convert/ConvertType.java | 28 + src/com/wentch/redkale/convert/DeMember.java | 64 + .../wentch/redkale/convert/Decodeable.java | 27 + src/com/wentch/redkale/convert/EnMember.java | 60 + .../wentch/redkale/convert/Encodeable.java | 27 + src/com/wentch/redkale/convert/Factory.java | 342 +++ src/com/wentch/redkale/convert/HashedMap.java | 68 + .../wentch/redkale/convert/MapDecoder.java | 78 + .../wentch/redkale/convert/MapEncoder.java | 62 + .../wentch/redkale/convert/ObjectDecoder.java | 144 ++ .../wentch/redkale/convert/ObjectEncoder.java | 213 ++ src/com/wentch/redkale/convert/Reader.java | 112 + .../wentch/redkale/convert/SimpledCoder.java | 42 + src/com/wentch/redkale/convert/Writer.java | 113 + .../redkale/convert/bson/BsonConvert.java | 61 + .../redkale/convert/bson/BsonFactory.java | 54 + .../redkale/convert/bson/BsonReader.java | 245 ++ .../convert/bson/BsonSimpledCoder.java | 17 + .../redkale/convert/bson/BsonWriter.java | 236 ++ .../convert/ext/BigIntegerSimpledCoder.java | 38 + .../convert/ext/BoolArraySimpledCoder.java | 68 + .../redkale/convert/ext/BoolSimpledCoder.java | 32 + .../convert/ext/ByteArraySimpledCoder.java | 68 + .../redkale/convert/ext/ByteSimpledCoder.java | 32 + .../convert/ext/CharArraySimpledCoder.java | 68 + .../redkale/convert/ext/CharSimpledCoder.java | 33 + .../redkale/convert/ext/DateSimpledCoder.java | 33 + .../convert/ext/DoubleArraySimpledCoder.java | 68 + .../convert/ext/DoubleSimpledCoder.java | 32 + .../redkale/convert/ext/EnumSimpledCoder.java | 44 + .../convert/ext/FloatArraySimpledCoder.java | 68 + .../convert/ext/FloatSimpledCoder.java | 32 + .../convert/ext/InetAddressSimpledCoder.java | 70 + .../convert/ext/IntArraySimpledCoder.java | 68 + .../redkale/convert/ext/IntSimpledCoder.java | 32 + .../convert/ext/LongArraySimpledCoder.java | 68 + .../redkale/convert/ext/LongSimpledCoder.java | 33 + .../convert/ext/NumberSimpledCoder.java | 32 + .../convert/ext/ShortArraySimpledCoder.java | 68 + .../convert/ext/ShortSimpledCoder.java | 32 + .../convert/ext/StringArraySimpledCoder.java | 68 + .../convert/ext/StringSimpledCoder.java | 32 + .../convert/ext/TwoLongSimpledCoder.java | 40 + .../redkale/convert/ext/TypeSimpledCoder.java | 42 + .../redkale/convert/json/JsonConvert.java | 81 + .../redkale/convert/json/JsonFactory.java | 52 + .../redkale/convert/json/JsonReader.java | 556 +++++ .../convert/json/JsonSimpledCoder.java | 17 + .../redkale/convert/json/JsonWriter.java | 278 +++ src/com/wentch/redkale/net/Async.java | 23 + .../wentch/redkale/net/AsyncConnection.java | 272 +++ .../redkale/net/AsyncDatagramChannel.java | 1298 +++++++++++ .../redkale/net/AsyncPooledConnection.java | 50 + .../wentch/redkale/net/AsyncWriteHandler.java | 74 + src/com/wentch/redkale/net/BufferPool.java | 61 + src/com/wentch/redkale/net/ChunkBuffer.java | 228 ++ src/com/wentch/redkale/net/Context.java | 104 + src/com/wentch/redkale/net/PrepareRunner.java | 99 + .../wentch/redkale/net/PrepareServlet.java | 72 + .../wentch/redkale/net/ProtocolServer.java | 159 ++ src/com/wentch/redkale/net/Request.java | 68 + src/com/wentch/redkale/net/Response.java | 108 + src/com/wentch/redkale/net/ResponsePool.java | 69 + src/com/wentch/redkale/net/SSLBuilder.java | 38 + src/com/wentch/redkale/net/Server.java | 178 ++ src/com/wentch/redkale/net/Servlet.java | 27 + src/com/wentch/redkale/net/Transport.java | 138 ++ src/com/wentch/redkale/net/WorkThread.java | 27 + .../wentch/redkale/net/http/AuthIgnore.java | 22 + .../redkale/net/http/BasedHttpServlet.java | 196 ++ .../wentch/redkale/net/http/ByteArray.java | 136 ++ .../wentch/redkale/net/http/HttpContext.java | 69 + .../redkale/net/http/HttpPrepareServlet.java | 163 ++ .../redkale/net/http/HttpProxyServlet.java | 197 ++ .../wentch/redkale/net/http/HttpRequest.java | 437 ++++ .../redkale/net/http/HttpResourceServlet.java | 328 +++ .../wentch/redkale/net/http/HttpResponse.java | 437 ++++ .../wentch/redkale/net/http/HttpServer.java | 68 + .../wentch/redkale/net/http/HttpServlet.java | 26 + src/com/wentch/redkale/net/http/MimeType.java | 255 ++ .../wentch/redkale/net/http/MultiContext.java | 264 +++ .../wentch/redkale/net/http/MultiPart.java | 115 + .../wentch/redkale/net/http/WebAction.java | 22 + .../wentch/redkale/net/http/WebServlet.java | 26 + .../wentch/redkale/net/http/WebSocket.java | 120 + .../redkale/net/http/WebSocketEngine.java | 44 + .../redkale/net/http/WebSocketGroup.java | 61 + .../redkale/net/http/WebSocketPacket.java | 102 + .../redkale/net/http/WebSocketRunner.java | 405 ++++ .../redkale/net/http/WebSocketServlet.java | 103 + .../wentch/redkale/net/sncp/ServiceEntry.java | 83 + src/com/wentch/redkale/net/sncp/Sncp.java | 313 +++ .../wentch/redkale/net/sncp/SncpClient.java | 178 ++ .../wentch/redkale/net/sncp/SncpContext.java | 51 + .../redkale/net/sncp/SncpDynServlet.java | 318 +++ .../redkale/net/sncp/SncpPrepareServlet.java | 80 + .../wentch/redkale/net/sncp/SncpRequest.java | 134 ++ .../wentch/redkale/net/sncp/SncpResponse.java | 49 + .../wentch/redkale/net/sncp/SncpServer.java | 59 + .../wentch/redkale/net/sncp/SncpServlet.java | 32 + .../service/DataCacheListenerService.java | 217 ++ .../service/DataSQLListenerService.java | 178 ++ .../wentch/redkale/service/MultiService.java | 14 + src/com/wentch/redkale/service/RemoteOn.java | 42 + src/com/wentch/redkale/service/Service.java | 29 + .../redkale/source/DataCacheListener.java | 21 + .../wentch/redkale/source/DataConnection.java | 29 + .../wentch/redkale/source/DataJDBCSource.java | 2075 +++++++++++++++++ .../wentch/redkale/source/DataJPASource.java | 1092 +++++++++ .../redkale/source/DataSQLListener.java | 20 + src/com/wentch/redkale/source/DataSource.java | 427 ++++ .../redkale/source/DataSourceFactory.java | 40 + .../redkale/source/DistributeGenerator.java | 39 + .../wentch/redkale/source/EntityCache.java | 208 ++ src/com/wentch/redkale/source/EntityInfo.java | 155 ++ src/com/wentch/redkale/source/FilterBean.java | 15 + .../wentch/redkale/source/FilterColumn.java | 60 + .../wentch/redkale/source/FilterExpress.java | 44 + .../wentch/redkale/source/FilterGroup.java | 62 + .../wentch/redkale/source/FilterGroups.java | 23 + src/com/wentch/redkale/source/FilterInfo.java | 512 ++++ .../redkale/source/FilterJoinColumn.java | 42 + src/com/wentch/redkale/source/Flipper.java | 106 + src/com/wentch/redkale/source/Range.java | 284 +++ .../wentch/redkale/source/SelectColumn.java | 80 + src/com/wentch/redkale/util/AnyValue.java | 347 +++ src/com/wentch/redkale/util/Attribute.java | 259 ++ src/com/wentch/redkale/util/AutoLoad.java | 24 + src/com/wentch/redkale/util/Creator.java | 207 ++ .../redkale/util/DebugMethodVisitor.java | 98 + src/com/wentch/redkale/util/Ignore.java | 22 + src/com/wentch/redkale/util/ObjectPool.java | 61 + src/com/wentch/redkale/util/Reproduce.java | 105 + .../wentch/redkale/util/ResourceFactory.java | 204 ++ src/com/wentch/redkale/util/Sheet.java | 89 + src/com/wentch/redkale/util/TwoLong.java | 78 + src/com/wentch/redkale/util/TypeToken.java | 26 + src/com/wentch/redkale/util/Utility.java | 303 +++ src/com/wentch/redkale/watch/WatchBean.java | 21 + .../wentch/redkale/watch/WatchFactory.java | 98 + src/com/wentch/redkale/watch/WatchNumber.java | 49 + src/com/wentch/redkale/watch/WatchOn.java | 34 + .../wentch/redkale/watch/WatchSupplier.java | 47 + 169 files changed, 22269 insertions(+) create mode 100644 bin/shutdown.bat create mode 100644 bin/shutdown.sh create mode 100644 bin/start.bat create mode 100644 bin/start.sh create mode 100644 conf/application.xml create mode 100644 conf/logging.properties create mode 100644 conf/persistence.xml create mode 100644 lib/javax.persistence_2.1.0.jar create mode 100644 src/META-INF/application-template.xml create mode 100644 src/META-INF/logging-template.properties create mode 100644 src/com/wentch/redkale/boot/Application.java create mode 100644 src/com/wentch/redkale/boot/ClassFilter.java create mode 100644 src/com/wentch/redkale/boot/LogFileHandler.java create mode 100644 src/com/wentch/redkale/boot/NodeHttpServer.java create mode 100644 src/com/wentch/redkale/boot/NodeServer.java create mode 100644 src/com/wentch/redkale/boot/NodeSncpServer.java create mode 100644 src/com/wentch/redkale/convert/AnyEncoder.java create mode 100644 src/com/wentch/redkale/convert/ArrayDecoder.java create mode 100644 src/com/wentch/redkale/convert/ArrayEncoder.java create mode 100644 src/com/wentch/redkale/convert/CollectionDecoder.java create mode 100644 src/com/wentch/redkale/convert/CollectionEncoder.java create mode 100644 src/com/wentch/redkale/convert/Convert.java create mode 100644 src/com/wentch/redkale/convert/ConvertColumn.java create mode 100644 src/com/wentch/redkale/convert/ConvertColumnEntry.java create mode 100644 src/com/wentch/redkale/convert/ConvertColumns.java create mode 100644 src/com/wentch/redkale/convert/ConvertException.java create mode 100644 src/com/wentch/redkale/convert/ConvertType.java create mode 100644 src/com/wentch/redkale/convert/DeMember.java create mode 100644 src/com/wentch/redkale/convert/Decodeable.java create mode 100644 src/com/wentch/redkale/convert/EnMember.java create mode 100644 src/com/wentch/redkale/convert/Encodeable.java create mode 100644 src/com/wentch/redkale/convert/Factory.java create mode 100644 src/com/wentch/redkale/convert/HashedMap.java create mode 100644 src/com/wentch/redkale/convert/MapDecoder.java create mode 100644 src/com/wentch/redkale/convert/MapEncoder.java create mode 100644 src/com/wentch/redkale/convert/ObjectDecoder.java create mode 100644 src/com/wentch/redkale/convert/ObjectEncoder.java create mode 100644 src/com/wentch/redkale/convert/Reader.java create mode 100644 src/com/wentch/redkale/convert/SimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/Writer.java create mode 100644 src/com/wentch/redkale/convert/bson/BsonConvert.java create mode 100644 src/com/wentch/redkale/convert/bson/BsonFactory.java create mode 100644 src/com/wentch/redkale/convert/bson/BsonReader.java create mode 100644 src/com/wentch/redkale/convert/bson/BsonSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/bson/BsonWriter.java create mode 100644 src/com/wentch/redkale/convert/ext/BigIntegerSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/BoolArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/BoolSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/ByteArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/ByteSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/CharArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/CharSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/DateSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/DoubleArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/DoubleSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/EnumSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/FloatArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/FloatSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/InetAddressSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/IntArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/IntSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/LongArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/LongSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/NumberSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/ShortArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/ShortSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/StringArraySimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/StringSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/TwoLongSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/ext/TypeSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/json/JsonConvert.java create mode 100644 src/com/wentch/redkale/convert/json/JsonFactory.java create mode 100644 src/com/wentch/redkale/convert/json/JsonReader.java create mode 100644 src/com/wentch/redkale/convert/json/JsonSimpledCoder.java create mode 100644 src/com/wentch/redkale/convert/json/JsonWriter.java create mode 100644 src/com/wentch/redkale/net/Async.java create mode 100644 src/com/wentch/redkale/net/AsyncConnection.java create mode 100644 src/com/wentch/redkale/net/AsyncDatagramChannel.java create mode 100644 src/com/wentch/redkale/net/AsyncPooledConnection.java create mode 100644 src/com/wentch/redkale/net/AsyncWriteHandler.java create mode 100644 src/com/wentch/redkale/net/BufferPool.java create mode 100644 src/com/wentch/redkale/net/ChunkBuffer.java create mode 100644 src/com/wentch/redkale/net/Context.java create mode 100644 src/com/wentch/redkale/net/PrepareRunner.java create mode 100644 src/com/wentch/redkale/net/PrepareServlet.java create mode 100644 src/com/wentch/redkale/net/ProtocolServer.java create mode 100644 src/com/wentch/redkale/net/Request.java create mode 100644 src/com/wentch/redkale/net/Response.java create mode 100644 src/com/wentch/redkale/net/ResponsePool.java create mode 100644 src/com/wentch/redkale/net/SSLBuilder.java create mode 100644 src/com/wentch/redkale/net/Server.java create mode 100644 src/com/wentch/redkale/net/Servlet.java create mode 100644 src/com/wentch/redkale/net/Transport.java create mode 100644 src/com/wentch/redkale/net/WorkThread.java create mode 100644 src/com/wentch/redkale/net/http/AuthIgnore.java create mode 100644 src/com/wentch/redkale/net/http/BasedHttpServlet.java create mode 100644 src/com/wentch/redkale/net/http/ByteArray.java create mode 100644 src/com/wentch/redkale/net/http/HttpContext.java create mode 100644 src/com/wentch/redkale/net/http/HttpPrepareServlet.java create mode 100644 src/com/wentch/redkale/net/http/HttpProxyServlet.java create mode 100644 src/com/wentch/redkale/net/http/HttpRequest.java create mode 100644 src/com/wentch/redkale/net/http/HttpResourceServlet.java create mode 100644 src/com/wentch/redkale/net/http/HttpResponse.java create mode 100644 src/com/wentch/redkale/net/http/HttpServer.java create mode 100644 src/com/wentch/redkale/net/http/HttpServlet.java create mode 100644 src/com/wentch/redkale/net/http/MimeType.java create mode 100644 src/com/wentch/redkale/net/http/MultiContext.java create mode 100644 src/com/wentch/redkale/net/http/MultiPart.java create mode 100644 src/com/wentch/redkale/net/http/WebAction.java create mode 100644 src/com/wentch/redkale/net/http/WebServlet.java create mode 100644 src/com/wentch/redkale/net/http/WebSocket.java create mode 100644 src/com/wentch/redkale/net/http/WebSocketEngine.java create mode 100644 src/com/wentch/redkale/net/http/WebSocketGroup.java create mode 100644 src/com/wentch/redkale/net/http/WebSocketPacket.java create mode 100644 src/com/wentch/redkale/net/http/WebSocketRunner.java create mode 100644 src/com/wentch/redkale/net/http/WebSocketServlet.java create mode 100644 src/com/wentch/redkale/net/sncp/ServiceEntry.java create mode 100644 src/com/wentch/redkale/net/sncp/Sncp.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpClient.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpContext.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpDynServlet.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpPrepareServlet.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpRequest.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpResponse.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpServer.java create mode 100644 src/com/wentch/redkale/net/sncp/SncpServlet.java create mode 100644 src/com/wentch/redkale/service/DataCacheListenerService.java create mode 100644 src/com/wentch/redkale/service/DataSQLListenerService.java create mode 100644 src/com/wentch/redkale/service/MultiService.java create mode 100644 src/com/wentch/redkale/service/RemoteOn.java create mode 100644 src/com/wentch/redkale/service/Service.java create mode 100644 src/com/wentch/redkale/source/DataCacheListener.java create mode 100644 src/com/wentch/redkale/source/DataConnection.java create mode 100644 src/com/wentch/redkale/source/DataJDBCSource.java create mode 100644 src/com/wentch/redkale/source/DataJPASource.java create mode 100644 src/com/wentch/redkale/source/DataSQLListener.java create mode 100644 src/com/wentch/redkale/source/DataSource.java create mode 100644 src/com/wentch/redkale/source/DataSourceFactory.java create mode 100644 src/com/wentch/redkale/source/DistributeGenerator.java create mode 100644 src/com/wentch/redkale/source/EntityCache.java create mode 100644 src/com/wentch/redkale/source/EntityInfo.java create mode 100644 src/com/wentch/redkale/source/FilterBean.java create mode 100644 src/com/wentch/redkale/source/FilterColumn.java create mode 100644 src/com/wentch/redkale/source/FilterExpress.java create mode 100644 src/com/wentch/redkale/source/FilterGroup.java create mode 100644 src/com/wentch/redkale/source/FilterGroups.java create mode 100644 src/com/wentch/redkale/source/FilterInfo.java create mode 100644 src/com/wentch/redkale/source/FilterJoinColumn.java create mode 100644 src/com/wentch/redkale/source/Flipper.java create mode 100644 src/com/wentch/redkale/source/Range.java create mode 100644 src/com/wentch/redkale/source/SelectColumn.java create mode 100644 src/com/wentch/redkale/util/AnyValue.java create mode 100644 src/com/wentch/redkale/util/Attribute.java create mode 100644 src/com/wentch/redkale/util/AutoLoad.java create mode 100644 src/com/wentch/redkale/util/Creator.java create mode 100644 src/com/wentch/redkale/util/DebugMethodVisitor.java create mode 100644 src/com/wentch/redkale/util/Ignore.java create mode 100644 src/com/wentch/redkale/util/ObjectPool.java create mode 100644 src/com/wentch/redkale/util/Reproduce.java create mode 100644 src/com/wentch/redkale/util/ResourceFactory.java create mode 100644 src/com/wentch/redkale/util/Sheet.java create mode 100644 src/com/wentch/redkale/util/TwoLong.java create mode 100644 src/com/wentch/redkale/util/TypeToken.java create mode 100644 src/com/wentch/redkale/util/Utility.java create mode 100644 src/com/wentch/redkale/watch/WatchBean.java create mode 100644 src/com/wentch/redkale/watch/WatchFactory.java create mode 100644 src/com/wentch/redkale/watch/WatchNumber.java create mode 100644 src/com/wentch/redkale/watch/WatchOn.java create mode 100644 src/com/wentch/redkale/watch/WatchSupplier.java diff --git a/bin/shutdown.bat b/bin/shutdown.bat new file mode 100644 index 000000000..c3e56f128 --- /dev/null +++ b/bin/shutdown.bat @@ -0,0 +1,7 @@ +@ECHO OFF + +SET APP_HOME=%~dp0 + +IF NOT EXIST "%APP_HOME%\conf\application.xml" SET APP_HOME=%~dp0.. + +java -DSHUTDOWN=true -DAPP_HOME=%APP_HOME% -classpath %APP_HOME%\lib\* com.wentch.redkale.boot.Application diff --git a/bin/shutdown.sh b/bin/shutdown.sh new file mode 100644 index 000000000..17d956d8e --- /dev/null +++ b/bin/shutdown.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +APP_HOME=`dirname "$0"` + +if [ ! -a "$APP_HOME"/conf/application.xml ]; then + APP_HOME="$APP_HOME"/.. +fi + +lib='.' +for jar in `ls $APP_HOME/lib/*.jar` +do + lib=$lib:$jar +done +export CLASSPATH=$CLASSPATH:$lib +echo "$APP_HOME" +java -DSHUTDOWN=true -DAPP_HOME="$APP_HOME" com.wentch.redkale.boot.Application diff --git a/bin/start.bat b/bin/start.bat new file mode 100644 index 000000000..8a3b84059 --- /dev/null +++ b/bin/start.bat @@ -0,0 +1,7 @@ +@ECHO OFF + +SET APP_HOME=%~dp0 + +IF NOT EXIST "%APP_HOME%\conf\application.xml" SET APP_HOME=%~dp0.. + +java -DAPP_HOME=%APP_HOME% -classpath %APP_HOME%\lib\* com.wentch.redkale.boot.Application diff --git a/bin/start.sh b/bin/start.sh new file mode 100644 index 000000000..f60e587e2 --- /dev/null +++ b/bin/start.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +ulimit -c unlimited + +ulimit -n 1024000 + +APP_HOME=`dirname "$0"` + +if [ ! -a "$APP_HOME"/conf/application.xml ]; then + APP_HOME="$APP_HOME"/.. +fi + +lib="$APP_HOME"/lib +for jar in `ls $APP_HOME/lib/*.jar` +do + lib=$lib:$jar +done +export CLASSPATH=$CLASSPATH:$lib + +echo "$APP_HOME" +nohup java -DAPP_HOME="$APP_HOME" com.wentch.redkale.boot.Application > "$APP_HOME"/log.out & + diff --git a/conf/application.xml b/conf/application.xml new file mode 100644 index 000000000..3394a7489 --- /dev/null +++ b/conf/application.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/conf/logging.properties b/conf/logging.properties new file mode 100644 index 000000000..f765eed5b --- /dev/null +++ b/conf/logging.properties @@ -0,0 +1,21 @@ + + +handlers = java.util.logging.ConsoleHandler + +############################################################ +.level = FINE + +org.level = INFO +javax.level = INFO +com.sun.level = INFO + + +java.util.logging.FileHandler.level = FINE +#100M +java.util.logging.FileHandler.limit = 52428800 +java.util.logging.FileHandler.count = 10000 +java.util.logging.FileHandler.encoding = UTF-8 +java.util.logging.FileHandler.pattern = log.log +java.util.logging.FileHandler.append = true + +java.util.logging.ConsoleHandler.level = FINE diff --git a/conf/persistence.xml b/conf/persistence.xml new file mode 100644 index 000000000..c2e46bb09 --- /dev/null +++ b/conf/persistence.xml @@ -0,0 +1,15 @@ + + + + org.eclipse.persistence.jpa.PersistenceProvider + false + ALL + NONE + + + + + + + + diff --git a/lib/javax.persistence_2.1.0.jar b/lib/javax.persistence_2.1.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..841d2e19b03423ad822c0a19b8838338559105bf GIT binary patch literal 164637 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 + + + + + + + + +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/META-INF/logging-template.properties b/src/META-INF/logging-template.properties new file mode 100644 index 000000000..355712519 --- /dev/null +++ b/src/META-INF/logging-template.properties @@ -0,0 +1,16 @@ + +handlers = java.util.logging.ConsoleHandler,java.util.logging.FileHandler +.handlers = java.util.logging.ConsoleHandler,java.util.logging.FileHandler + +############################################################ +.level = INFO + +#java.util.logging.FileHandler.level = INFO +#100M +java.util.logging.FileHandler.limit = 104857600 +java.util.logging.FileHandler.count = 10 +java.util.logging.FileHandler.encoding = UTF-8 +java.util.logging.FileHandler.pattern = ./logs/log-%u.log +java.util.logging.FileHandler.append = true + +#java.util.logging.ConsoleHandler.level = INFO diff --git a/src/com/wentch/redkale/boot/Application.java b/src/com/wentch/redkale/boot/Application.java new file mode 100644 index 000000000..bdd2fa872 --- /dev/null +++ b/src/com/wentch/redkale/boot/Application.java @@ -0,0 +1,512 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.boot; + +import static com.wentch.redkale.boot.NodeServer.LINE_SEPARATOR; +import com.wentch.redkale.convert.bson.*; +import com.wentch.redkale.convert.json.*; +import com.wentch.redkale.net.*; +import com.wentch.redkale.net.http.*; +import com.wentch.redkale.net.sncp.*; +import com.wentch.redkale.service.*; +import com.wentch.redkale.source.*; +import com.wentch.redkale.util.*; +import com.wentch.redkale.util.AnyValue.DefaultAnyValue; +import com.wentch.redkale.watch.*; +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.nio.*; +import java.nio.channels.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.*; +import javax.annotation.*; +import javax.xml.parsers.*; +import org.w3c.dom.*; + +/** + * 编译时需要加入: -XDignore.symbol.file=true + * + * 进程启动类,程序启动后读取application.xml,进行classpath扫描动态加载Service与Servlet, + * 再进行Service、Servlet与其他资源之间的依赖注入。 + * + * @author zhangjx + */ +public final class Application { + + public static final String RESNAME_TIME = "APP_TIME"; + + public static final String RESNAME_HOME = "APP_HOME"; + + public static final String RESNAME_NODE = "APP_NODE"; + + public static final String RESNAME_ADDR = "APP_ADDR"; + + public static final String RESNAME_GRES = "APP_GRES"; + + protected final ResourceFactory factory = ResourceFactory.root(); + + protected final WatchFactory watch = WatchFactory.root(); + + protected final HashMap localServices = new HashMap<>(); + + protected final ArrayList remoteServices = new ArrayList<>(); + + protected boolean serviceInited = false; + + protected final InetAddress localAddress = Utility.localInetAddress(); + + protected String nodeName = ""; + + //-------------------------------------------------------------------------------------------- + private File home; + + private final Logger logger; + + private final AnyValue config; + + private final List servers = new CopyOnWriteArrayList<>(); + + private final List sources = new CopyOnWriteArrayList<>(); + + private final long startTime = System.currentTimeMillis(); + + private CountDownLatch cdl; + + private Application(final AnyValue config) { + this.config = config; + final File root = new File(System.getProperty(RESNAME_HOME)); + this.factory.register(RESNAME_TIME, long.class, this.startTime); + this.factory.register(RESNAME_HOME, Path.class, root.toPath()); + this.factory.register(RESNAME_HOME, File.class, root); + try { + this.factory.register(RESNAME_HOME, root.getCanonicalPath()); + this.home = root.getCanonicalFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + final File logconf = new File(root, "conf/logging.properties"); + this.nodeName = config.getValue("node", ""); + this.factory.register(RESNAME_NODE, this.nodeName); + this.factory.register(RESNAME_ADDR, this.localAddress.getHostAddress()); + this.factory.register(RESNAME_ADDR, InetAddress.class, this.localAddress); + if (logconf.isFile() && logconf.canRead()) { + try { + final String rootpath = root.getCanonicalPath().replace('\\', '/'); + FileInputStream fin = new FileInputStream(logconf); + Properties properties = new Properties(); + properties.load(fin); + fin.close(); + properties.entrySet().stream().forEach(x -> { + x.setValue(x.getValue().toString().replace("${APP_HOME}", rootpath)); + }); + + if (properties.getProperty("java.util.logging.FileHandler.formatter") == null) { + properties.setProperty("java.util.logging.FileHandler.formatter", LogFileHandler.LoggingFormater.class.getName()); + } + if (properties.getProperty("java.util.logging.ConsoleHandler.formatter") == null) { + properties.setProperty("java.util.logging.ConsoleHandler.formatter", LogFileHandler.LoggingFormater.class.getName()); + } + String fileHandlerPattern = properties.getProperty("java.util.logging.FileHandler.pattern"); + if (fileHandlerPattern != null && fileHandlerPattern.contains("%d")) { + final String fileHandlerClass = LogFileHandler.class.getName(); + Properties prop = new Properties(); + String handlers = properties.getProperty("handlers"); + if (handlers != null && handlers.contains("java.util.logging.FileHandler")) { + prop.setProperty("handlers", handlers.replace("java.util.logging.FileHandler", fileHandlerClass)); + } + if (!prop.isEmpty()) { + String prefix = fileHandlerClass + "."; + properties.entrySet().stream().forEach(x -> { + if (x.getKey().toString().startsWith("java.util.logging.FileHandler.")) { + prop.put(x.getKey().toString().replace("java.util.logging.FileHandler.", prefix), x.getValue()); + } + }); + prop.entrySet().stream().forEach(x -> { + properties.put(x.getKey(), x.getValue()); + }); + } + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + final PrintStream ps = new PrintStream(out); + properties.forEach((x, y) -> ps.println(x + "=" + y)); + LogManager.getLogManager().readConfiguration(new ByteArrayInputStream(out.toByteArray())); + } catch (Exception e) { + Logger.getLogger(this.getClass().getSimpleName()).log(Level.WARNING, "init logger configuration error", e); + } + } + logger = Logger.getLogger(this.getClass().getSimpleName()); + } + + public File getHome() { + return home; + } + + private void initLogging() { + + } + + public void init() throws Exception { + System.setProperty("convert.bson.pool.size", "128"); + System.setProperty("convert.json.pool.size", "128"); + System.setProperty("convert.bson.writer.buffer.defsize", "4096"); + System.setProperty("convert.json.writer.buffer.defsize", "4096"); + final File root = new File(System.getProperty(RESNAME_HOME)); + this.factory.register(BsonFactory.root()); + this.factory.register(JsonFactory.root()); + this.factory.register(BsonFactory.root().getConvert()); + this.factory.register(JsonFactory.root().getConvert()); + File persist = new File(root, "conf/persistence.xml"); + if (persist.isFile()) System.setProperty(DataJDBCSource.DATASOURCE_CONFPATH, persist.getCanonicalPath()); + logger.log(Level.INFO, RESNAME_HOME + "=" + root.getCanonicalPath() + "\r\n" + RESNAME_ADDR + "=" + this.localAddress.getHostAddress()); + String lib = config.getValue("lib", "").trim().replace("${APP_HOME}", root.getCanonicalPath()); + lib = lib.isEmpty() ? (root.getCanonicalPath() + "/conf") : (lib + ";" + root.getCanonicalPath() + "/conf"); + Server.loadLib(logger, lib); + initLogging(); + InetAddress addr = Utility.localInetAddress(); + if (addr != null) { + byte[] bs = addr.getAddress(); + int v = (0xff & bs[bs.length - 2]) % 10 * 100 + (0xff & bs[bs.length - 1]); + this.factory.register("property.datasource.nodeid", "" + v); + } + initResources(); + } + + public static void singleton(Service service) throws Exception { + final Application application = Application.create(); + application.init(); + application.factory.register(service); + new NodeHttpServer(application, new CountDownLatch(1), null).load(application.config); + application.factory.inject(service); + } + + private static Application create() throws IOException { + final String home = new File(System.getProperty(RESNAME_HOME, "")).getCanonicalPath(); + System.setProperty(RESNAME_HOME, home); + File appfile = new File(home, "conf/application.xml"); + //System.setProperty(DataConnection.PERSIST_FILEPATH, appfile.getCanonicalPath()); + return new Application(load(new FileInputStream(appfile))); + } + + public static void main(String[] args) throws Exception { + //运行主程序 + final Application application = Application.create(); + if (System.getProperty("SHUTDOWN") != null) { + application.sendShutDown(); + return; + } + application.init(); + application.startSelfServer(); + application.start(); + System.exit(0); + } + + private void startSelfServer() throws Exception { + final Application application = this; + new Thread() { + { + setName("Application-Control-Thread"); + } + + @Override + public void run() { + try { + final DatagramChannel channel = DatagramChannel.open(); + channel.configureBlocking(true); + channel.bind(new InetSocketAddress(config.getValue("host", "127.0.0.1"), config.getIntValue("port"))); + boolean loop = true; + ByteBuffer buffer = ByteBuffer.allocateDirect(1024); + while (loop) { + buffer.clear(); + SocketAddress address = channel.receive(buffer); + buffer.flip(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + if ("SHUTDOWN".equalsIgnoreCase(new String(bytes))) { + try { + long s = System.currentTimeMillis(); + logger.info(application.getClass().getSimpleName() + " shutdowning"); + application.shutdown(); + application.cdl.countDown(); + buffer.clear(); + buffer.put("SHUTDOWN OK".getBytes()); + buffer.flip(); + channel.send(buffer, address); + long e = System.currentTimeMillis() - s; + logger.info(application.getClass().getSimpleName() + " shutdown in " + e + " ms"); + Thread.sleep(100L); + System.exit(0); + } catch (Exception ex) { + logger.log(Level.INFO, "SHUTDOWN FAIL", ex); + buffer.clear(); + buffer.put("SHUTDOWN FAIL".getBytes()); + buffer.flip(); + channel.send(buffer, address); + } + } + } + } catch (Exception e) { + logger.log(Level.INFO, "Control fail", e); + System.exit(1); + } + } + }.start(); + } + + private void sendShutDown() throws Exception { + final DatagramChannel channel = DatagramChannel.open(); + channel.configureBlocking(true); + channel.connect(new InetSocketAddress(config.getValue("host", "127.0.0.1"), config.getIntValue("port"))); + ByteBuffer buffer = ByteBuffer.allocate(128); + buffer.put("SHUTDOWN".getBytes()); + buffer.flip(); + channel.write(buffer); + buffer.clear(); + channel.configureBlocking(false); + channel.read(buffer); + buffer.flip(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + channel.close(); + logger.info(new String(bytes)); + Thread.sleep(500); + } + + public void start() throws Exception { + final AnyValue[] entrys = config.getAnyValues("server"); + cdl = new CountDownLatch(entrys.length + 1); + CountDownLatch timecd = new CountDownLatch(entrys.length); + runServers(timecd, entrys); + timecd.await(); + logger.info(this.getClass().getSimpleName() + " started in " + (System.currentTimeMillis() - startTime) + " ms"); + cdl.await(); + } + + @SuppressWarnings("unchecked") + private void runServers(CountDownLatch timecd, final AnyValue[] entrys) throws Exception { + CountDownLatch servicecdl = new CountDownLatch(entrys.length); + for (final AnyValue entry : entrys) { + new Thread() { + { + String host = entry.getValue("host", "").replace("0.0.0.0", ""); + setName(entry.getValue("protocol", "Server").toUpperCase() + "-" + host + ":" + entry.getIntValue("port", 80) + "-Thread"); + this.setDaemon(true); + } + + @Override + public void run() { + try { + //Thread ctd = Thread.currentThread(); + //ctd.setContextClassLoader(new URLClassLoader(new URL[0], ctd.getContextClassLoader())); + NodeServer server = null; + if ("HTTP".equalsIgnoreCase(entry.getValue("protocol", ""))) { + server = new NodeHttpServer(Application.this, servicecdl, new HttpServer(startTime, watch)); + } else if ("SNCP".equalsIgnoreCase(entry.getValue("protocol", ""))) { + server = new NodeSncpServer(Application.this, servicecdl, new SncpServer(startTime, watch)); + } + if (server == null) { + logger.log(Level.SEVERE, "Not found Server Class for protocol({0})", entry.getValue("protocol")); + System.exit(0); + } + servers.add(server); + + server.load(entry); //必须在init之前 + server.init(entry); + server.start(); + timecd.countDown(); + } catch (Exception ex) { + logger.log(Level.WARNING, entry + " runServers error", ex); + } finally { + cdl.countDown(); + } + } + }.start(); + } + } + + private void initResources() throws Exception { + + this.factory.add(DataSource.class, (ResourceFactory rf, final Object src, Field field) -> { + try { + Resource rs = field.getAnnotation(Resource.class); + if (rs == null) return; + if (src.getClass().getAnnotation(RemoteOn.class) != null) return; + DataSource source = DataSourceFactory.create(rs.name()); + sources.add(source); + rf.register(rs.name(), DataSource.class, source); + field.set(src, source); + rf.inject(source); // 给 "datasource.nodeid" 赋值 + } catch (Exception e) { + logger.log(Level.SEVERE, "DataSource inject error", e); + } + }); + + this.factory.add(DataSQLListener.class, (ResourceFactory rf, Object src, Field field) -> { + + try { + Resource rs = field.getAnnotation(Resource.class); + if (rs == null) return; + if (src.getClass().getAnnotation(RemoteOn.class) != null) return; + DataSQLListener service = rf.findChild("", DataSQLListener.class); + if (service != null) { + field.set(src, service); + rf.inject(service); + } + } catch (Exception e) { + logger.log(Level.SEVERE, DataSQLListener.class.getSimpleName() + " inject error", e); + } + } + ); + + this.factory.add(DataCacheListener.class, (ResourceFactory rf, Object src, Field field) -> { + + try { + Resource rs = field.getAnnotation(Resource.class); + if (rs == null) return; + if (src.getClass().getAnnotation(RemoteOn.class) != null) return; + DataCacheListener service = rf.findChild("", DataCacheListener.class); + if (service != null) { + field.set(src, service); + rf.inject(service); + } + } catch (Exception e) { + logger.log(Level.SEVERE, DataCacheListener.class.getSimpleName() + " inject error", e); + } + } + ); + //------------------------------------------------------------------------- + final AnyValue resources = config.getAnyValue("resources"); + if (resources != null) { + factory.register(RESNAME_GRES, AnyValue.class, resources); + //------------------------------------------------------------------------ + final AnyValue properties = resources.getAnyValue("properties"); + if (properties != null) { + String dfloads = properties.getValue("load"); + if (dfloads != null) { + for (String dfload : dfloads.split(";")) { + if (dfload.trim().isEmpty()) continue; + dfload = dfload.trim().replace("${APP_HOME}", home.getCanonicalPath()).replace('\\', '/'); + final File df = (dfload.indexOf('/') < 0) ? new File(home, "conf/" + dfload) : new File(dfload); + if (df.isFile()) { + Properties ps = new Properties(); + InputStream in = new FileInputStream(df); + ps.load(in); + in.close(); + ps.forEach((x, y) -> factory.register("property." + x, y)); + } + } + } + for (AnyValue prop : properties.getAnyValues("property")) { + String name = prop.getValue("name"); + String value = prop.getValue("value"); + if (name == null || value == null) continue; + if (name.startsWith("system.property.")) { + System.setProperty(name.substring("system.property.".length()), value); + } else { + factory.register("property." + name, value); + } + } + } + + //------------------------------------------------------------------------ + final String host = this.localAddress.getHostAddress(); + + for (AnyValue conf : resources.getAnyValues("remote")) { + final String name = conf.getValue("name"); + if (name == null) throw new RuntimeException("remote name is null"); + String protocol = conf.getValue("protocol", "UDP").toUpperCase(); + if (!"TCP".equalsIgnoreCase(protocol) && !"UDP".equalsIgnoreCase(protocol)) { + throw new RuntimeException("Not supported Transport Protocol " + conf.getValue("protocol")); + } + AnyValue[] addrs = conf.getAnyValues("address"); + InetSocketAddress[] addresses = new InetSocketAddress[addrs.length]; + int i = -1; + for (AnyValue addr : addrs) { + addresses[++i] = new InetSocketAddress(addr.getValue("addr"), addr.getIntValue("port")); + } + if (addresses.length < 1) throw new RuntimeException("Transport(" + name + ") have no address "); + Transport transport = new Transport(name, protocol, watch, 100, addresses[0]); + factory.register(name, Transport.class, transport); + if (this.nodeName.isEmpty() && host.equals(addrs[0].getValue("addr"))) { + this.nodeName = name; + this.factory.register(RESNAME_NODE, this.nodeName); + } + } + } + + //------------------------------------------------------------------------ + logger.info(RESNAME_NODE + "=" + this.nodeName); + logger.info("datasource.nodeid=" + this.factory.find("property.datasource.nodeid", String.class)); + + } + + private void shutdown() throws Exception { + servers.stream().forEach((server) -> { + try { + server.shutdown(); + } catch (Exception t) { + logger.log(Level.WARNING, " shutdown server(" + server.getSocketAddress() + ") error", t); + } finally { + cdl.countDown(); + } + }); + final StringBuilder sb = logger.isLoggable(Level.INFO) ? new StringBuilder() : null; + localServices.entrySet().parallelStream().forEach(k -> { + Class x = k.getKey(); + ServiceEntry y = k.getValue(); + long s = System.currentTimeMillis(); + y.getService().destroy(y.getServiceConf()); + long e = System.currentTimeMillis() - s; + if (e > 2 && sb != null) { + sb.append("LocalService(").append(y.getNames()).append("|").append(y.getServiceClass()).append(") destroy ").append(e).append("ms").append(LINE_SEPARATOR); + } + }); + if (sb != null && sb.length() > 0) logger.log(Level.INFO, sb.toString()); + for (DataSource source : sources) { + try { + source.getClass().getMethod("close").invoke(source); + } catch (Exception e) { + logger.log(Level.FINER, "close DataSource erroneous", e); + } + } + } + + private static AnyValue load(final InputStream in0) { + final DefaultAnyValue any = new DefaultAnyValue(); + try (final InputStream in = in0) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(in); + Element root = doc.getDocumentElement(); + load(any, root); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + return any; + } + + private static void load(final DefaultAnyValue any, final Node root) { + final String home = System.getProperty(RESNAME_HOME); + NamedNodeMap nodes = root.getAttributes(); + if (nodes == null) return; + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + any.addValue(node.getNodeName(), node.getNodeValue().replace("${APP_HOME}", home)); + } + NodeList children = root.getChildNodes(); + if (children == null) return; + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + DefaultAnyValue sub = new DefaultAnyValue(); + load(sub, node); + any.addValue(node.getNodeName(), sub); + } + + } +} diff --git a/src/com/wentch/redkale/boot/ClassFilter.java b/src/com/wentch/redkale/boot/ClassFilter.java new file mode 100644 index 000000000..4693fa003 --- /dev/null +++ b/src/com/wentch/redkale/boot/ClassFilter.java @@ -0,0 +1,319 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.boot; + +import com.wentch.redkale.util.Ignore; +import com.wentch.redkale.util.AutoLoad; +import com.wentch.redkale.util.AnyValue; +import java.io.*; +import java.lang.annotation.*; +import java.lang.reflect.*; +import java.net.*; +import java.util.*; +import java.util.jar.*; +import java.util.logging.*; +import java.util.regex.*; + +/** + * class过滤器, 符合条件的class会保留下来存入FilterEntry。 + * + * @author zhangjx + * @param + */ +public final class ClassFilter { + + private final Set> entrys = new HashSet<>(); + + private boolean refused; + + private Class superClass; + + private Class annotationClass; + + private Pattern[] includePatterns; + + private Pattern[] excludePatterns; + + public ClassFilter(Class annotationClass, Class superClass) { + this.annotationClass = annotationClass; + this.superClass = superClass; + } + + /** + * 获取符合条件的class集合 + *

+ * @return + */ + public final Set> getFilterEntrys() { + return entrys; + } + + /** + * 自动扫描地过滤指定的class + *

+ * @param property + * @param clazzname + */ + @SuppressWarnings("unchecked") + public final void filter(AnyValue property, String clazzname) { + filter(property, clazzname, true); + } + + /** + * 过滤指定的class + *

+ * @param property application.xml中对应class节点下的property属性项 + * @param clazzname class名称 + * @param autoscan 为true表示自动扫描的, false表示显著调用filter, AutoLoad的注解将被忽略 + */ + public final void filter(AnyValue property, String clazzname, boolean autoscan) { + if (!accept(property, clazzname)) return; + try { + Class clazz = Class.forName(clazzname); + if (accept(property, clazz, autoscan)) { + FilterEntry en = new FilterEntry(clazz, property); + if (!entrys.contains(en)) entrys.add(en); + } + } catch (Throwable cfe) { + } + } + + private static Pattern[] toPattern(String[] regs) { + if (regs == null) return null; + int i = 0; + Pattern[] rs = new Pattern[regs.length]; + for (String reg : regs) { + if (reg == null || reg.trim().isEmpty()) continue; + rs[i++] = Pattern.compile(reg.trim()); + } + if (i == 0) return null; + if (i == rs.length) return rs; + Pattern[] ps = new Pattern[i]; + System.arraycopy(rs, 0, ps, 0, i); + return ps; + } + + /** + * 判断class是否有效 + *

+ * @param property + * @param classname + * @return + */ + public boolean accept(AnyValue property, String classname) { + if (this.refused) return false; + if (classname.startsWith("java.") || classname.startsWith("javax.")) return false; + if (excludePatterns != null) { + for (Pattern reg : excludePatterns) { + if (reg.matcher(classname).matches()) return false; + } + } + if (includePatterns != null) { + for (Pattern reg : includePatterns) { + if (reg.matcher(classname).matches()) return true; + } + } + return includePatterns == null; + } + + /** + * 判断class是否有效 + *

+ * @param property + * @param clazz + * @param autoscan + * @return + */ + @SuppressWarnings("unchecked") + public boolean accept(AnyValue property, Class clazz, boolean autoscan) { + if (this.refused || !Modifier.isPublic(clazz.getModifiers())) return false; + if (clazz.getAnnotation(Ignore.class) != null) return false; + if (autoscan) { + AutoLoad auto = (AutoLoad) clazz.getAnnotation(AutoLoad.class); + if (auto != null && !auto.value()) return false; + } + if (annotationClass != null && clazz.getAnnotation(annotationClass) == null) return false; + return superClass == null || (clazz != superClass && superClass.isAssignableFrom(clazz)); + } + + public void setSuperClass(Class superClass) { + this.superClass = superClass; + } + + public void setAnnotationClass(Class annotationClass) { + this.annotationClass = annotationClass; + } + + public Pattern[] getIncludePatterns() { + return includePatterns; + } + + public void setIncludePatterns(String[] includePatterns) { + this.includePatterns = toPattern(includePatterns); + } + + public Pattern[] getExcludePatterns() { + return excludePatterns; + } + + public void setExcludePatterns(String[] excludePatterns) { + this.excludePatterns = toPattern(excludePatterns); + } + + public Class getAnnotationClass() { + return annotationClass; + } + + public Class getSuperClass() { + return superClass; + } + + public boolean isRefused() { + return refused; + } + + public void setRefused(boolean refused) { + this.refused = refused; + } + + /** + * 存放符合条件的class与class指定的属性项 + *

+ * @param + */ + public static final class FilterEntry { + + private final String name; + + private final Class type; + + private final AnyValue property; + + protected Object attachment; + + public FilterEntry(Class type, AnyValue property) { + this.type = type; + this.property = property; + this.name = property == null ? "" : property.getValue("name", ""); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "[thread=" + Thread.currentThread().getName() + + ", type=" + this.type.getSimpleName() + ", name=" + name + + ", remote=" + (property == null ? "null" : property.getValue("remote")) + "]"; + } + + @Override + public int hashCode() { + return this.type.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + return (this.type == ((FilterEntry) obj).type && this.name.equals(((FilterEntry) obj).name)); + } + + public Class getType() { + return type; + } + + public String getName() { + return name; + } + + public AnyValue getProperty() { + return property; + } + + public Object getAttachment() { + return attachment; + } + + public void setAttachment(Object attachment) { + this.attachment = attachment; + } + + } + + /** + * class加载类 + */ + public static class Loader { + + protected static final Logger logger = Logger.getLogger(Loader.class.getName()); + + /** + * 加载当前线程的classpath扫描所有class进行过滤 + *

+ * @param exclude 不需要扫描的文件夹, 可以为null + * @param filters + * @throws IOException + */ + public static void load(final File exclude, final ClassFilter... filters) throws IOException { + URLClassLoader loader = (URLClassLoader) Thread.currentThread().getContextClassLoader(); + List urlfiles = new ArrayList<>(2); + List urljares = new ArrayList<>(2); + final URL exurl = exclude != null ? exclude.toURI().toURL() : null; + for (URL url : loader.getURLs()) { + if (exurl != null && exurl.sameFile(url)) continue; + if (url.getPath().endsWith(".jar")) { + urljares.add(url); + } else { + urlfiles.add(url); + } + } + + List files = new ArrayList<>(); + boolean debug = logger.isLoggable(Level.FINEST); + StringBuilder debugstr = new StringBuilder(); + for (URL url : urljares) { + try (JarFile jar = new JarFile(URLDecoder.decode(url.getFile(), "UTF-8"))) { + Enumeration it = jar.entries(); + while (it.hasMoreElements()) { + String entryname = it.nextElement().getName().replace('/', '.'); + if (entryname.endsWith(".class") && entryname.indexOf('$') < 0) { + String classname = entryname.substring(0, entryname.length() - 6); + if (classname.startsWith("javax.") || classname.startsWith("org.") || classname.startsWith("com.mysql.")) continue; + if (debug) debugstr.append(classname).append("\r\n"); + for (final ClassFilter filter : filters) { + if (filter != null) filter.filter(null, classname); + } + } + } + } + } + for (URL url : urlfiles) { + files.clear(); + File root = new File(url.getFile()); + String rootpath = root.getPath(); + loadClassFiles(exclude, root, files); + for (File f : files) { + String classname = f.getPath().substring(rootpath.length() + 1, f.getPath().length() - 6).replace(File.separatorChar, '.'); + if (classname.startsWith("javax.") || classname.startsWith("org.") || classname.startsWith("com.mysql.")) continue; + if (debug) debugstr.append(classname).append("\r\n"); + for (final ClassFilter filter : filters) { + if (filter != null) filter.filter(null, classname); + } + } + } + //if (debug) logger.log(Level.INFO, "scan classes: \r\n{0}", debugstr); + } + + private static void loadClassFiles(File exclude, File root, List files) { + if (root.isFile() && root.getName().endsWith(".class")) { + files.add(root); + } else if (root.isDirectory()) { + if (exclude != null && exclude.equals(root)) return; + for (File f : root.listFiles()) { + loadClassFiles(exclude, f, files); + } + } + } + } +} diff --git a/src/com/wentch/redkale/boot/LogFileHandler.java b/src/com/wentch/redkale/boot/LogFileHandler.java new file mode 100644 index 000000000..e21921c1f --- /dev/null +++ b/src/com/wentch/redkale/boot/LogFileHandler.java @@ -0,0 +1,230 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.boot; + +import java.io.*; +import java.nio.file.*; +import static java.nio.file.StandardCopyOption.*; +import java.time.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import java.util.logging.*; +import java.util.logging.Formatter; + +/** + * 自定义的日志存储类 + *

+ * @author zhangjx + */ +public class LogFileHandler extends Handler { + + public static class LoggingFormater extends Formatter { + + private static final String format = "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%tL %4$s %2$s\r\n%5$s%6$s\r\n"; + + @Override + public String format(LogRecord record) { + String source; + if (record.getSourceClassName() != null) { + source = record.getSourceClassName(); + if (record.getSourceMethodName() != null) { + source += " " + record.getSourceMethodName(); + } + } else { + source = record.getLoggerName(); + } + String message = formatMessage(record); + String throwable = ""; + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw) { + @Override + public void println() { + super.print("\r\n"); + } + }; + pw.println(); + record.getThrown().printStackTrace(pw); + pw.close(); + throwable = sw.toString(); + } + return String.format(format, + System.currentTimeMillis(), + source, + record.getLoggerName(), + record.getLevel().getName(), + message, + throwable); + } + + } + + protected final LinkedBlockingQueue records = new LinkedBlockingQueue(); + + private String pattern; + + private int limit; //文件大小限制 + + private final AtomicInteger index = new AtomicInteger(); + + private int count = 1; //文件限制 + + private long tomorrow; + + private boolean append; + + private final AtomicLong length = new AtomicLong(); + + private File logfile; + + private OutputStream stream; + + public LogFileHandler() { + updateTomorrow(); + configure(); + open(); + } + + private void updateTomorrow() { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + cal.add(Calendar.DAY_OF_YEAR, 1); + long t = cal.getTimeInMillis(); + if (this.tomorrow != t) index.set(0); + this.tomorrow = t; + } + + private void open() { + new Thread() { + { + setName("Logging-FileHandler-Thread"); + setDaemon(true); + } + + @Override + public void run() { + while (true) { + try { + LogRecord record = records.take(); + if ((limit > 0 && limit <= length.get()) || tomorrow <= record.getMillis()) { + updateTomorrow(); + if (stream != null) { + stream.close(); + for (int i = Math.min(count - 2, index.get() - 1); i > 0; i--) { + File greater = new File(logfile.getPath() + "." + i); + if (greater.exists()) Files.move(greater.toPath(), new File(logfile.getPath() + "." + (i + 1)).toPath(), REPLACE_EXISTING, ATOMIC_MOVE); + } + Files.move(logfile.toPath(), new File(logfile.getPath() + ".1").toPath(), REPLACE_EXISTING, ATOMIC_MOVE); + stream = null; + } + } + if (stream == null) { + index.incrementAndGet(); + java.time.LocalDate date = LocalDate.now(); + logfile = new File(pattern.replace("%d", String.valueOf((date.getYear() * 10000 + date.getMonthValue() * 100 + date.getDayOfMonth())))); + logfile.getParentFile().mkdirs(); + length.set(logfile.length()); + stream = new FileOutputStream(logfile, append); + } + //----------------------写日志------------------------- + String message = getFormatter().format(record); + String encoding = getEncoding(); + byte[] bytes = encoding == null ? message.getBytes() : message.getBytes(encoding); + stream.write(bytes); + length.addAndGet(bytes.length); + } catch (Exception e) { + ErrorManager err = getErrorManager(); + if (err != null) err.error(null, e, ErrorManager.WRITE_FAILURE); + } + } + + } + }.start(); + } + + private void configure() { + LogManager manager = LogManager.getLogManager(); + String cname = getClass().getName(); + pattern = manager.getProperty(cname + ".pattern"); + if (pattern == null) pattern = "logs/log-%d.log"; + String limitstr = manager.getProperty(cname + ".limit"); + try { + if (limitstr != null) limit = Math.abs(Integer.decode(limitstr)); + } catch (Exception e) { + } + String countstr = manager.getProperty(cname + ".count"); + try { + if (countstr != null) count = Math.max(1, Math.abs(Integer.decode(countstr))); + } catch (Exception e) { + } + String appendstr = manager.getProperty(cname + ".append"); + try { + if (appendstr != null) append = "true".equalsIgnoreCase(appendstr) || "1".equals(appendstr); + } catch (Exception e) { + } + String levelstr = manager.getProperty(cname + ".level"); + try { + if (levelstr != null) { + Level l = Level.parse(levelstr); + setLevel(l != null ? l : Level.ALL); + } + } catch (Exception e) { + } + String filterstr = manager.getProperty(cname + ".filter"); + try { + if (filterstr != null) { + Class clz = ClassLoader.getSystemClassLoader().loadClass(filterstr); + setFilter((Filter) clz.newInstance()); + } + } catch (Exception e) { + } + String formatterstr = manager.getProperty(cname + ".formatter"); + try { + if (formatterstr != null) { + Class clz = ClassLoader.getSystemClassLoader().loadClass(formatterstr); + setFormatter((Formatter) clz.newInstance()); + } + } catch (Exception e) { + } + if (getFormatter() == null) setFormatter(new SimpleFormatter()); + + String encodingstr = manager.getProperty(cname + ".encoding"); + try { + if (encodingstr != null) setEncoding(encodingstr); + } catch (Exception e) { + } + } + + @Override + public void publish(LogRecord record) { + records.offer(record); + } + + @Override + public void flush() { + try { + if (stream != null) stream.flush(); + } catch (Exception e) { + ErrorManager err = getErrorManager(); + if (err != null) err.error(null, e, ErrorManager.FLUSH_FAILURE); + } + } + + @Override + public void close() throws SecurityException { + try { + if (stream != null) stream.close(); + } catch (Exception e) { + ErrorManager err = getErrorManager(); + if (err != null) err.error(null, e, ErrorManager.CLOSE_FAILURE); + } + } + +} diff --git a/src/com/wentch/redkale/boot/NodeHttpServer.java b/src/com/wentch/redkale/boot/NodeHttpServer.java new file mode 100644 index 000000000..6fc78c3bc --- /dev/null +++ b/src/com/wentch/redkale/boot/NodeHttpServer.java @@ -0,0 +1,83 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.boot; + +import com.wentch.redkale.net.http.WebServlet; +import com.wentch.redkale.net.http.HttpServer; +import com.wentch.redkale.net.http.HttpServlet; +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.boot.ClassFilter.FilterEntry; +import com.wentch.redkale.service.Service; +import java.lang.reflect.Modifier; +import java.net.InetSocketAddress; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public final class NodeHttpServer extends NodeServer { + + private final HttpServer server; + + public NodeHttpServer(Application application, CountDownLatch servicecdl, HttpServer server) { + super(application, servicecdl, server); + this.server = server; + } + + @Override + public void init(AnyValue config) throws Exception { + server.init(config); + super.init(config); + } + + @Override + public InetSocketAddress getSocketAddress() { + return server == null ? null : server.getSocketAddress(); + } + + @Override + public void load(AnyValue config) throws Exception { + super.load(config); + ClassFilter httpFilter = createHttpServletClassFilter(application.nodeName, config); + ClassFilter serviceFilter = createServiceClassFilter(application.nodeName, config); + long s = System.currentTimeMillis(); + ClassFilter.Loader.load(application.getHome(), serviceFilter, httpFilter); + long e = System.currentTimeMillis() - s; + logger.info(this.getClass().getSimpleName() + " load filter class in " + e + " ms"); + loadService(config.getAnyValue("services"), serviceFilter); //必须在servlet之前 + if (server != null) initHttpServlet(config.getAnyValue("servlets"), httpFilter); + } + + protected static ClassFilter createHttpServletClassFilter(final String node, final AnyValue config) { + return createClassFilter(node, config, WebServlet.class, HttpServlet.class, null, "servlets", "servlet"); + } + + protected void initHttpServlet(final AnyValue conf, final ClassFilter filter) throws Exception { + final StringBuilder sb = logger.isLoggable(Level.FINE) ? new StringBuilder() : null; + final String prefix = conf == null ? "" : conf.getValue("prefix", ""); + final String threadName = "[" + Thread.currentThread().getName() + "] "; + for (FilterEntry en : filter.getFilterEntrys()) { + Class clazz = en.getType(); + if (Modifier.isAbstract(clazz.getModifiers())) continue; + WebServlet ws = clazz.getAnnotation(WebServlet.class); + if (ws == null || ws.value().length == 0) continue; + final HttpServlet servlet = clazz.newInstance(); + application.factory.inject(servlet); + String[] mappings = ws.value(); + if (ws.fillurl() && !prefix.isEmpty()) { + for (int i = 0; i < mappings.length; i++) { + mappings[i] = prefix + mappings[i]; + } + } + this.server.addHttpServlet(servlet, en.getProperty(), mappings); + if (sb != null) sb.append(threadName).append(" Loaded ").append(clazz.getName()).append(" --> ").append(Arrays.toString(mappings)).append(LINE_SEPARATOR); + } + if (sb != null && sb.length() > 0) logger.log(Level.FINE, sb.toString()); + } +} diff --git a/src/com/wentch/redkale/boot/NodeServer.java b/src/com/wentch/redkale/boot/NodeServer.java new file mode 100644 index 000000000..b5325e5b3 --- /dev/null +++ b/src/com/wentch/redkale/boot/NodeServer.java @@ -0,0 +1,250 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.boot; + +import com.wentch.redkale.net.sncp.ServiceEntry; +import com.wentch.redkale.net.Server; +import com.wentch.redkale.net.sncp.Sncp; +import com.wentch.redkale.service.Service; +import com.wentch.redkale.service.MultiService; +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.util.Ignore; +import com.wentch.redkale.boot.ClassFilter.FilterEntry; +import com.wentch.redkale.util.AnyValue.DefaultAnyValue; +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.net.InetSocketAddress; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public abstract class NodeServer { + + public static final String LINE_SEPARATOR = "\r\n"; + + protected final Logger logger; + + protected final Application application; + + private final CountDownLatch servicecdl; + + private final Server server; + + protected Consumer consumer; + + public NodeServer(Application application, CountDownLatch servicecdl, Server server) { + this.application = application; + this.servicecdl = servicecdl; + this.server = server; + this.logger = Logger.getLogger(this.getClass().getSimpleName()); + } + + public void load(final AnyValue config) throws Exception { + } + + public void init(AnyValue config) throws Exception { + //设置root文件夹 + String webroot = config.getValue("root", "root"); + File myroot = new File(webroot); + if (!webroot.contains(":") && !webroot.startsWith("/")) { + myroot = new File(System.getProperty(Application.RESNAME_HOME), webroot); + } + final String homepath = myroot.getCanonicalPath(); + Server.loadLib(logger, config.getValue("lib", "") + ";" + homepath + "/lib/*;" + homepath + "/classes"); + } + + public abstract InetSocketAddress getSocketAddress(); + + public void start() throws IOException { + server.start(); + } + + public void shutdown() throws IOException { + server.shutdown(); + } + + protected void loadLocalService(final Set> entrys) throws Exception { + final HashMap localServices = application.localServices; + for (final FilterEntry entry : entrys) { + Class serviceClass = entry.getType(); + if (serviceClass.getAnnotation(Ignore.class) != null) continue; + final boolean multi = MultiService.class.isAssignableFrom(serviceClass); + if (!multi) { //单例模式 + synchronized (application.localServices) { + ServiceEntry old = localServices.get(serviceClass); + if (old == null) { + old = new ServiceEntry(serviceClass, (Service) serviceClass.newInstance(), entry.getProperty(), ""); + localServices.put(serviceClass, old); + } + if (consumer != null) consumer.accept(old); + continue; + } + } + String name = multi ? entry.getName() : ""; + synchronized (application.localServices) { + ServiceEntry old = localServices.get(serviceClass); + if (old != null && old.containsName(name)) { + if (consumer != null) consumer.accept(old); + continue; + } + final Service service = (Service) serviceClass.newInstance(); + if (old == null) { + old = new ServiceEntry(serviceClass, service, entry.getProperty(), name); + localServices.put(serviceClass, old); + } else { + old.addName(name); + } + if (consumer != null) consumer.accept(old); + } + } + } + + protected void loadRemoteService(final Set> entrys) throws Exception { + for (FilterEntry entry : entrys) { + Class serviceClass = entry.getType(); + if (serviceClass.getAnnotation(Ignore.class) != null) continue; + String remote = entry.getAttachment().toString(); + Service service = Sncp.createRemoteService(entry.getName(), serviceClass, remote); + synchronized (application.remoteServices) { + application.remoteServices.add(new ServiceEntry(serviceClass, service, entry.getProperty(), entry.getName())); + } + } + } + + @SuppressWarnings("unchecked") + protected void loadService(final AnyValue servicesConf, ClassFilter serviceFilter) throws Exception { + if (serviceFilter == null) return; + final String threadName = "[" + Thread.currentThread().getName() + "] "; + final Set> entrys = serviceFilter.getFilterEntrys(); + final Set> localentrys = new HashSet<>(entrys.size()); + final Set> remotentrys = new HashSet<>(entrys.size()); + final String defremote = getRemoteName(servicesConf, "LOCAL"); + for (FilterEntry entry : entrys) { //service实现类 + final Class type = entry.getType(); + if (type.isInterface()) continue; + if (Modifier.isFinal(type.getModifiers())) continue; + if (!Modifier.isPublic(type.getModifiers())) continue; + if (Modifier.isAbstract(type.getModifiers())) continue; + if (type.getAnnotation(Ignore.class) != null) continue; + final String remote = getRemoteName(entry.getProperty(), defremote); + if ("LOCAL".equals(remote)) { //本地模式 + localentrys.add(entry); + } else { //远程模式 + entry.setAttachment(remote); + remotentrys.add(entry); + } + } + loadLocalService(localentrys); + loadRemoteService(remotentrys); + servicecdl.countDown(); + servicecdl.await(); + + final StringBuilder sb = logger.isLoggable(Level.INFO) ? new StringBuilder() : null; + synchronized (application) { + if (!application.serviceInited) { + application.serviceInited = true; + //--------------- register --------------- + application.localServices.forEach((x, y) -> { + y.getNames().forEach(n -> application.factory.register(n, y.getServiceClass(), y.getService())); + }); + application.remoteServices.forEach(y -> { + y.getNames().forEach(n -> application.factory.register(n, y.getServiceClass(), y.getService())); + }); + //---------------- inject ---------------- + application.localServices.forEach((x, y) -> { + application.factory.inject(y.getService()); + }); + application.remoteServices.forEach(y -> { + application.factory.inject(y.getService()); + }); + //----------------- init ----------------- + application.localServices.entrySet().parallelStream().forEach(k -> { + Class x = k.getKey(); + ServiceEntry y = k.getValue(); + long s = System.currentTimeMillis(); + y.getService().init(y.getServiceConf()); + long e = System.currentTimeMillis() - s; + if (e > 2 && sb != null) { + sb.append(threadName).append("LocalService(").append(y.getNames()).append("|").append(y.getServiceClass()).append(") init ").append(e).append("ms").append(LINE_SEPARATOR); + } + }); + if (sb != null && sb.length() > 0) logger.log(Level.INFO, sb.toString()); + } + } + } + + private String getRemoteName(AnyValue av, String remote) { + remote = (remote == null || remote.trim().isEmpty()) ? "LOCAL" : remote.trim(); + if (av == null) return remote; + String r = av.getValue("remote"); + if ("LOCAL".equalsIgnoreCase(r)) return "LOCAL"; + if (r != null && !r.trim().isEmpty()) return r.trim(); + return remote; + } + + protected static ClassFilter createServiceClassFilter(final String localNode, final AnyValue config) { + return createClassFilter(localNode, config, null, Service.class, Annotation.class, "services", "service"); + } + + protected static ClassFilter createClassFilter(final String localNode, final AnyValue config, Class ref, + Class inter, Class ref2, String properties, String property) { + ClassFilter filter = new ClassFilter(ref, inter); + if (properties == null && properties == null) return filter; + AnyValue list = config == null ? null : config.getAnyValue(properties); + if (list == null) return filter; + if ("services".equals(properties)) { + for (AnyValue group : list.getAnyValues("group")) { + String remotenames = group.getValue("remotenames"); + for (AnyValue propnode : group.getAnyValues(property)) { + boolean hasremote = false; + if (remotenames != null) { + for (String rn : remotenames.split(";")) { + rn = rn.trim(); + if (rn.isEmpty() || localNode.equals(rn)) continue; + DefaultAnyValue s = new DefaultAnyValue(); + s.addValue("value", propnode.getValue("value")); + s.addValue("name", rn); + s.addValue("remote", rn); + ((DefaultAnyValue) list).addValue(property, s); + hasremote = true; + } + } + if (hasremote) { + ((DefaultAnyValue) propnode).setValue("name", localNode); + ((DefaultAnyValue) propnode).setValue("remote", ""); + ((DefaultAnyValue) list).addValue(property, propnode); + } + } + } + } + for (AnyValue av : list.getAnyValues(property)) { + for (AnyValue prop : av.getAnyValues("property")) { + ((DefaultAnyValue) av).addValue(prop.getValue("name"), prop.getValue("value")); + } + filter.filter(av, av.getValue("value"), false); + } + if (list.getBoolValue("autoload", true)) { + String includes = list.getValue("includes", ""); + String excludes = list.getValue("excludes", ""); + filter.setIncludePatterns(includes.split(";")); + filter.setExcludePatterns(excludes.split(";")); + } else { + if (ref2 == null || ref2 == Annotation.class) { + filter.setRefused(true); + } else if (ref2 != Annotation.class) { + filter.setAnnotationClass(ref2); + } + } + return filter; + } + +} diff --git a/src/com/wentch/redkale/boot/NodeSncpServer.java b/src/com/wentch/redkale/boot/NodeSncpServer.java new file mode 100644 index 000000000..8f6e74899 --- /dev/null +++ b/src/com/wentch/redkale/boot/NodeSncpServer.java @@ -0,0 +1,50 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.boot; + +import com.wentch.redkale.net.sncp.SncpServer; +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.service.Service; +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; + +/** + * + * @author zhangjx + */ +public final class NodeSncpServer extends NodeServer { + + private final SncpServer server; + + public NodeSncpServer(Application application, CountDownLatch regcdl, SncpServer server) { + super(application, regcdl, server); + this.server = server; + this.consumer = x -> server.addService(x); + } + + @Override + public void init(AnyValue config) throws Exception { + server.init(config); + super.init(config); + } + + @Override + public InetSocketAddress getSocketAddress() { + return server == null ? null : server.getSocketAddress(); + } + + @Override + public void load(AnyValue config) throws Exception { + super.load(config); + ClassFilter serviceFilter = createServiceClassFilter(application.nodeName, config); + long s = System.currentTimeMillis(); + ClassFilter.Loader.load(application.getHome(), serviceFilter); + long e = System.currentTimeMillis() - s; + logger.info(this.getClass().getSimpleName() + " load filter class in " + e + " ms"); + loadService(config.getAnyValue("services"), serviceFilter); //必须在servlet之前 + } + +} diff --git a/src/com/wentch/redkale/convert/AnyEncoder.java b/src/com/wentch/redkale/convert/AnyEncoder.java new file mode 100644 index 000000000..6522a3b31 --- /dev/null +++ b/src/com/wentch/redkale/convert/AnyEncoder.java @@ -0,0 +1,40 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.Type; + +/** + * 对不明类型的对象进行序列化; BSON序列化时将对象的类名写入Writer,JSON则不写入。 + * + * @author zhangjx + * @param + */ +public final class AnyEncoder implements Encodeable { + + final Factory factory; + + AnyEncoder(Factory factory) { + this.factory = factory; + } + + @Override + @SuppressWarnings("unchecked") + public void convertTo(final Writer out, final T value) { + if (value == null) { + out.writeNull(); + } else { + out.wirteClassName(value.getClass()); + factory.loadEncoder(value.getClass()).convertTo(out, value); + } + } + + @Override + public Type getType() { + return Object.class; + } + +} diff --git a/src/com/wentch/redkale/convert/ArrayDecoder.java b/src/com/wentch/redkale/convert/ArrayDecoder.java new file mode 100644 index 000000000..fa41b8b3a --- /dev/null +++ b/src/com/wentch/redkale/convert/ArrayDecoder.java @@ -0,0 +1,79 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.*; +import java.util.*; + +/** + * 对象数组的序列化,不包含int[]、long[]这样的primitive class数组. + * 数组长度不能超过 32767。 在BSON中数组长度设定的是short,对于大于32767长度的数组传输会影响性能,所以没有采用int存储。 + * 支持一定程度的泛型。 + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +public final class ArrayDecoder implements Decodeable { + + private final Type type; + + private final Type componentType; + + private final Class componentClass; + + private final Decodeable decoder; + + public ArrayDecoder(final Factory factory, final Type type) { + this.type = type; + if (type instanceof GenericArrayType) { + Type t = ((GenericArrayType) type).getGenericComponentType(); + this.componentType = t instanceof TypeVariable ? Object.class : t; + } else if ((type instanceof Class) && ((Class) type).isArray()) { + this.componentType = ((Class) type).getComponentType(); + } else { + throw new ConvertException("(" + type + ") is not a array type"); + } + if (this.componentType instanceof ParameterizedType) { + this.componentClass = (Class) ((ParameterizedType) this.componentType).getRawType(); + } else { + this.componentClass = (Class) this.componentType; + } + factory.register(type, this); + this.decoder = factory.loadDecoder(this.componentType); + } + + @Override + public T[] convertFrom(Reader in) { + final int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) return null; + final Decodeable localdecoder = this.decoder; + final List result = new ArrayList(); + if (len == Reader.SIGN_NOLENGTH) { + while (in.hasNext()) { + result.add(localdecoder.convertFrom(in)); + } + } else { + for (int i = 0; i < len; i++) { + result.add(localdecoder.convertFrom(in)); + } + } + in.readArrayE(); + T[] rs = (T[]) Array.newInstance((Class) this.componentClass, result.size()); + return result.toArray(rs); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{componentType:" + this.componentType + ", decoder:" + this.decoder + "}"; + } + + @Override + public Type getType() { + return type; + } + +} diff --git a/src/com/wentch/redkale/convert/ArrayEncoder.java b/src/com/wentch/redkale/convert/ArrayEncoder.java new file mode 100644 index 000000000..f85744375 --- /dev/null +++ b/src/com/wentch/redkale/convert/ArrayEncoder.java @@ -0,0 +1,75 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.*; + +/** + * 对象数组的反序列化,不包含int[]、long[]这样的primitive class数组. + * 数组长度不能超过 32767。 在BSON中数组长度设定的是short,对于大于32767长度的数组传输会影响性能,所以没有采用int存储。 + * 支持一定程度的泛型。 + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +public final class ArrayEncoder implements Encodeable { + + private final Type type; + + private final Type componentType; + + private final Encodeable anyEncoder; + + private final Encodeable encoder; + + public ArrayEncoder(final Factory factory, final Type type) { + this.type = type; + if (type instanceof GenericArrayType) { + Type t = ((GenericArrayType) type).getGenericComponentType(); + this.componentType = t instanceof TypeVariable ? Object.class : t; + } else if ((type instanceof Class) && ((Class) type).isArray()) { + this.componentType = ((Class) type).getComponentType(); + } else { + throw new ConvertException("(" + type + ") is not a array type"); + } + factory.register(type, this); + this.encoder = factory.loadEncoder(this.componentType); + this.anyEncoder = factory.getAnyEncoder(); + } + + @Override + public void convertTo(Writer out, T[] value) { + if (value == null) { + out.writeNull(); + return; + } + if (value.length == 0) { + out.writeArrayB(0); + out.writeArrayE(); + return; + } + out.writeArrayB(value.length); + final Type comp = this.componentType; + boolean first = true; + for (Object v : value) { + if (!first) out.writeArrayMark(); + ((v.getClass() == comp) ? encoder : anyEncoder).convertTo(out, v); + if (first) first = false; + } + out.writeArrayE(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{componentType:" + this.componentType + ", encoder:" + this.encoder + "}"; + } + + @Override + public Type getType() { + return type; + } +} diff --git a/src/com/wentch/redkale/convert/CollectionDecoder.java b/src/com/wentch/redkale/convert/CollectionDecoder.java new file mode 100644 index 000000000..df87cfd99 --- /dev/null +++ b/src/com/wentch/redkale/convert/CollectionDecoder.java @@ -0,0 +1,69 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import com.wentch.redkale.util.Creator; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +/** + * 对象集合的反序列化. + * 集合大小不能超过 32767。 在BSON中集合大小设定的是short,对于大于32767长度的集合传输会影响性能,所以没有采用int存储。 + * 支持一定程度的泛型。 + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +public final class CollectionDecoder implements Decodeable> { + + private final Type type; + + private final Type componentType; + + protected Creator> creator; + + private final Decodeable decoder; + + public CollectionDecoder(final Factory factory, final Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) type; + this.componentType = pt.getActualTypeArguments()[0]; + this.creator = factory.loadCreator((Class) pt.getRawType()); + factory.register(type, this); + this.decoder = factory.loadDecoder(this.componentType); + } else { + throw new ConvertException("collectiondecoder not support the type (" + type + ")"); + } + } + + @Override + public Collection convertFrom(Reader in) { + final int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) return null; + final Decodeable localdecoder = this.decoder; + final Collection result = this.creator.create(); + if (len == Reader.SIGN_NOLENGTH) { + while (in.hasNext()) { + result.add(localdecoder.convertFrom(in)); + } + } else { + for (int i = 0; i < len; i++) { + result.add(localdecoder.convertFrom(in)); + } + } + in.readArrayE(); + return result; + } + + @Override + public Type getType() { + return type; + } + +} diff --git a/src/com/wentch/redkale/convert/CollectionEncoder.java b/src/com/wentch/redkale/convert/CollectionEncoder.java new file mode 100644 index 000000000..e823a5775 --- /dev/null +++ b/src/com/wentch/redkale/convert/CollectionEncoder.java @@ -0,0 +1,65 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.*; +import java.util.Collection; + +/** + * 对象集合的序列化. + * 集合大小不能超过 32767。 在BSON中集合大小设定的是short,对于大于32767长度的集合传输会影响性能,所以没有采用int存储。 + * 支持一定程度的泛型。 + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +public final class CollectionEncoder implements Encodeable> { + + private final Type type; + + private final Encodeable encoder; + + public CollectionEncoder(final Factory factory, final Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + Type t = ((ParameterizedType) type).getActualTypeArguments()[0]; + if (t instanceof TypeVariable) { + this.encoder = factory.getAnyEncoder(); + } else { + this.encoder = factory.loadEncoder(t); + } + } else { + this.encoder = factory.getAnyEncoder(); + } + } + + @Override + public void convertTo(Writer out, Collection value) { + if (value == null) { + out.writeNull(); + return; + } + if (value.isEmpty()) { + out.writeArrayB(0); + out.writeArrayE(); + return; + } + out.writeArrayB(value.size()); + boolean first = true; + for (Object v : value) { + if (!first) out.writeArrayMark(); + encoder.convertTo(out, v); + if (first) first = false; + } + out.writeArrayE(); + } + + @Override + public Type getType() { + return type; + } +} diff --git a/src/com/wentch/redkale/convert/Convert.java b/src/com/wentch/redkale/convert/Convert.java new file mode 100644 index 000000000..61b42ff63 --- /dev/null +++ b/src/com/wentch/redkale/convert/Convert.java @@ -0,0 +1,26 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +/** + * 序列化操作类 + * + * @author zhangjx + * @param + * @param + */ +public abstract class Convert { + + protected final Factory factory; + + protected Convert(Factory factory) { + this.factory = factory; + } + + public Factory getFactory() { + return this.factory; + } +} diff --git a/src/com/wentch/redkale/convert/ConvertColumn.java b/src/com/wentch/redkale/convert/ConvertColumn.java new file mode 100644 index 000000000..1a868f0c4 --- /dev/null +++ b/src/com/wentch/redkale/convert/ConvertColumn.java @@ -0,0 +1,44 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + +/** + * 依附在setter、getter方法、字段进行简单的配置 + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({METHOD, FIELD}) +@Retention(RUNTIME) +@Repeatable(ConvertColumns.class) +public @interface ConvertColumn { + + /** + * 给字段取个别名, 只对JSON有效 + * + * @return + */ + String name() default ""; + + /** + * 解析/序列化时是否屏蔽该字段 + * + * @return + */ + boolean ignore() default false; + + /** + * 解析/序列化定制化的TYPE + * + * @return + */ + ConvertType type() default ConvertType.ALL; +} diff --git a/src/com/wentch/redkale/convert/ConvertColumnEntry.java b/src/com/wentch/redkale/convert/ConvertColumnEntry.java new file mode 100644 index 000000000..c5339b17a --- /dev/null +++ b/src/com/wentch/redkale/convert/ConvertColumnEntry.java @@ -0,0 +1,67 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +/** + * ConvertColumn 对应的实体类 + * + * @author zhangjx + */ +public final class ConvertColumnEntry { + + private String name = ""; + + private boolean ignore; + + private ConvertType convertType; + + public ConvertColumnEntry() { + } + + public ConvertColumnEntry(ConvertColumn column) { + if (column == null) return; + this.name = column.name(); + this.ignore = column.ignore(); + this.convertType = column.type(); + } + + public ConvertColumnEntry(String name, boolean ignore) { + this.name = name; + this.ignore = ignore; + this.convertType = ConvertType.ALL; + } + + public ConvertColumnEntry(String name, boolean ignore, ConvertType convertType) { + this.name = name; + this.ignore = ignore; + this.convertType = convertType; + } + + public String name() { + return name == null ? "" : name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean ignore() { + return ignore; + } + + public void setIgnore(boolean ignore) { + this.ignore = ignore; + } + + public ConvertType type() { + return convertType == null ? ConvertType.ALL : convertType; + } + + public void setConvertType(ConvertType convertType) { + this.convertType = convertType; + } + +} diff --git a/src/com/wentch/redkale/convert/ConvertColumns.java b/src/com/wentch/redkale/convert/ConvertColumns.java new file mode 100644 index 000000000..a5e613ad0 --- /dev/null +++ b/src/com/wentch/redkale/convert/ConvertColumns.java @@ -0,0 +1,24 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + +/** + * ConvertColumn 的多用类 + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({METHOD, FIELD}) +@Retention(RUNTIME) +public @interface ConvertColumns { + + ConvertColumn[] value(); +} diff --git a/src/com/wentch/redkale/convert/ConvertException.java b/src/com/wentch/redkale/convert/ConvertException.java new file mode 100644 index 000000000..002c6946e --- /dev/null +++ b/src/com/wentch/redkale/convert/ConvertException.java @@ -0,0 +1,28 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +/** + * + * @author zhangjx + */ +public class ConvertException extends RuntimeException { + + public ConvertException() { + super(); + } + + public ConvertException(String s) { + super(s); + } + + public ConvertException(String message, Throwable cause) { + super(message, cause); + } + + public ConvertException(Throwable cause) { + super(cause); + } +} diff --git a/src/com/wentch/redkale/convert/ConvertType.java b/src/com/wentch/redkale/convert/ConvertType.java new file mode 100644 index 000000000..7871a01d8 --- /dev/null +++ b/src/com/wentch/redkale/convert/ConvertType.java @@ -0,0 +1,28 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +/** + * + * @author zhangjx + */ +public enum ConvertType { + + JSON(1), + BSON(2), + ALL(127); + + private final int value; + + private ConvertType(int v) { + this.value = v; + } + + public boolean contains(ConvertType type) { + if (type == null) return false; + return this.value >= type.value && (this.value & type.value) > 0; + } +} diff --git a/src/com/wentch/redkale/convert/DeMember.java b/src/com/wentch/redkale/convert/DeMember.java new file mode 100644 index 000000000..2897e7d23 --- /dev/null +++ b/src/com/wentch/redkale/convert/DeMember.java @@ -0,0 +1,64 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import com.wentch.redkale.util.Attribute; + +/** + * + * @author zhangjx + * @param + * @param + * @param + */ +@SuppressWarnings("unchecked") +public final class DeMember implements Comparable> { + + protected final Attribute attribute; + + protected Decodeable decoder; + + public DeMember(final Attribute attribute) { + this.attribute = attribute; + } + + public DeMember(Attribute attribute, Decodeable decoder) { + this(attribute); + this.decoder = decoder; + } + + public final void read(R in, T obj) { + this.attribute.set(obj, decoder.convertFrom(in)); + } + + public Attribute getAttribute() { + return this.attribute; + } + + @Override + public final int compareTo(DeMember o) { + if (o == null) return 1; + return this.attribute.field().compareTo(o.attribute.field()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof DeMember)) return false; + DeMember other = (DeMember) obj; + return compareTo(other) == 0; + } + + @Override + public int hashCode() { + return this.attribute.field().hashCode(); + } + + @Override + public String toString() { + return "DeMember{" + "attribute=" + attribute.field() + ", decoder=" + decoder + '}'; + } +} diff --git a/src/com/wentch/redkale/convert/Decodeable.java b/src/com/wentch/redkale/convert/Decodeable.java new file mode 100644 index 000000000..c6b58899a --- /dev/null +++ b/src/com/wentch/redkale/convert/Decodeable.java @@ -0,0 +1,27 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.Type; + +/** + * + * @author zhangjx + * @param + * @param + */ +public interface Decodeable { + + public T convertFrom(final R in); + + /** + * 泛型映射接口 + * + * @return + */ + public Type getType(); + +} diff --git a/src/com/wentch/redkale/convert/EnMember.java b/src/com/wentch/redkale/convert/EnMember.java new file mode 100644 index 000000000..85fb0fe4f --- /dev/null +++ b/src/com/wentch/redkale/convert/EnMember.java @@ -0,0 +1,60 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import com.wentch.redkale.util.Attribute; + +/** + * + * @author zhangjx + * @param + * @param + * @param + */ +@SuppressWarnings("unchecked") +public final class EnMember implements Comparable> { + + private final Attribute attribute; + + final Encodeable encoder; + + public EnMember(Attribute attribute, Encodeable encoder) { + this.attribute = attribute; + this.encoder = encoder; + } + + public boolean write(final W out, final boolean comma, final T obj) { + F value = attribute.get(obj); + if (value == null) return comma; + out.writeField(comma, attribute); + encoder.convertTo(out, value); + return true; + } + + @Override + public final int compareTo(EnMember o) { + if (o == null) return 1; + return this.attribute.field().compareTo(o.attribute.field()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof EnMember)) return false; + EnMember other = (EnMember) obj; + return compareTo(other) == 0; + } + + @Override + public int hashCode() { + return this.attribute.field().hashCode(); + } + + @Override + public String toString() { + return "EnMember{" + "attribute=" + attribute.field() + ", encoder=" + encoder + '}'; + } +} diff --git a/src/com/wentch/redkale/convert/Encodeable.java b/src/com/wentch/redkale/convert/Encodeable.java new file mode 100644 index 000000000..595383445 --- /dev/null +++ b/src/com/wentch/redkale/convert/Encodeable.java @@ -0,0 +1,27 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.Type; + +/** + * + * @author zhangjx + * @param + * @param + */ +public interface Encodeable { + + public void convertTo(final W out, T value); + + /** + * 泛型映射接口 + * + * @return + */ + public Type getType(); + +} diff --git a/src/com/wentch/redkale/convert/Factory.java b/src/com/wentch/redkale/convert/Factory.java new file mode 100644 index 000000000..41da5a10b --- /dev/null +++ b/src/com/wentch/redkale/convert/Factory.java @@ -0,0 +1,342 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import com.wentch.redkale.convert.ext.StringArraySimpledCoder; +import com.wentch.redkale.convert.ext.DoubleArraySimpledCoder; +import com.wentch.redkale.convert.ext.LongSimpledCoder; +import com.wentch.redkale.convert.ext.ByteArraySimpledCoder; +import com.wentch.redkale.convert.ext.IntArraySimpledCoder; +import com.wentch.redkale.convert.ext.DoubleSimpledCoder; +import com.wentch.redkale.convert.ext.TwoLongSimpledCoder; +import com.wentch.redkale.convert.ext.CharSimpledCoder; +import com.wentch.redkale.convert.ext.IntSimpledCoder; +import com.wentch.redkale.convert.ext.InetAddressSimpledCoder; +import com.wentch.redkale.convert.ext.LongArraySimpledCoder; +import com.wentch.redkale.convert.ext.DateSimpledCoder; +import com.wentch.redkale.convert.ext.BoolSimpledCoder; +import com.wentch.redkale.convert.ext.CharArraySimpledCoder; +import com.wentch.redkale.convert.ext.EnumSimpledCoder; +import com.wentch.redkale.convert.ext.BigIntegerSimpledCoder; +import com.wentch.redkale.convert.ext.ByteSimpledCoder; +import com.wentch.redkale.convert.ext.StringSimpledCoder; +import com.wentch.redkale.convert.ext.NumberSimpledCoder; +import com.wentch.redkale.convert.ext.TypeSimpledCoder; +import com.wentch.redkale.convert.ext.ShortArraySimpledCoder; +import com.wentch.redkale.convert.ext.BoolArraySimpledCoder; +import com.wentch.redkale.convert.ext.ShortSimpledCoder; +import com.wentch.redkale.convert.ext.FloatArraySimpledCoder; +import com.wentch.redkale.convert.ext.FloatSimpledCoder; +import com.wentch.redkale.util.TwoLong; +import com.wentch.redkale.util.Creator; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Map; +import java.lang.reflect.*; +import java.math.BigInteger; +import java.net.*; +import static com.wentch.redkale.convert.ext.InetAddressSimpledCoder.*; +import java.util.*; + +/** + * + * @author zhangjx + * @param + * @param + */ +@SuppressWarnings("unchecked") +public abstract class Factory { + + private final Factory parent; + + protected Convert convert; + + private final Encodeable anyEncoder = new AnyEncoder(this); + + //----------------------------------------------------------------------------------- + private final HashedMap creators = new HashedMap(); + + private final HashedMap> decoders = new HashedMap(); + + private final HashedMap> encoders = new HashedMap(); + + private final HashMap columnEntrys = new HashMap(); + + protected Factory(Factory parent) { + this.parent = parent; + if (parent == null) { + //--------------------------------------------------------- + this.register(boolean.class, BoolSimpledCoder.instance); + this.register(Boolean.class, BoolSimpledCoder.instance); + + this.register(byte.class, ByteSimpledCoder.instance); + this.register(Byte.class, ByteSimpledCoder.instance); + + this.register(short.class, ShortSimpledCoder.instance); + this.register(Short.class, ShortSimpledCoder.instance); + + this.register(char.class, CharSimpledCoder.instance); + this.register(Character.class, CharSimpledCoder.instance); + + this.register(int.class, IntSimpledCoder.instance); + this.register(Integer.class, IntSimpledCoder.instance); + + this.register(long.class, LongSimpledCoder.instance); + this.register(Long.class, LongSimpledCoder.instance); + + this.register(float.class, FloatSimpledCoder.instance); + this.register(Float.class, FloatSimpledCoder.instance); + + this.register(double.class, DoubleSimpledCoder.instance); + this.register(Double.class, DoubleSimpledCoder.instance); + + this.register(Number.class, NumberSimpledCoder.instance); + this.register(String.class, StringSimpledCoder.instance); + this.register(java.util.Date.class, DateSimpledCoder.instance); + this.register(BigInteger.class, BigIntegerSimpledCoder.instance); + this.register(InetAddress.class, InetAddressSimpledCoder.instance); + this.register(TwoLong.class, TwoLongSimpledCoder.instance); + this.register(Class.class, TypeSimpledCoder.instance); + this.register(InetSocketAddress.class, InetSocketAddressSimpledCoder.instance); + this.register(InetSocketAddress.class, InetSocketAddressSimpledCoder.instance); + //--------------------------------------------------------- + this.register(boolean[].class, BoolArraySimpledCoder.instance); + this.register(byte[].class, ByteArraySimpledCoder.instance); + this.register(short[].class, ShortArraySimpledCoder.instance); + this.register(char[].class, CharArraySimpledCoder.instance); + this.register(int[].class, IntArraySimpledCoder.instance); + this.register(long[].class, LongArraySimpledCoder.instance); + this.register(float[].class, FloatArraySimpledCoder.instance); + this.register(double[].class, DoubleArraySimpledCoder.instance); + this.register(String[].class, StringArraySimpledCoder.instance); + //--------------------------------------------------------- + } + } + + public Factory parent() { + return this.parent; + } + + public abstract ConvertType getConvertType(); + + public abstract boolean isReversible(); + + public abstract Factory createChild(); + + public Convert getConvert() { + return convert; + } + + public ConvertColumnEntry findRef(AccessibleObject field) { + if (field == null) return null; + ConvertColumnEntry en = this.columnEntrys.get(field); + if (en != null) return en; + final ConvertType ct = this.getConvertType(); + for (ConvertColumn ref : field.getAnnotationsByType(ConvertColumn.class)) { + if (ref.type().contains(ct)) return new ConvertColumnEntry(ref); + } + return null; + } + + public final boolean register(final Class type, String column, ConvertColumnEntry entry) { + if (type == null || column == null || entry == null) return false; + try { + final Field field = type.getDeclaredField(column); + String get = "get"; + if (field.getType() == boolean.class || field.getType() == Boolean.class) get = "is"; + char[] cols = column.toCharArray(); + cols[0] = Character.toUpperCase(cols[0]); + String col2 = new String(cols); + try { + register(type.getMethod(get + col2), entry); + } catch (Exception ex) { + } + try { + register(type.getMethod("set" + col2, field.getType()), entry); + } catch (Exception ex) { + } + return register(field, entry); + } catch (Exception e) { + return false; + } + } + + public final boolean register(final AccessibleObject field, final ConvertColumnEntry entry) { + if (field == null || entry == null) return false; + this.columnEntrys.put(field, entry); + return true; + } + + public final void register(final Class clazz, final Creator creator) { + creators.put(clazz, creator); + } + + public final Creator findCreator(Class type) { + Creator creator = creators.get(type); + if (creator != null) return creator; + return this.parent == null ? null : this.parent.findCreator(type); + } + + public final Creator loadCreator(Class type) { + Creator result = findCreator(type); + if (result == null) { + result = Creator.create(type); + creators.put(type, result); + } + return result; + } + + //---------------------------------------------------------------------- + public final Encodeable getAnyEncoder() { + return (Encodeable) anyEncoder; + } + + public final void register(final Type clazz, final SimpledCoder coder) { + decoders.put(clazz, coder); + encoders.put(clazz, coder); + } + + public final void register(final Type clazz, final Decodeable decoder) { + decoders.put(clazz, decoder); + } + + public final void register(final Type clazz, final Encodeable printer) { + encoders.put(clazz, printer); + } + + public final Decodeable findDecoder(final Type type) { + Decodeable rs = (Decodeable) decoders.get(type); + if (rs != null) return rs; + return this.parent == null ? null : this.parent.findDecoder(type); + } + + public final Encodeable findEncoder(final Type type) { + Encodeable rs = (Encodeable) encoders.get(type); + if (rs != null) return rs; + return this.parent == null ? null : this.parent.findEncoder(type); + } + + public final Decodeable loadDecoder(final Type type) { + Decodeable decoder = findDecoder(type); + if (decoder != null) return decoder; + if (type instanceof GenericArrayType) return new ArrayDecoder(this, type); + Class clazz; + if (type instanceof ParameterizedType) { + final ParameterizedType pts = (ParameterizedType) type; + clazz = (Class) (pts).getRawType(); + } else if (type instanceof Class) { + clazz = (Class) type; + } else { + throw new ConvertException("not support the type (" + type + ")"); + } + decoder = findDecoder(clazz); + if (decoder != null) return decoder; + return createDecoder(type, clazz); + } + + public final Decodeable createDecoder(final Type type) { + Class clazz; + if (type instanceof ParameterizedType) { + final ParameterizedType pts = (ParameterizedType) type; + clazz = (Class) (pts).getRawType(); + } else if (type instanceof Class) { + clazz = (Class) type; + } else { + throw new ConvertException("not support the type (" + type + ")"); + } + return createDecoder(type, clazz); + } + + private Decodeable createDecoder(final Type type, final Class clazz) { + Decodeable decoder = null; + ObjectDecoder od = null; + if (clazz.isEnum()) { + decoder = new EnumSimpledCoder(clazz); + } else if (clazz.isArray()) { + decoder = new ArrayDecoder(this, type); + } else if (Collection.class.isAssignableFrom(clazz)) { + decoder = new CollectionDecoder(this, type); + } else if (Map.class.isAssignableFrom(clazz)) { + decoder = new MapDecoder(this, type); + } else if (clazz == Object.class) { + od = new ObjectDecoder(type); + decoder = od; + } else if (!clazz.getName().startsWith("java.")) { + od = new ObjectDecoder(type); + decoder = od; + } + if (decoder == null) throw new ConvertException("not support the type (" + type + ")"); + register(type, decoder); + if (od != null) od.init(this); + return decoder; + } + + public final Encodeable loadEncoder(final Type type) { + Encodeable encoder = findEncoder(type); + if (encoder != null) return encoder; + if (type instanceof GenericArrayType) return new ArrayEncoder(this, type); + Class clazz; + if (type instanceof ParameterizedType) { + final ParameterizedType pts = (ParameterizedType) type; + clazz = (Class) (pts).getRawType(); + } else if (type instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) type; + Type t = Object.class; + if (tv.getBounds().length == 1) { + t = tv.getBounds()[0]; + } + if (!(t instanceof Class)) t = Object.class; + clazz = (Class) t; + } else if (type instanceof Class) { + clazz = (Class) type; + } else { + throw new ConvertException("not support the type (" + type + ")"); + } + encoder = findEncoder(clazz); + if (encoder != null) return encoder; + return createEncoder(type, clazz); + } + + public final Encodeable createEncoder(final Type type) { + Class clazz; + if (type instanceof ParameterizedType) { + final ParameterizedType pts = (ParameterizedType) type; + clazz = (Class) (pts).getRawType(); + } else if (type instanceof Class) { + clazz = (Class) type; + } else { + throw new ConvertException("not support the type (" + type + ")"); + } + return createEncoder(type, clazz); + } + + private Encodeable createEncoder(final Type type, final Class clazz) { + Encodeable encoder = null; + ObjectEncoder oe = null; + if (clazz.isEnum()) { + encoder = new EnumSimpledCoder(clazz); + } else if (clazz.isArray()) { + encoder = new ArrayEncoder(this, type); + } else if (Collection.class.isAssignableFrom(clazz)) { + encoder = new CollectionEncoder(this, type); + } else if (Map.class.isAssignableFrom(clazz)) { + encoder = new MapEncoder(this, type); + } else if (clazz == Object.class) { + return (Encodeable) this.anyEncoder; + } else if (!clazz.getName().startsWith("java.")) { + oe = new ObjectEncoder(type); + encoder = oe; + } + if (encoder == null) throw new ConvertException("not support the type (" + type + ")"); + register(type, encoder); + if (oe != null) oe.init(this); + return encoder; + + } + +} diff --git a/src/com/wentch/redkale/convert/HashedMap.java b/src/com/wentch/redkale/convert/HashedMap.java new file mode 100644 index 000000000..acd7fbbec --- /dev/null +++ b/src/com/wentch/redkale/convert/HashedMap.java @@ -0,0 +1,68 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +/** + * 只增不减的伪Map类 + * + * @author zhangjx + * @param + * @param + */ +@SuppressWarnings("unchecked") +public final class HashedMap { + + protected final transient Entry[] table; + + public HashedMap() { + this(128); + } + + public HashedMap(int initCapacity) { + this.table = new Entry[Math.max(initCapacity, 16)]; + } + + public final V get(final K key) { + final K k = key; + final Entry[] data = this.table; + Entry entry = data[k.hashCode() & (data.length - 1)]; + while (entry != null) { + if (k == entry.key) return entry.value; + entry = entry.next; + } + return null; + } + + public final V put(K key, V value) { + final K k = key; + final Entry[] data = this.table; + final int index = k.hashCode() & (data.length - 1); + Entry entry = data[index]; + while (entry != null) { + if (k == entry.key) { + entry.value = value; + return entry.value; + } + entry = entry.next; + } + data[index] = new Entry<>(key, value, data[index]); + return null; + } + + protected static final class Entry { + + protected V value; + + protected final K key; + + protected final Entry next; + + protected Entry(K key, V value, Entry next) { + this.key = key; + this.value = value; + this.next = next; + } + } +} diff --git a/src/com/wentch/redkale/convert/MapDecoder.java b/src/com/wentch/redkale/convert/MapDecoder.java new file mode 100644 index 000000000..dfd7c2c1e --- /dev/null +++ b/src/com/wentch/redkale/convert/MapDecoder.java @@ -0,0 +1,78 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import com.wentch.redkale.util.Creator; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; + +/** + * + * @author zhangjx + * @param + * @param + */ +@SuppressWarnings("unchecked") +public final class MapDecoder implements Decodeable> { + + private final Type type; + + private final Type keyType; + + private final Type valueType; + + protected Creator> creator; + + private final Decodeable keyDecoder; + + private final Decodeable valueDecoder; + + public MapDecoder(final Factory factory, final Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) type; + this.keyType = pt.getActualTypeArguments()[0]; + this.valueType = pt.getActualTypeArguments()[1]; + this.creator = factory.loadCreator((Class) pt.getRawType()); + factory.register(type, this); + this.keyDecoder = factory.loadDecoder(this.keyType); + this.valueDecoder = factory.loadDecoder(this.valueType); + } else { + throw new ConvertException("mapdecoder not support the type (" + type + ")"); + } + } + + @Override + public Map convertFrom(Reader in) { + final int len = in.readMapB(); + if (len == Reader.SIGN_NULL) return null; + final Map result = this.creator.create(); + if (len == Reader.SIGN_NOLENGTH) { + while (in.hasNext()) { + K key = keyDecoder.convertFrom(in); + in.skipBlank(); + V value = valueDecoder.convertFrom(in); + result.put(key, value); + } + } else { + for (int i = 0; i < len; i++) { + K key = keyDecoder.convertFrom(in); + in.skipBlank(); + V value = valueDecoder.convertFrom(in); + result.put(key, value); + } + } + in.readMapE(); + return result; + } + + @Override + public Type getType() { + return this.type; + } + +} diff --git a/src/com/wentch/redkale/convert/MapEncoder.java b/src/com/wentch/redkale/convert/MapEncoder.java new file mode 100644 index 000000000..b4c12ff39 --- /dev/null +++ b/src/com/wentch/redkale/convert/MapEncoder.java @@ -0,0 +1,62 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; + +/** + * + * @author zhangjx + * @param + * @param + */ +@SuppressWarnings("unchecked") +public final class MapEncoder implements Encodeable> { + + private final Type type; + + private final Encodeable keyencoder; + + private final Encodeable valencoder; + + public MapEncoder(final Factory factory, final Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + final Type[] pt = ((ParameterizedType) type).getActualTypeArguments(); + this.keyencoder = factory.loadEncoder(pt[0]); + this.valencoder = factory.loadEncoder(pt[1]); + } else { + this.keyencoder = factory.getAnyEncoder(); + this.valencoder = factory.getAnyEncoder(); + } + } + + @Override + public void convertTo(Writer out, Map value) { + final Map values = value; + if (values == null) { + out.writeNull(); + return; + } + out.writeMapB(values.size()); + boolean first = true; + for (Map.Entry en : values.entrySet()) { + if (!first) out.writeArrayMark(); + this.keyencoder.convertTo(out, en.getKey()); + out.writeMapMark(); + this.valencoder.convertTo(out, en.getValue()); + if (first) first = false; + } + out.writeMapE(); + } + + @Override + public Type getType() { + return type; + } +} diff --git a/src/com/wentch/redkale/convert/ObjectDecoder.java b/src/com/wentch/redkale/convert/ObjectDecoder.java new file mode 100644 index 000000000..072ee8c46 --- /dev/null +++ b/src/com/wentch/redkale/convert/ObjectDecoder.java @@ -0,0 +1,144 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + + +import static com.wentch.redkale.convert.ObjectEncoder.TYPEZERO; +import com.wentch.redkale.util.Creator; +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * + * @author zhangjx + * @param + * @param + */ +@SuppressWarnings("unchecked") +public final class ObjectDecoder implements Decodeable { + + protected final Type type; + + protected final Class typeClass; + + protected Creator creator; + + protected DeMember[] members; + + protected Factory factory; + + protected ObjectDecoder(Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) type; + this.typeClass = (Class) pt.getRawType(); + } else { + this.typeClass = (Class) type; + } + this.members = new DeMember[0]; + } + + public void init(final Factory factory) { + this.factory = factory; + if (type == Object.class) return; + + Class clazz = null; + if (type instanceof ParameterizedType) { + final ParameterizedType pts = (ParameterizedType) type; + clazz = (Class) (pts).getRawType(); + } else if (!(type instanceof Class)) { + throw new ConvertException("[" + type + "] is no a class"); + } else { + clazz = (Class) type; + } + final Type[] virGenericTypes = this.typeClass.getTypeParameters(); + final Type[] realGenericTypes = (type instanceof ParameterizedType) ? ((ParameterizedType) type).getActualTypeArguments() : TYPEZERO; + this.creator = factory.loadCreator(clazz); + final Set list = new HashSet<>(); + try { + ConvertColumnEntry ref; + for (final Field field : clazz.getFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + ref = factory.findRef(field); + if (ref != null && ref.ignore()) continue; + Type t = ObjectEncoder.makeGenericType(field.getGenericType(), virGenericTypes, realGenericTypes); + list.add(new DeMember(ObjectEncoder.createAttribute(factory, clazz, field, null, null), factory.loadDecoder(t))); + } + final boolean reversible = factory.isReversible(); + for (final Method method : clazz.getMethods()) { + if (Modifier.isStatic(method.getModifiers())) continue; + if (Modifier.isAbstract(method.getModifiers())) continue; + if (method.isSynthetic()) continue; + if (method.getName().length() < 4) continue; + if (!method.getName().startsWith("set")) continue; + if (method.getParameterTypes().length != 1) continue; + if (method.getReturnType() != void.class) continue; + if (reversible) { + boolean is = method.getParameterTypes()[0] == boolean.class || method.getParameterTypes()[0] == Boolean.class; + try { + clazz.getMethod(method.getName().replaceFirst("set", is ? "is" : "get")); + } catch (Exception e) { + continue; + } + } + ref = factory.findRef(method); + if (ref != null && ref.ignore()) continue; + Type t = ObjectEncoder.makeGenericType(method.getGenericParameterTypes()[0], virGenericTypes, realGenericTypes); + list.add(new DeMember(ObjectEncoder.createAttribute(factory, clazz, null, null, method), factory.loadDecoder(t))); + } + this.members = list.toArray(new DeMember[list.size()]); + Arrays.sort(this.members); + } catch (Exception ex) { + throw new ConvertException(ex); + } + } + + /** + * 对象格式: [0x1][short字段个数][字段名][字段值]...[0x2] + * + * @param in + * @return + */ + @Override + public final T convertFrom(final R in) { + final String clazz = in.readClassName(); + if (clazz != null && !clazz.isEmpty()) { + try { + return (T) factory.loadDecoder(Class.forName(clazz)).convertFrom(in); + } catch (Exception ex) { + throw new ConvertException(ex); + } + } + if (in.readObjectB() == Reader.SIGN_NULL) return null; + final T result = this.creator.create(); + final AtomicInteger index = new AtomicInteger(); + while (in.hasNext()) { + DeMember member = in.readField(index, members); + in.skipBlank(); + if (member == null) { + in.skipValue(); //跳过该属性的值 + } else { + member.read(in, result); + } + index.incrementAndGet(); + } + in.readObjectE(); + return result; + } + + @Override + public final Type getType() { + return this.type; + } + + @Override + public String toString() { + return "ObjectDecoder{" + "type=" + type + ", members=" + Arrays.toString(members) + '}'; + } +} diff --git a/src/com/wentch/redkale/convert/ObjectEncoder.java b/src/com/wentch/redkale/convert/ObjectEncoder.java new file mode 100644 index 000000000..9fdcc24b1 --- /dev/null +++ b/src/com/wentch/redkale/convert/ObjectEncoder.java @@ -0,0 +1,213 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import com.wentch.redkale.util.Attribute; +import java.lang.reflect.*; +import java.util.*; + +/** + * + * @author zhangjx + * @param + * @param + */ +@SuppressWarnings("unchecked") +public final class ObjectEncoder implements Encodeable { + + static final Type[] TYPEZERO = new Type[0]; + + protected final Type type; + + protected final Class typeClass; + + protected EnMember[] members; + + protected Factory factory; + + protected ObjectEncoder(Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) type; + this.typeClass = (Class) pt.getRawType(); + } else { + this.typeClass = (Class) type; + } + this.members = new EnMember[0]; + } + + static Type makeGenericType(final Type type, final Type[] virGenericTypes, final Type[] realGenericTypes) { + if (type instanceof Class) { + return type; + } else if (type instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) type; + Type[] paramTypes = pt.getActualTypeArguments(); + final Type[] newTypes = new Type[paramTypes.length]; + int count = 0; + for (int i = 0; i < newTypes.length; i++) { + newTypes[i] = makeGenericType(paramTypes[i], virGenericTypes, realGenericTypes); + if (paramTypes[i] == newTypes[i]) count++; + } + if (count == paramTypes.length) return pt; + return new ParameterizedType() { + + @Override + public Type[] getActualTypeArguments() { + return newTypes; + } + + @Override + public Type getRawType() { + return pt.getRawType(); + } + + @Override + public Type getOwnerType() { + return pt.getOwnerType(); + } + + }; + } + if (realGenericTypes == null) return type; + if (type instanceof WildcardType) { + final WildcardType wt = (WildcardType) type; + for (Type f : wt.getUpperBounds()) { + for (int i = 0; i < virGenericTypes.length; i++) { + if (virGenericTypes[i] == f) return realGenericTypes.length == 0 ? Object.class : realGenericTypes[i]; + } + } + } else if (type instanceof TypeVariable) { + for (int i = 0; i < virGenericTypes.length; i++) { + if (virGenericTypes[i] == type) return i >= realGenericTypes.length ? Object.class : realGenericTypes[i]; + } + } + return type; + } + + private static String readGetSetFieldName(Method method) { + if (method == null) return null; + String fname = method.getName(); + if (!fname.startsWith("is") && !fname.startsWith("get") && !fname.startsWith("set")) return fname; + fname = fname.substring(fname.startsWith("is") ? 2 : 3); + if (fname.length() > 1 && !(fname.charAt(1) >= 'A' && fname.charAt(1) <= 'Z')) { + fname = Character.toLowerCase(fname.charAt(0)) + fname.substring(1); + } else if (fname.length() == 1) { + fname = "" + Character.toLowerCase(fname.charAt(0)); + } + return fname; + } + + static Attribute createAttribute(final Factory factory, Class clazz, final Field field, final Method getter, final Method setter) { + String fieldalias = null; + if (field != null) { // public field + ConvertColumnEntry ref = factory.findRef(field); + fieldalias = ref == null || ref.name().isEmpty() ? field.getName() : ref.name(); + } else if (getter != null) { + ConvertColumnEntry ref = factory.findRef(getter); + String mfieldname = readGetSetFieldName(getter); + if (ref == null) { + try { + ref = factory.findRef(clazz.getDeclaredField(mfieldname)); + } catch (Exception e) { + } + } + fieldalias = ref == null || ref.name().isEmpty() ? mfieldname : ref.name(); + } else { // setter != null + ConvertColumnEntry ref = factory.findRef(setter); + String mfieldname = readGetSetFieldName(setter); + if (ref == null) { + try { + ref = factory.findRef(clazz.getDeclaredField(mfieldname)); + } catch (Exception e) { + } + } + fieldalias = ref == null || ref.name().isEmpty() ? mfieldname : ref.name(); + } + return Attribute.create(clazz, fieldalias, field, getter, setter); + } + + public void init(final Factory factory) { + this.factory = factory; + if (type == Object.class) return; + //if (!(type instanceof Class)) throw new ConvertException("[" + type + "] is no a class"); + final Class clazz = this.typeClass; + final Set list = new HashSet<>(); + final Type[] virGenericTypes = this.typeClass.getTypeParameters(); + final Type[] realGenericTypes = (type instanceof ParameterizedType) ? ((ParameterizedType) type).getActualTypeArguments() : null; + if (realGenericTypes != null) { + // println(type + "," + Arrays.toString(virGenericTypes) + ", " + Arrays.toString(realGenericTypes)); + } + try { + ConvertColumnEntry ref; + for (final Field field : clazz.getFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + ref = factory.findRef(field); + if (ref != null && ref.ignore()) continue; + Type t = makeGenericType(field.getGenericType(), virGenericTypes, realGenericTypes); + list.add(new EnMember(createAttribute(factory, clazz, field, null, null), factory.loadEncoder(t))); + } + final boolean reversible = factory.isReversible(); + for (final Method method : clazz.getMethods()) { + if (Modifier.isStatic(method.getModifiers())) continue; + if (Modifier.isAbstract(method.getModifiers())) continue; + if (method.isSynthetic()) continue; + if (method.getName().length() < 3) continue; + if (method.getName().equals("getClass")) continue; + if (!method.getName().startsWith("is") && !method.getName().startsWith("get")) continue; + if (method.getParameterTypes().length != 0) continue; + if (method.getReturnType() == void.class) continue; + if (reversible) { + boolean is = method.getName().startsWith("is"); + try { + clazz.getMethod(method.getName().replaceFirst(is ? "is" : "get", "set"), method.getReturnType()); + } catch (Exception e) { + continue; + } + } + ref = factory.findRef(method); + if (ref != null && ref.ignore()) continue; + Type t = makeGenericType(method.getGenericReturnType(), virGenericTypes, realGenericTypes); + list.add(new EnMember(createAttribute(factory, clazz, null, method, null), factory.loadEncoder(t))); + } + this.members = list.toArray(new EnMember[list.size()]); + Arrays.sort(this.members); + + } catch (Exception ex) { + throw new ConvertException(ex); + } + } + + @Override + public final void convertTo(W out, T value) { + if (value == null) { + out.wirteClassName(null); + out.writeNull(); + return; + } + if (value != null && value.getClass() != this.typeClass) { + final Class clz = value.getClass(); + out.wirteClassName(clz); + factory.loadEncoder(clz).convertTo(out, value); + return; + } + out.writeObjectB(members.length, value); + boolean comma = false; + for (EnMember member : members) { + comma = member.write(out, comma, value); + } + out.writeObjectE(value); + } + + @Override + public final Type getType() { + return this.type; + } + + @Override + public String toString() { + return "ObjectEncoder{" + "type=" + type + ", members=" + Arrays.toString(members) + '}'; + } +} diff --git a/src/com/wentch/redkale/convert/Reader.java b/src/com/wentch/redkale/convert/Reader.java new file mode 100644 index 000000000..ba40ed756 --- /dev/null +++ b/src/com/wentch/redkale/convert/Reader.java @@ -0,0 +1,112 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * + * @author zhangjx + */ +public interface Reader { + + public static final short SIGN_NULL = -1; + + public static final short SIGN_NOLENGTH = -2; + + /** + * 是否还存在下个元素或字段 + * + * @return + */ + public boolean hasNext(); + + /** + * 跳过值(不包含值前面的字段) + */ + public void skipValue(); + + /** + * /跳过字段与值之间的多余内容, json就是跳过:符, map跳过: + */ + public void skipBlank(); + + /** + * 读取对象的开头 返回字段数 + * + * @return + */ + public int readObjectB(); + + /** + * 读取对象的尾端 + * + */ + public void readObjectE(); + + /** + * 读取数组的开头并返回数组的长度 + * + * @return + */ + public int readArrayB(); + + /** + * 读取数组的尾端 + * + */ + public void readArrayE(); + + /** + * 读取map的开头并返回map的size + * + * @return + */ + public int readMapB(); + + /** + * 读取数组的尾端 + * + */ + public void readMapE(); + + /** + * 根据字段读取字段对应的DeMember + * + * @param index + * @param members + * @return + */ + public DeMember readField(final AtomicInteger index, final DeMember[] members); + + public boolean readBoolean(); + + public byte readByte(); + + public char readChar(); + + public short readShort(); + + public int readInt(); + + public long readLong(); + + public float readFloat(); + + public double readDouble(); + + /** + * 读取无转义字符长度不超过255的字符串, 例如枚举值、字段名、类名字符串等 + * + * @return + */ + public String readSmallString(); + + public String readClassName(); + + public String readString(); + +} diff --git a/src/com/wentch/redkale/convert/SimpledCoder.java b/src/com/wentch/redkale/convert/SimpledCoder.java new file mode 100644 index 000000000..db0e42657 --- /dev/null +++ b/src/com/wentch/redkale/convert/SimpledCoder.java @@ -0,0 +1,42 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * + * @author zhangjx + * @param + * @param + * @param + */ +public abstract class SimpledCoder implements Decodeable, Encodeable { + + private Type type; + + @Override + public abstract void convertTo(final W out, final T value); + + @Override + public abstract T convertFrom(final R in); + + @Override + @SuppressWarnings("unchecked") + public Class getType() { + if (type == null) { + Type[] ts = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments(); + type = ts[ts.length - 1]; + } + return (Class) type; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} diff --git a/src/com/wentch/redkale/convert/Writer.java b/src/com/wentch/redkale/convert/Writer.java new file mode 100644 index 000000000..bf86892bb --- /dev/null +++ b/src/com/wentch/redkale/convert/Writer.java @@ -0,0 +1,113 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert; + +import com.wentch.redkale.util.Attribute; + +/** + * + * @author zhangjx + */ +public interface Writer { + + /** + * 输出null值 + */ + public void writeNull(); + + /** + * + * @param clazz + */ + public void wirteClassName(Class clazz); + + /** + * 输出一个对象前的操作 + * + * @param fieldCount 字段个数 + * + * @param obj + */ + public void writeObjectB(int fieldCount, Object obj); + + /** + * 输出一个对象后的操作 + * + * @param obj + */ + public void writeObjectE(Object obj); + + /** + * 输出一个数组前的操作 + * + * @param size 数组长度 + */ + public void writeArrayB(int size); + + /** + * 输出数组元素间的间隔符 + * + */ + public void writeArrayMark(); + + /** + * 输出一个数组后的操作 + * + */ + public void writeArrayE(); + + /** + * 输出一个Map前的操作 + * + * @param size map大小 + */ + public void writeMapB(int size); + + /** + * 输出一个Map中key与value间的间隔符 + * + */ + public void writeMapMark(); + + /** + * 输出一个Map后的操作 + * + */ + public void writeMapE(); + + /** + * 输出一个字段 + * + * @param comma 是否非第一个字段 + * @param attribute + */ + public void writeField(boolean comma, Attribute attribute); + + public void writeBoolean(boolean value); + + public void writeByte(byte value); + + public void writeChar(char value); + + public void writeShort(short value); + + public void writeInt(int value); + + public void writeLong(long value); + + public void writeFloat(float value); + + public void writeDouble(double value); + + /** + * 写入无转义字符长度不超过255的字符串, 例如枚举值、字段名、类名字符串等 * + * + * @param value + */ + public void writeSmallString(String value); + + public void writeString(String value); +} diff --git a/src/com/wentch/redkale/convert/bson/BsonConvert.java b/src/com/wentch/redkale/convert/bson/BsonConvert.java new file mode 100644 index 000000000..dd3b27539 --- /dev/null +++ b/src/com/wentch/redkale/convert/bson/BsonConvert.java @@ -0,0 +1,61 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.bson; + +import com.wentch.redkale.convert.Convert; +import com.wentch.redkale.convert.Factory; +import com.wentch.redkale.util.ObjectPool; +import java.lang.reflect.Type; + +/** + * + * @author zhangjx + */ +public final class BsonConvert extends Convert { + + private static final ObjectPool readerPool = new ObjectPool<>(Integer.getInteger("convert.bson.pool.size", 16), BsonReader.class); + + private static final ObjectPool writerPool = new ObjectPool<>(Integer.getInteger("convert.bson.pool.size", 16), BsonWriter.class); + + protected BsonConvert(Factory factory) { + super(factory); + } + + public T convertFrom(final Type type, final byte[] bytes) { + if (bytes == null) return null; + return convertFrom(type, bytes, 0, bytes.length); + } + + public T convertFrom(final Type type, final byte[] bytes, int start, int len) { + if (type == null) return null; + final BsonReader in = readerPool.poll(); + in.setBytes(bytes, start, len); + @SuppressWarnings("unchecked") + T rs = (T) factory.loadDecoder(type).convertFrom(in); + readerPool.offer(in); + return rs; + } + + public byte[] convertTo(final Type type, Object value) { + if (type == null) return null; + final BsonWriter out = writerPool.poll(); + factory.loadEncoder(type).convertTo(out, value); + byte[] result = out.toArray(); + writerPool.offer(out); + return result; + } + + public byte[] convertTo(Object value) { + if (value == null) { + final BsonWriter out = writerPool.poll(); + out.writeNull(); + byte[] result = out.toArray(); + writerPool.offer(out); + return result; + } + return convertTo(value.getClass(), value); + } +} diff --git a/src/com/wentch/redkale/convert/bson/BsonFactory.java b/src/com/wentch/redkale/convert/bson/BsonFactory.java new file mode 100644 index 000000000..7930b24fb --- /dev/null +++ b/src/com/wentch/redkale/convert/bson/BsonFactory.java @@ -0,0 +1,54 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.bson; + +import com.wentch.redkale.convert.ConvertType; +import com.wentch.redkale.convert.Factory; +import java.io.Serializable; + +/** + * + * @author zhangjx + */ +public final class BsonFactory extends Factory { + + private static final BsonFactory instance = new BsonFactory(null); + + static { + instance.register(Serializable.class, instance.loadDecoder(Object.class)); + instance.register(Serializable.class, instance.loadEncoder(Object.class)); + } + + private BsonFactory(BsonFactory parent) { + super(parent); + this.convert = new BsonConvert(this); + } + + public static BsonFactory root() { + return instance; + } + + @Override + public final BsonConvert getConvert() { + return (BsonConvert) convert; + } + + @Override + public BsonFactory createChild() { + return new BsonFactory(this); + } + + @Override + public ConvertType getConvertType() { + return ConvertType.BSON; + } + + @Override + public boolean isReversible() { + return true; + } + +} diff --git a/src/com/wentch/redkale/convert/bson/BsonReader.java b/src/com/wentch/redkale/convert/bson/BsonReader.java new file mode 100644 index 000000000..8095f6035 --- /dev/null +++ b/src/com/wentch/redkale/convert/bson/BsonReader.java @@ -0,0 +1,245 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.bson; + +import com.wentch.redkale.convert.ConvertException; +import com.wentch.redkale.convert.DeMember; +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.util.ObjectPool.Poolable; +import com.wentch.redkale.util.Utility; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * + * @author zhangjx + */ +public final class BsonReader implements Reader, Poolable { + + public static final short SIGN_OBJECTB = (short) 0xBB; + + public static final short SIGN_OBJECTE = (short) 0xEE; + + public static final byte SIGN_HASNEXT = 1; + + public static final byte SIGN_NONEXT = 0; + + public static final byte VERBOSE_NO = 1; + + public static final byte VERBOSE_YES = 2; + + private int position = -1; + + private byte[] content; + + public BsonReader() { + } + + public BsonReader(byte[] bytes) { + setBytes(bytes, 0, bytes.length); + } + + public BsonReader(byte[] bytes, int start, int len) { + setBytes(bytes, start, len); + } + + public final void setBytes(byte[] bytes) { + setBytes(bytes, 0, bytes.length); + } + + public final void setBytes(byte[] bytes, int start, int len) { + this.content = bytes; + this.position = start - 1; + //this.limit = start + len - 1; + } + + @Override + public void prepare() { + } + + @Override + public void release() { + this.position = -1; + //this.limit = -1; + this.content = null; + } + + public void close() { + this.release(); + } + + /** + * 跳过属性的值 + */ + @Override + public final void skipValue() { + + } + + /** + * 判断下一个非空白字节是否为{ + * + */ + @Override + public int readObjectB() { + short bt = readShort(); + if (bt == Reader.SIGN_NULL) return bt; + if (bt != SIGN_OBJECTB) { + throw new ConvertException("a bson object must begin with " + (SIGN_OBJECTB) + + " (position = " + position + ") but '" + this.content[this.position] + "'"); + } + return bt; + } + + @Override + public void readObjectE() { + if (readShort() != SIGN_OBJECTE) { + throw new ConvertException("a bson object must end with " + (SIGN_OBJECTE) + + " (position = " + position + ") but '" + this.content[this.position] + "'"); + } + } + + @Override + public int readMapB() { + return readArrayB(); + } + + @Override + public void readMapE() { + } + + /** + * 判断下一个非空白字节是否为[ + * + * @return + */ + @Override + public int readArrayB() { + return readShort(); + } + + @Override + public void readArrayE() { + } + + /** + * 判断下一个非空白字节是否: + */ + @Override + public void skipBlank() { + } + + /** + * 判断对象是否存在下一个属性或者数组是否存在下一个元素 + * + * @return + */ + @Override + public boolean hasNext() { + byte b = readByte(); + if (b == SIGN_HASNEXT) return true; + if (b != SIGN_NONEXT) throw new ConvertException("hasNext option must be (" + (SIGN_HASNEXT) + + " or " + (SIGN_NONEXT) + ") but '" + b + "' at position(" + this.position + ")"); + return false; + } + + @Override + public DeMember readField(final AtomicInteger index, final DeMember[] members) { + final String exceptedfield = readSmallString(); + final int len = members.length; + int v = index.get(); + if (v >= len) { + v = 0; + index.set(0); + } + for (int k = v; k < len; k++) { + if (exceptedfield.equals(members[k].getAttribute().field())) { + index.set(k); + return members[k]; + } + } + for (int k = 0; k < v; k++) { + if (exceptedfield.equals(members[k].getAttribute().field())) { + index.set(k); + return members[k]; + } + } + return null; + } + + //------------------------------------------------------------ + @Override + public boolean readBoolean() { + return content[++this.position] == 1; + } + + @Override + public byte readByte() { + return content[++this.position]; + } + + @Override + public char readChar() { + return (char) ((0xff00 & (content[++this.position] << 8)) | (0xff & content[++this.position])); + } + + @Override + public short readShort() { + return (short) ((0xff00 & (content[++this.position] << 8)) | (0xff & content[++this.position])); + } + + @Override + public int readInt() { + return ((content[++this.position] & 0xff) << 24) | ((content[++this.position] & 0xff) << 16) + | ((content[++this.position] & 0xff) << 8) | (content[++this.position] & 0xff); + } + + @Override + public long readLong() { + return ((((long) content[++this.position] & 0xff) << 56) + | (((long) content[++this.position] & 0xff) << 48) + | (((long) content[++this.position] & 0xff) << 40) + | (((long) content[++this.position] & 0xff) << 32) + | (((long) content[++this.position] & 0xff) << 24) + | (((long) content[++this.position] & 0xff) << 16) + | (((long) content[++this.position] & 0xff) << 8) + | (((long) content[++this.position] & 0xff))); + } + + @Override + public float readFloat() { + return Float.intBitsToFloat(readInt()); + } + + @Override + public double readDouble() { + return Double.longBitsToDouble(readLong()); + } + + @Override + public String readClassName() { + return readSmallString(); + } + + @Override + public String readSmallString() { + int len = 0xff & readByte(); + if (len == 0) return ""; + String value = new String(content, ++this.position, len); + this.position += len - 1; + return value; + } + + @Override + public String readString() { + int len = readInt(); + if (len == SIGN_NULL) return null; + if (len == 0) return ""; + String value = new String(Utility.decodeUTF8(content, ++this.position, len)); + this.position += len - 1; + return value; + } + +} diff --git a/src/com/wentch/redkale/convert/bson/BsonSimpledCoder.java b/src/com/wentch/redkale/convert/bson/BsonSimpledCoder.java new file mode 100644 index 000000000..840e1fca3 --- /dev/null +++ b/src/com/wentch/redkale/convert/bson/BsonSimpledCoder.java @@ -0,0 +1,17 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.bson; + +import com.wentch.redkale.convert.SimpledCoder; + +/** + * + * @author zhangjx + * @param + */ +public abstract class BsonSimpledCoder extends SimpledCoder { + +} diff --git a/src/com/wentch/redkale/convert/bson/BsonWriter.java b/src/com/wentch/redkale/convert/bson/BsonWriter.java new file mode 100644 index 000000000..d64382a0b --- /dev/null +++ b/src/com/wentch/redkale/convert/bson/BsonWriter.java @@ -0,0 +1,236 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.bson; + +import com.wentch.redkale.util.Utility; +import com.wentch.redkale.util.Attribute; +import com.wentch.redkale.convert.ConvertException; +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.Writer; +import com.wentch.redkale.util.ObjectPool.Poolable; + +/** + * + * @author zhangjx + */ +public final class BsonWriter implements Writer, Poolable { + + private static final int defaultSize = Integer.getInteger("convert.bson.writer.buffer.defsize", 1024); + + protected int count; + + private byte[] content; + + public byte[] toArray() { + if (count == content.length) return content; + byte[] newdata = new byte[count]; + System.arraycopy(content, 0, newdata, 0, count); + return newdata; + } + + public BsonWriter() { + this(defaultSize); + } + + public BsonWriter(int size) { + this.content = new byte[size > 32 ? size : 32]; + } + + //----------------------------------------------------------------------- + //----------------------------------------------------------------------- + /** + * 返回指定至少指定长度的缓冲区 + * + * @param len + * @return + */ + public byte[] expand(int len) { + int newcount = count + len; + if (newcount <= content.length) return content; + byte[] newdata = new byte[Math.max(content.length * 3 / 2, newcount)]; + System.arraycopy(content, 0, newdata, 0, count); + this.content = newdata; + return newdata; + } + + public void writeTo(final byte ch) { + expand(1); + content[count++] = ch; + } + + public void writeTo(final byte... chs) { + int len = chs.length; + expand(len); + System.arraycopy(chs, 0, content, count, len); + count += len; + } + + public void writeTo(final byte[] chs, final int start, final int end) { + int len = end - start; + expand(len); + System.arraycopy(chs, start, content, count, len); + count += len; + } + + @Override + public void prepare() { + } + + @Override + public void release() { + this.count = 0; + if (this.content.length > defaultSize) { + this.content = new byte[defaultSize]; + } + } + + //------------------------------------------------------------------------ + public final int count() { + return this.count; + } + + public final void count(int count) { + if (count >= 0) this.count = count; + } + + //----------------------------------------------------------------------- + @Override + public String toString() { + return new String(content, 0, count); + } + + @Override + public void writeBoolean(boolean value) { + writeTo(value ? (byte) 1 : (byte) 0); + } + + @Override + public void writeByte(byte value) { + writeTo(value); + } + + @Override + public void writeChar(final char value) { + writeTo((byte) ((value & 0xFF00) >> 8), (byte) (value & 0xFF)); + } + + @Override + public void writeShort(short value) { + writeTo((byte) (value >> 8), (byte) value); + } + + @Override + public void writeInt(int value) { + writeTo((byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value); + } + + @Override + public void writeLong(long value) { + writeTo((byte) (value >> 56), (byte) (value >> 48), (byte) (value >> 40), (byte) (value >> 32), + (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value); + } + + @Override + public void writeFloat(float value) { + writeInt(Float.floatToIntBits(value)); + } + + @Override + public void writeDouble(double value) { + writeLong(Double.doubleToLongBits(value)); + } + + @Override + public void wirteClassName(Class clazz) { + writeSmallString(clazz == null ? "" : clazz.getName()); + } + + @Override + public final void writeObjectB(int fieldCount, Object obj) { + writeSmallString(""); + writeShort(BsonReader.SIGN_OBJECTB); + } + + @Override + public final void writeObjectE(Object obj) { + writeByte(BsonReader.SIGN_NONEXT); + writeShort(BsonReader.SIGN_OBJECTE); + } + + @Override + public final void writeField(boolean comma, Attribute attribute) { + writeByte(BsonReader.SIGN_HASNEXT); + writeSmallString(attribute.field()); + } + + /** + * 对于类的字段名、枚举值这些长度一般不超过255且不会出现双字节字符的字符串采用writeSmallString处理, readSmallString用于读取 + * + * @param value + */ + @Override + public void writeSmallString(String value) { + if (value.isEmpty()) { + writeTo((byte) 0); + return; + } + char[] chars = Utility.charArray(value); + if (chars.length > 255) throw new ConvertException("'" + value + "' has very long length"); + byte[] bytes = new byte[chars.length + 1]; + bytes[0] = (byte) chars.length; + for (int i = 0; i < chars.length; i++) { + if (chars[i] > Byte.MAX_VALUE) throw new ConvertException("'" + value + "' has double-word"); + bytes[i + 1] = (byte) chars[i]; + } + writeTo(bytes); + } + + @Override + public void writeString(String value) { + if (value == null) { + writeInt(Reader.SIGN_NULL); + return; + } else if (value.isEmpty()) { + writeInt(0); + return; + } + byte[] bytes = Utility.encodeUTF8(value); + writeInt(bytes.length); + writeTo(bytes); + } + + @Override + public final void writeNull() { + writeShort(Reader.SIGN_NULL); + } + + @Override + public void writeArrayB(int size) { + writeShort((short) size); + } + + @Override + public void writeArrayMark() { + } + + @Override + public void writeArrayE() { + } + + @Override + public void writeMapB(int size) { + writeArrayB(size); + } + + @Override + public void writeMapMark() { + } + + @Override + public void writeMapE() { + } + +} diff --git a/src/com/wentch/redkale/convert/ext/BigIntegerSimpledCoder.java b/src/com/wentch/redkale/convert/ext/BigIntegerSimpledCoder.java new file mode 100644 index 000000000..735e85c3e --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/BigIntegerSimpledCoder.java @@ -0,0 +1,38 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; +import com.wentch.redkale.convert.Reader; +import java.math.BigInteger; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class BigIntegerSimpledCoder extends SimpledCoder { + + public static final BigIntegerSimpledCoder instance = new BigIntegerSimpledCoder(); + + @Override + public void convertTo(W out, BigInteger value) { + if (value == null) { + out.writeNull(); + return; + } + ByteArraySimpledCoder.instance.convertTo(out, value.toByteArray()); + } + + @Override + public BigInteger convertFrom(R in) { + byte[] bytes = ByteArraySimpledCoder.instance.convertFrom(in); + return bytes == null ? null : new BigInteger(bytes); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/BoolArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/BoolArraySimpledCoder.java new file mode 100644 index 000000000..696386202 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/BoolArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class BoolArraySimpledCoder extends SimpledCoder { + + public static final BoolArraySimpledCoder instance = new BoolArraySimpledCoder(); + + @Override + public void convertTo(W out, boolean[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (boolean v : values) { + if (flag) out.writeArrayMark(); + out.writeBoolean(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public boolean[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + boolean[] data = new boolean[8]; + while (in.hasNext()) { + if (size >= data.length) { + boolean[] newdata = new boolean[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readBoolean(); + } + in.readArrayE(); + boolean[] newdata = new boolean[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + boolean[] values = new boolean[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readBoolean(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/BoolSimpledCoder.java b/src/com/wentch/redkale/convert/ext/BoolSimpledCoder.java new file mode 100644 index 000000000..4b13eb923 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/BoolSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class BoolSimpledCoder extends SimpledCoder { + + public static final BoolSimpledCoder instance = new BoolSimpledCoder(); + + @Override + public void convertTo(W out, Boolean value) { + out.writeBoolean(value); + } + + @Override + public Boolean convertFrom(R in) { + return in.readBoolean(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/ByteArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/ByteArraySimpledCoder.java new file mode 100644 index 000000000..ffd09ccce --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/ByteArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class ByteArraySimpledCoder extends SimpledCoder { + + public static final ByteArraySimpledCoder instance = new ByteArraySimpledCoder(); + + @Override + public void convertTo(W out, byte[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (byte v : values) { + if (flag) out.writeArrayMark(); + out.writeByte(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public byte[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + byte[] data = new byte[8]; + while (in.hasNext()) { + if (size >= data.length) { + byte[] newdata = new byte[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readByte(); + } + in.readArrayE(); + byte[] newdata = new byte[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + byte[] values = new byte[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readByte(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/ByteSimpledCoder.java b/src/com/wentch/redkale/convert/ext/ByteSimpledCoder.java new file mode 100644 index 000000000..e76a6e84c --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/ByteSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class ByteSimpledCoder extends SimpledCoder { + + public static final ByteSimpledCoder instance = new ByteSimpledCoder(); + + @Override + public void convertTo(W out, Byte value) { + out.writeByte(value); + } + + @Override + public Byte convertFrom(R in) { + return in.readByte(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/CharArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/CharArraySimpledCoder.java new file mode 100644 index 000000000..9b5be5edb --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/CharArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class CharArraySimpledCoder extends SimpledCoder { + + public static final CharArraySimpledCoder instance = new CharArraySimpledCoder(); + + @Override + public void convertTo(W out, char[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (char v : values) { + if (flag) out.writeArrayMark(); + out.writeChar(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public char[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + char[] data = new char[8]; + while (in.hasNext()) { + if (size >= data.length) { + char[] newdata = new char[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readChar(); + } + in.readArrayE(); + char[] newdata = new char[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + char[] values = new char[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readChar(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/CharSimpledCoder.java b/src/com/wentch/redkale/convert/ext/CharSimpledCoder.java new file mode 100644 index 000000000..26b501043 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/CharSimpledCoder.java @@ -0,0 +1,33 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class CharSimpledCoder extends SimpledCoder { + + public static final CharSimpledCoder instance = new CharSimpledCoder(); + + @Override + public void convertTo(W out, Character value) { + out.writeChar(value); + } + + @Override + public Character convertFrom(R in) { + return in.readChar(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/DateSimpledCoder.java b/src/com/wentch/redkale/convert/ext/DateSimpledCoder.java new file mode 100644 index 000000000..4df76df2f --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/DateSimpledCoder.java @@ -0,0 +1,33 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; +import java.util.Date; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class DateSimpledCoder extends SimpledCoder { + + public static final DateSimpledCoder instance = new DateSimpledCoder(); + + @Override + public void convertTo(W out, Date value) { + out.writeLong(value.getTime()); + } + + @Override + public Date convertFrom(R in) { + return new Date(in.readLong()); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/DoubleArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/DoubleArraySimpledCoder.java new file mode 100644 index 000000000..04aa3a543 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/DoubleArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class DoubleArraySimpledCoder extends SimpledCoder { + + public static final DoubleArraySimpledCoder instance = new DoubleArraySimpledCoder(); + + @Override + public void convertTo(W out, double[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (double v : values) { + if (flag) out.writeArrayMark(); + out.writeDouble(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public double[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + double[] data = new double[8]; + while (in.hasNext()) { + if (size >= data.length) { + double[] newdata = new double[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readDouble(); + } + in.readArrayE(); + double[] newdata = new double[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + double[] values = new double[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readDouble(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/DoubleSimpledCoder.java b/src/com/wentch/redkale/convert/ext/DoubleSimpledCoder.java new file mode 100644 index 000000000..a56456562 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/DoubleSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class DoubleSimpledCoder extends SimpledCoder { + + public static final DoubleSimpledCoder instance = new DoubleSimpledCoder(); + + @Override + public void convertTo(W out, Double value) { + out.writeDouble(value); + } + + @Override + public Double convertFrom(R in) { + return in.readDouble(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/EnumSimpledCoder.java b/src/com/wentch/redkale/convert/ext/EnumSimpledCoder.java new file mode 100644 index 000000000..323b2b25e --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/EnumSimpledCoder.java @@ -0,0 +1,44 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + * @param + */ +public final class EnumSimpledCoder extends SimpledCoder { + + private final Class type; + + public EnumSimpledCoder(Class type) { + this.type = type; + } + + @Override + public void convertTo(final W out, final E value) { + if (value == null) { + out.writeNull(); + } else { + out.writeSmallString(value.toString()); + } + } + + @Override + @SuppressWarnings("unchecked") + public E convertFrom(final R in) { + String value = in.readSmallString(); + if (value == null) return null; + return (E) Enum.valueOf(type, value); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/FloatArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/FloatArraySimpledCoder.java new file mode 100644 index 000000000..ccba72bae --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/FloatArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class FloatArraySimpledCoder extends SimpledCoder { + + public static final FloatArraySimpledCoder instance = new FloatArraySimpledCoder(); + + @Override + public void convertTo(W out, float[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (float v : values) { + if (flag) out.writeArrayMark(); + out.writeFloat(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public float[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + float[] data = new float[8]; + while (in.hasNext()) { + if (size >= data.length) { + float[] newdata = new float[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readFloat(); + } + in.readArrayE(); + float[] newdata = new float[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + float[] values = new float[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readFloat(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/FloatSimpledCoder.java b/src/com/wentch/redkale/convert/ext/FloatSimpledCoder.java new file mode 100644 index 000000000..a2b9cbed5 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/FloatSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class FloatSimpledCoder extends SimpledCoder { + + public static final FloatSimpledCoder instance = new FloatSimpledCoder(); + + @Override + public void convertTo(W out, Float value) { + out.writeFloat(value); + } + + @Override + public Float convertFrom(R in) { + return in.readFloat(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/InetAddressSimpledCoder.java b/src/com/wentch/redkale/convert/ext/InetAddressSimpledCoder.java new file mode 100644 index 000000000..e7d5eb205 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/InetAddressSimpledCoder.java @@ -0,0 +1,70 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; +import com.wentch.redkale.convert.Reader; +import java.net.*; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class InetAddressSimpledCoder extends SimpledCoder { + + public static final InetAddressSimpledCoder instance = new InetAddressSimpledCoder(); + + @Override + public void convertTo(W out, InetAddress value) { + if (value == null) { + out.writeNull(); + return; + } + ByteArraySimpledCoder.instance.convertTo(out, value.getAddress()); + } + + @Override + public InetAddress convertFrom(R in) { + byte[] bytes = ByteArraySimpledCoder.instance.convertFrom(in); + if (bytes == null) return null; + try { + return InetAddress.getByAddress(bytes); + } catch (Exception ex) { + return null; + } + } + + public static class InetSocketAddressSimpledCoder extends SimpledCoder { + + public static final InetSocketAddressSimpledCoder instance = new InetSocketAddressSimpledCoder(); + + @Override + public void convertTo(W out, InetSocketAddress value) { + if (value == null) { + out.writeNull(); + return; + } + ByteArraySimpledCoder.instance.convertTo(out, value.getAddress().getAddress()); + out.writeInt(value.getPort()); + } + + @Override + public InetSocketAddress convertFrom(R in) { + byte[] bytes = ByteArraySimpledCoder.instance.convertFrom(in); + if (bytes == null) return null; + int port = in.readInt(); + try { + return new InetSocketAddress(InetAddress.getByAddress(bytes), port); + } catch (Exception ex) { + return null; + } + } + + } +} diff --git a/src/com/wentch/redkale/convert/ext/IntArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/IntArraySimpledCoder.java new file mode 100644 index 000000000..8a8c9248a --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/IntArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class IntArraySimpledCoder extends SimpledCoder { + + public static final IntArraySimpledCoder instance = new IntArraySimpledCoder(); + + @Override + public void convertTo(W out, int[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (int v : values) { + if (flag) out.writeArrayMark(); + out.writeInt(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public int[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + int[] data = new int[8]; + while (in.hasNext()) { + if (size >= data.length) { + int[] newdata = new int[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readInt(); + } + in.readArrayE(); + int[] newdata = new int[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + int[] values = new int[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readInt(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/IntSimpledCoder.java b/src/com/wentch/redkale/convert/ext/IntSimpledCoder.java new file mode 100644 index 000000000..e67c1d710 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/IntSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class IntSimpledCoder extends SimpledCoder { + + public static final IntSimpledCoder instance = new IntSimpledCoder(); + + @Override + public void convertTo(W out, Integer value) { + out.writeInt(value); + } + + @Override + public Integer convertFrom(R in) { + return in.readInt(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/LongArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/LongArraySimpledCoder.java new file mode 100644 index 000000000..2ea858b96 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/LongArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class LongArraySimpledCoder extends SimpledCoder { + + public static final LongArraySimpledCoder instance = new LongArraySimpledCoder(); + + @Override + public void convertTo(W out, long[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (long v : values) { + if (flag) out.writeArrayMark(); + out.writeLong(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public long[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + long[] data = new long[8]; + while (in.hasNext()) { + if (size >= data.length) { + long[] newdata = new long[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readInt(); + } + in.readArrayE(); + long[] newdata = new long[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + long[] values = new long[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readInt(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/LongSimpledCoder.java b/src/com/wentch/redkale/convert/ext/LongSimpledCoder.java new file mode 100644 index 000000000..df1d8dcfb --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/LongSimpledCoder.java @@ -0,0 +1,33 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class LongSimpledCoder extends SimpledCoder { + + public static final LongSimpledCoder instance = new LongSimpledCoder(); + + @Override + public void convertTo(W out, Long value) { + out.writeLong(value); + } + + @Override + public Long convertFrom(R in) { + return in.readLong(); + } + +} \ No newline at end of file diff --git a/src/com/wentch/redkale/convert/ext/NumberSimpledCoder.java b/src/com/wentch/redkale/convert/ext/NumberSimpledCoder.java new file mode 100644 index 000000000..aa9e369c7 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/NumberSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class NumberSimpledCoder extends SimpledCoder { + + public static final NumberSimpledCoder instance = new NumberSimpledCoder(); + + @Override + public void convertTo(W out, Number value) { + out.writeLong(value == null ? 0L : value.longValue()); + } + + @Override + public Number convertFrom(R in) { + return in.readLong(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/ShortArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/ShortArraySimpledCoder.java new file mode 100644 index 000000000..8c73f94e1 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/ShortArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class ShortArraySimpledCoder extends SimpledCoder { + + public static final ShortArraySimpledCoder instance = new ShortArraySimpledCoder(); + + @Override + public void convertTo(W out, short[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (short v : values) { + if (flag) out.writeArrayMark(); + out.writeShort(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public short[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + short[] data = new short[8]; + while (in.hasNext()) { + if (size >= data.length) { + short[] newdata = new short[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readShort(); + } + in.readArrayE(); + short[] newdata = new short[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + short[] values = new short[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readShort(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/ShortSimpledCoder.java b/src/com/wentch/redkale/convert/ext/ShortSimpledCoder.java new file mode 100644 index 000000000..50c91b1c9 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/ShortSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class ShortSimpledCoder extends SimpledCoder { + + public static final ShortSimpledCoder instance = new ShortSimpledCoder(); + + @Override + public void convertTo(W out, Short value) { + out.writeShort(value); + } + + @Override + public Short convertFrom(R in) { + return in.readShort(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/StringArraySimpledCoder.java b/src/com/wentch/redkale/convert/ext/StringArraySimpledCoder.java new file mode 100644 index 000000000..8cbc06cf7 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/StringArraySimpledCoder.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class StringArraySimpledCoder extends SimpledCoder { + + public static final StringArraySimpledCoder instance = new StringArraySimpledCoder(); + + @Override + public void convertTo(W out, String[] values) { + if (values == null) { + out.writeNull(); + return; + } + out.writeArrayB(values.length); + boolean flag = false; + for (String v : values) { + if (flag) out.writeArrayMark(); + out.writeString(v); + flag = true; + } + out.writeArrayE(); + } + + @Override + public String[] convertFrom(R in) { + int len = in.readArrayB(); + if (len == Reader.SIGN_NULL) { + return null; + } else if (len == Reader.SIGN_NOLENGTH) { + int size = 0; + String[] data = new String[8]; + while (in.hasNext()) { + if (size >= data.length) { + String[] newdata = new String[data.length + 4]; + System.arraycopy(data, 0, newdata, 0, size); + data = newdata; + } + data[size++] = in.readString(); + } + in.readArrayE(); + String[] newdata = new String[size]; + System.arraycopy(data, 0, newdata, 0, size); + return newdata; + } else { + String[] values = new String[len]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readString(); + } + in.readArrayE(); + return values; + } + } + +} diff --git a/src/com/wentch/redkale/convert/ext/StringSimpledCoder.java b/src/com/wentch/redkale/convert/ext/StringSimpledCoder.java new file mode 100644 index 000000000..aeafee793 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/StringSimpledCoder.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.convert.Writer; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class StringSimpledCoder extends SimpledCoder { + + public static final StringSimpledCoder instance = new StringSimpledCoder(); + + @Override + public void convertTo(W out, String value) { + out.writeString(value); + } + + @Override + public String convertFrom(R in) { + return in.readString(); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/TwoLongSimpledCoder.java b/src/com/wentch/redkale/convert/ext/TwoLongSimpledCoder.java new file mode 100644 index 000000000..50fdfc505 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/TwoLongSimpledCoder.java @@ -0,0 +1,40 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.Writer; +import com.wentch.redkale.convert.SimpledCoder; +import com.wentch.redkale.util.TwoLong; + +/** + * + * @author zhangjx + * @param + * @param + */ +public final class TwoLongSimpledCoder extends SimpledCoder { + + public static final TwoLongSimpledCoder instance = new TwoLongSimpledCoder(); + + @Override + public void convertTo(final W out, final TwoLong value) { + if (value == null) { + out.writeNull(); + } else { + out.writeSmallString(value.getFirst() + "_" + value.getSecond()); + } + } + + @Override + public TwoLong convertFrom(R in) { + String str = in.readString(); + if (str == null) return null; + int pos = str.indexOf('_'); + return new TwoLong(Long.parseLong(str.substring(0, pos)), Long.parseLong(str.substring(pos + 1))); + } + +} diff --git a/src/com/wentch/redkale/convert/ext/TypeSimpledCoder.java b/src/com/wentch/redkale/convert/ext/TypeSimpledCoder.java new file mode 100644 index 000000000..dc6397252 --- /dev/null +++ b/src/com/wentch/redkale/convert/ext/TypeSimpledCoder.java @@ -0,0 +1,42 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.ext; + +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.convert.Writer; +import com.wentch.redkale.convert.SimpledCoder; + +/** + * + * @author zhangjx + * @param + * @param + */ +public class TypeSimpledCoder extends SimpledCoder { + + public static final TypeSimpledCoder instance = new TypeSimpledCoder(); + + @Override + public void convertTo(final W out, final Class value) { + if (value == null) { + out.writeNull(); + } else { + out.writeSmallString(value.getName()); + } + } + + @Override + public Class convertFrom(R in) { + String str = in.readSmallString(); + if (str == null) return null; + try { + return Class.forName(str); + } catch (Exception e) { + return null; + } + } + +} diff --git a/src/com/wentch/redkale/convert/json/JsonConvert.java b/src/com/wentch/redkale/convert/json/JsonConvert.java new file mode 100644 index 000000000..3d69fe442 --- /dev/null +++ b/src/com/wentch/redkale/convert/json/JsonConvert.java @@ -0,0 +1,81 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.json; + +import com.wentch.redkale.util.Utility; +import com.wentch.redkale.util.ObjectPool; +import com.wentch.redkale.convert.Convert; +import java.lang.reflect.Type; + +/** + * + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public final class JsonConvert extends Convert { + + private static final ObjectPool readerPool = new ObjectPool<>(Integer.getInteger("convert.json.pool.size", 16), JsonReader.class); + + private static final ObjectPool writerPool = new ObjectPool<>(Integer.getInteger("convert.json.pool.size", 16), JsonWriter.class); + + protected JsonConvert(JsonFactory factory) { + super(factory); + } + + @Override + public JsonFactory getFactory() { + return (JsonFactory) factory; + } + + public T convertFrom(final Type type, final String text) { + if (text == null) return null; + return convertFrom(type, Utility.charArray(text)); + } + + public T convertFrom(final Type type, final char[] text) { + if (text == null) return null; + return convertFrom(type, text, 0, text.length); + } + + public T convertFrom(final Type type, final char[] text, int start, int len) { + if (text == null || type == null) return null; + final JsonReader in = readerPool.poll(); + in.setText(text, start, len); + T rs = (T) factory.loadDecoder(type).convertFrom(in); + readerPool.offer(in); + return rs; + } + + public String convertTo(final Type type, Object value) { + if (type == null) return null; + if (value == null) return "null"; + final JsonWriter out = writerPool.poll(); + factory.loadEncoder(type).convertTo(out, value); + String result = out.toString(); + writerPool.offer(out); + return result; + } + + public String convertTo(Object value) { + if (value == null) return "null"; + return convertTo(value.getClass(), value); + } + + public byte[] convertToUTF8Bytes(Object value) { + if (value == null) return new byte[]{110, 117, 108, 108}; + return convertToUTF8Bytes(value.getClass(), value); + } + + public byte[] convertToUTF8Bytes(final Type type, Object value) { + if (type == null) return null; + if (value == null) return new byte[]{110, 117, 108, 108}; + final JsonWriter out = writerPool.poll(); + factory.loadEncoder(type).convertTo(out, value); + byte[] result = out.toUTF8Bytes(); + writerPool.offer(out); + return result; + } +} diff --git a/src/com/wentch/redkale/convert/json/JsonFactory.java b/src/com/wentch/redkale/convert/json/JsonFactory.java new file mode 100644 index 000000000..91b703dca --- /dev/null +++ b/src/com/wentch/redkale/convert/json/JsonFactory.java @@ -0,0 +1,52 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.json; + +import com.wentch.redkale.convert.ConvertType; +import com.wentch.redkale.convert.Factory; +import java.io.Serializable; + +/** + * + * @author zhangjx + */ +public final class JsonFactory extends Factory { + + private static final JsonFactory instance = new JsonFactory(null); + + static { + instance.register(Serializable.class, instance.loadEncoder(Object.class)); + } + + private JsonFactory(JsonFactory parent) { + super(parent); + this.convert = new JsonConvert(this); + } + + public static JsonFactory root() { + return instance; + } + + @Override + public final JsonConvert getConvert() { + return (JsonConvert) convert; + } + + @Override + public JsonFactory createChild() { + return new JsonFactory(this); + } + + @Override + public ConvertType getConvertType() { + return ConvertType.JSON; + } + + @Override + public boolean isReversible() { + return false; + } +} diff --git a/src/com/wentch/redkale/convert/json/JsonReader.java b/src/com/wentch/redkale/convert/json/JsonReader.java new file mode 100644 index 000000000..2d6aa1c63 --- /dev/null +++ b/src/com/wentch/redkale/convert/json/JsonReader.java @@ -0,0 +1,556 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.json; + +import com.wentch.redkale.convert.ConvertException; +import com.wentch.redkale.convert.DeMember; +import com.wentch.redkale.convert.Reader; +import com.wentch.redkale.util.ObjectPool.Poolable; +import com.wentch.redkale.util.Utility; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * + * @author zhangjx + */ +public final class JsonReader implements Reader, Poolable { + + private int position = -1; + + private char[] text; + + private int limit; + + public JsonReader() { + } + + public JsonReader(String json) { + setText(Utility.charArray(json)); + } + + public JsonReader(char[] text) { + setText(text, 0, text.length); + } + + public JsonReader(char[] text, int start, int len) { + setText(text, start, len); + } + + public final void setText(String text) { + JsonReader.this.setText(Utility.charArray(text)); + } + + public final void setText(char[] text) { + setText(text, 0, text.length); + } + + public final void setText(char[] text, int start, int len) { + this.text = text; + this.position = start - 1; + this.limit = start + len - 1; + } + + @Override + public void prepare() { + } + + @Override + public void release() { + this.position = -1; + this.limit = -1; + this.text = null; + } + + public void close() { + this.release(); + } + + /** + * 找到指定的属性值 例如: {id : 1, data : { name : 'a', items : [1,2,3]}} seek('data.items') 直接跳转到 [1,2,3]; + * + * @param key + */ + public final void seek(String key) { + if (key == null || key.length() < 1) return; + final String[] keys = key.split("\\."); + nextGoodChar(); //读掉 { [ + for (String key1 : keys) { + while (this.hasNext()) { + String field = this.readSmallString(); + skipBlank(); + if (key1.equals(field)) break; + skipValue(); + } + } + + } + + /** + * 跳过属性的值 + */ + @Override + public final void skipValue() { + final char ch = nextGoodChar(); + if (ch == '"' || ch == '\'') { + backChar(ch); + readString(); + } else if (ch == '{') { + while (hasNext()) { + this.readSmallString(); //读掉field + this.skipBlank(); + this.skipValue(); + } + } else if (ch == '[') { + while (hasNext()) { + this.skipValue(); + } + } else { + char c; + for (;;) { + c = nextChar(); + if (c <= ' ') return; + if (c == '}' || c == ']' || c == ',' || c == ':') { + backChar(c); + return; + } + } + } + } + + /** + * 读取下一个字符, 不跳过空白字符 + * + * @return + */ + protected char nextChar() { + return this.text[++this.position]; + } + + /** + * 跳过空白字符, 返回一个非空白字符 + * + * @return + */ + protected char nextGoodChar() { + char c = nextChar(); + if (c > ' ') return c; + for (;;) { + c = nextChar(); + if (c > ' ') return c; + } + } + + /** + * 回退最后读取的字符 + * + * @param ch + */ + protected void backChar(char ch) { + this.position--; + } + + /** + * 判断下一个非空白字符是否为{ + * + */ + @Override + public int readObjectB() { + char ch = this.text[++this.position]; + if (ch == '{') return SIGN_NOLENGTH; + if (ch <= ' ') { + for (;;) { + ch = this.text[++this.position]; + if (ch > ' ') break; + } + if (ch == '{') return SIGN_NOLENGTH; + } + if (ch == 'n' && text[++position] == 'u' && text[++position] == 'l' && text[++position] == 'l') return SIGN_NULL; + if (ch == 'N' && text[++position] == 'U' && text[++position] == 'L' && text[++position] == 'L') return SIGN_NULL; + throw new ConvertException("a json object text must begin with '{' (position = " + position + ") but '" + ch + "'"); + } + + @Override + public void readObjectE() { + } + + /** + * 判断下一个非空白字符是否为{ + * + */ + @Override + public int readMapB() { + return readArrayB(); + } + + @Override + public void readMapE() { + } + + /** + * 判断下一个非空白字符是否为[ + * + * @return + */ + @Override + public int readArrayB() { + char ch = this.text[++this.position]; + if (ch == '[') return SIGN_NOLENGTH; + if (ch == '{') return SIGN_NOLENGTH; + if (ch <= ' ') { + for (;;) { + ch = this.text[++this.position]; + if (ch > ' ') break; + } + if (ch == '[') return SIGN_NOLENGTH; + if (ch == '{') return SIGN_NOLENGTH; + } + if (ch == 'n' && text[++position] == 'u' && text[++position] == 'l' && text[++position] == 'l') return SIGN_NULL; + if (ch == 'N' && text[++position] == 'U' && text[++position] == 'L' && text[++position] == 'L') return SIGN_NULL; + throw new ConvertException("a json array text must begin with '[' (position = " + position + ") but '" + ch + "'"); + } + + @Override + public void readArrayE() { + } + + /** + * 判断下一个非空白字符是否: + */ + @Override + public void skipBlank() { + char ch = this.text[++this.position]; + if (ch == ':') return; + if (ch <= ' ') { + for (;;) { + ch = this.text[++this.position]; + if (ch > ' ') break; + } + if (ch == ':') return; + } + throw new ConvertException("'" + new String(text) + "'expected a ':' but '" + ch + "'(position = " + position + ")"); + } + + /** + * 判断对象是否存在下一个属性或者数组是否存在下一个元素 + * + * @return + */ + @Override + public boolean hasNext() { + char ch = this.text[++this.position]; + if (ch == ',') return true; + if (ch == '}' || ch == ']') return false; + if (ch <= ' ') { + for (;;) { + ch = this.text[++this.position]; + if (ch > ' ') break; + } + if (ch == ',') return true; + if (ch == '}' || ch == ']') return false; + } + this.position--; + return true; + } + + @Override + public String readClassName() { + return null; + } + + @Override + public String readSmallString() { + final int eof = this.limit; + if (this.position == eof) return null; + final char[] text0 = this.text; + int currpos = this.position; + char ch = text0[++currpos]; + if (ch <= ' ') { + for (;;) { + ch = text0[++currpos]; + if (ch > ' ') break; + } + } + if (ch == '"' || ch == '\'') { + final char quote = ch; + final int start = currpos + 1; + for (;;) { + ch = text0[++currpos]; + if (ch == '\\') { + this.position = currpos - 1; + return readEscapeValue(quote, start); + } else if (ch == quote) { + break; + } + } + this.position = currpos; + char[] chs = new char[currpos - start]; + System.arraycopy(text0, start, chs, 0, chs.length); + return new String(chs); + } else { + int start = currpos; + for (;;) { + if (currpos == eof) break; + ch = text0[++currpos]; + if (ch == ',' || ch == ']' || ch == '}' || ch <= ' ' || ch == ':') break; + } + int len = currpos - start; + if (len < 1) { + this.position = currpos; + return String.valueOf(ch); + } + this.position = currpos - 1; + if (len == 4 && text0[start] == 'n' && text0[start + 1] == 'u' && text0[start + 2] == 'l' && text0[start + 3] == 'l') return null; + return new String(text0, start, len); + } + } + + /** + * 读取一个int + * + * @return + */ + @Override + public final int readInt() { + final char[] text0 = this.text; + final int eof = this.limit; + int currpos = this.position; + char firstchar = text0[++currpos]; + if (firstchar <= ' ') { + for (;;) { + firstchar = text0[++currpos]; + if (firstchar > ' ') break; + } + } + if (firstchar == '"' || firstchar == '\'') { + firstchar = text0[++currpos]; + if (firstchar == '"' || firstchar == '\'') { + this.position = currpos; + return 0; + } + } + int value = 0; + final boolean negative = firstchar == '-'; + if (!negative) { + if (firstchar < '0' || firstchar > '9') throw new NumberFormatException("illegal escape(" + firstchar + ") (position = " + currpos + ")"); + value = firstchar - '0'; + } + for (;;) { + if (currpos == eof) break; + char ch = text0[++currpos]; + if (ch >= '0' && ch <= '9') { + value = (value << 3) + (value << 1) + (ch - '0'); + } else if (ch == '"' || ch == '\'') { + } else if (ch == ',' || ch == '}' || ch == ']' || ch <= ' ' || ch == ':') { + break; + } else { + throw new NumberFormatException("illegal escape(" + ch + ") (position = " + currpos + ")"); + } + } + this.position = currpos - 1; + return negative ? -value : value; + } + + /** + * 读取一个long + * + * @return + */ + @Override + public final long readLong() { + final char[] text0 = this.text; + final int eof = this.limit; + int currpos = this.position; + char firstchar = text0[++currpos]; + if (firstchar <= ' ') { + for (;;) { + firstchar = text0[++currpos]; + if (firstchar > ' ') break; + } + } + if (firstchar == '"' || firstchar == '\'') { + firstchar = text0[++currpos]; + if (firstchar == '"' || firstchar == '\'') { + this.position = currpos; + return 0L; + } + } + long value = 0; + final boolean negative = firstchar == '-'; + if (!negative) { + if (firstchar < '0' || firstchar > '9') throw new NumberFormatException("illegal escape(" + firstchar + ") (position = " + currpos + ")"); + value = firstchar - '0'; + } + for (;;) { + if (currpos == eof) break; + char ch = text0[++currpos]; + if (ch >= '0' && ch <= '9') { + value = (value << 3) + (value << 1) + (ch - '0'); + } else if (ch == '"' || ch == '\'') { + } else if (ch == ',' || ch == '}' || ch == ']' || ch <= ' ' || ch == ':') { + break; + } else { + throw new NumberFormatException("illegal escape(" + ch + ") (position = " + currpos + ") but '" + ch + "'"); + } + } + this.position = currpos - 1; + return negative ? -value : value; + } + + @Override + public DeMember readField(final AtomicInteger index, final DeMember[] members) { + final String exceptedfield = this.readSmallString(); + final int len = members.length; + int v = index.get(); + if (v >= len) { + v = 0; + index.set(0); + } + for (int k = v; k < len; k++) { + if (exceptedfield.equals(members[k].getAttribute().field())) { + index.set(k); + return members[k]; + } + } + for (int k = 0; k < v; k++) { + if (exceptedfield.equals(members[k].getAttribute().field())) { + index.set(k); + return members[k]; + } + } + return null; + //if (result == null && len == 1 && text0[start] == '@') return REFER; + } +//------------------------------------------------------------ + + @Override + public boolean readBoolean() { + return "true".equalsIgnoreCase(this.readSmallString()); + } + + @Override + public byte readByte() { + return (byte) readInt(); + } + + @Override + public char readChar() { + return (char) readInt(); + } + + @Override + public short readShort() { + return (short) readInt(); + } + + @Override + public float readFloat() { + String chars = readSmallString(); + if (chars == null || chars.isEmpty()) return 0.f; + return Float.parseFloat(chars); + } + + @Override + public double readDouble() { + String chars = readSmallString(); + if (chars == null || chars.isEmpty()) return 0.0; + return Double.parseDouble(chars); + } + + /** + * 读取字符串, 必须是"或者'包围的字符串值 + * + * @return + */ + @Override + public String readString() { + final char[] text0 = this.text; + int currpos = this.position; + char expected = text0[++currpos]; + if (expected <= ' ') { + for (;;) { + expected = text0[++currpos]; + if (expected > ' ') break; + } + } + if (expected != '"' && expected != '\'') { + if (expected == 'n' && text0.length > currpos + 3) { + if (text0[++currpos] == 'u' && text0[++currpos] == 'l' && text0[++currpos] == 'l') { + this.position = currpos; + if (text0.length > currpos + 4) { + char ch = text0[currpos + 1]; + if (ch == ',' || ch <= ' ' || ch == '}' || ch == ']' || ch == ':') return null; + } else { + return null; + } + } + } + this.position = currpos; + throw new ConvertException("expected a ':' after a key but '" + text0[position] + "' (position = " + position + ")"); + } + final int start = ++currpos; + for (;;) { + char ch = text0[currpos]; + if (ch == expected) { + break; + } else if (ch == '\\') { + this.position = currpos - 1; + return readEscapeValue(expected, start); + } + currpos++; + } + this.position = currpos; + return new String(text0, start, currpos - start); + } + + private String readEscapeValue(final char expected, int start) { + StringBuilder array = new StringBuilder(); + final char[] text0 = this.text; + int pos = this.position; + array.append(text0, start, pos + 1 - start); + char c; + for (;;) { + c = text0[++pos]; + if (c == expected) { + this.position = pos; + return array.toString(); + } else if (c == '\\') { + c = text0[++pos]; + switch (c) { + case '"': + case '\'': + case '\\': + case '/': + array.append(c); + break; + case 'n': + array.append('\n'); + break; + case 'r': + array.append('\r'); + break; + case 'u': + array.append((char) Integer.parseInt(new String(new char[]{text0[++pos], text0[++pos], text0[++pos], text0[++pos]}), 16)); + break; + case 't': + array.append('\t'); + break; + case 'b': + array.append('\b'); + break; + case 'f': + array.append('\f'); + break; + default: + this.position = pos; + throw new ConvertException("illegal escape(" + c + ") (position = " + this.position + ")"); + } + } else { + array.append(c); + } + } + } + +} diff --git a/src/com/wentch/redkale/convert/json/JsonSimpledCoder.java b/src/com/wentch/redkale/convert/json/JsonSimpledCoder.java new file mode 100644 index 000000000..cd28528ff --- /dev/null +++ b/src/com/wentch/redkale/convert/json/JsonSimpledCoder.java @@ -0,0 +1,17 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.json; + +import com.wentch.redkale.convert.SimpledCoder; + +/** + * + * @author zhangjx + * @param + */ +public abstract class JsonSimpledCoder extends SimpledCoder { + +} diff --git a/src/com/wentch/redkale/convert/json/JsonWriter.java b/src/com/wentch/redkale/convert/json/JsonWriter.java new file mode 100644 index 000000000..0f101613b --- /dev/null +++ b/src/com/wentch/redkale/convert/json/JsonWriter.java @@ -0,0 +1,278 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.convert.json; + +import com.wentch.redkale.util.Attribute; +import com.wentch.redkale.util.Utility; +import com.wentch.redkale.convert.Writer; +import com.wentch.redkale.util.ObjectPool.Poolable; + +/** + * + * writeTo系列的方法输出的字符不能含特殊字符 + * + * @author zhangjx + */ +public final class JsonWriter implements Writer, Poolable { + + private static final char[] CHARS_TUREVALUE = "true".toCharArray(); + + private static final char[] CHARS_FALSEVALUE = "false".toCharArray(); + + private static final int defaultSize = Integer.getInteger("convert.json.writer.buffer.defsize", 1024); + + protected int count; + + private char[] content; + + public JsonWriter() { + this(defaultSize); + } + + public JsonWriter(int size) { + this.content = new char[size > 128 ? size : 128]; + } + + //----------------------------------------------------------------------- + //----------------------------------------------------------------------- + /** + * 返回指定至少指定长度的缓冲区 + * + * @param len + * @return + */ + public char[] expand(int len) { + int newcount = count + len; + if (newcount <= content.length) return content; + char[] newdata = new char[Math.max(content.length * 3 / 2, newcount)]; + System.arraycopy(content, 0, newdata, 0, count); + this.content = newdata; + return newdata; + } + + public void writeTo(final char ch) { + expand(1); + content[count++] = ch; + } + + public void writeTo(final char... chs) { + int len = chs.length; + expand(len); + System.arraycopy(chs, 0, content, count, len); + count += len; + } + + public void writeTo(final char[] chs, final int start, final int end) { + int len = end - start; + expand(len); + System.arraycopy(chs, start, content, count, len); + count += len; + } + + /** + * 注意: 该String值不能为null且不会进行转义, 只用于不含需要转义字符的字符串,例如enum、double、BigInteger转换的String + * + * @param quote + * @param value + */ + public void writeTo(final boolean quote, final String value) { + int len = value.length(); + expand(len + (quote ? 2 : 0)); + if (quote) content[count++] = '"'; + value.getChars(0, len, content, count); + count += len; + if (quote) content[count++] = '"'; + } + + @Override + public void prepare() { + } + + @Override + public void release() { + this.count = 0; + if (this.content.length > defaultSize) { + this.content = new char[defaultSize]; + } + } + + public char[] toArray() { + if (count == content.length) return content; + char[] newdata = new char[count]; + System.arraycopy(content, 0, newdata, 0, count); + return newdata; + } + + public byte[] toUTF8Bytes() { + return Utility.encodeUTF8(content, 0, count); + } + + //------------------------------------------------------------------------ + public final int count() { + return this.count; + } + + public final void count(int count) { + if (count >= 0) this.count = count; + } + +// @SuppressWarnings("unchecked") +// public final boolean writeRefer(final Object value) { +// if (stack == null) return false; +// int index = stack.indexOf(value); +// if (index > -1) { +// int deep = stack.size() - index; +// if (deep < 0) throw new ConvertException("the refer deep value(" + deep + ") is illegal"); +// writeTo('{', '"', '@', '"', ':', '"'); +// for (int i = 0; i < deep; i++) { +// writeTo('@'); +// } +// writeTo('"', '}'); +// return true; +// } +// return false; +// } + //----------------------------------------------------------------------- + @Override + public String toString() { + return new String(content, 0, count); + } + + @Override + public void writeBoolean(boolean value) { + writeTo(value ? CHARS_TUREVALUE : CHARS_FALSEVALUE); + } + + @Override + public void writeByte(byte value) { + writeInt(value); + } + + @Override + public void writeChar(char value) { + writeInt(value); + } + + @Override + public void writeShort(short value) { + writeInt(value); + } + + @Override + public void writeInt(int value) { + writeSmallString(String.valueOf(value)); + } + + @Override + public void writeLong(long value) { + writeSmallString(String.valueOf(value)); + } + + @Override + public void writeFloat(float value) { + writeSmallString(String.valueOf(value)); + } + + @Override + public void writeDouble(double value) { + writeSmallString(String.valueOf(value)); + } + + @Override + public void writeString(String value) { + if (value == null) { + writeNull(); + return; + } + expand(value.length() * 2 + 2); + content[count++] = '"'; + for (char ch : Utility.charArray(value)) { + switch (ch) { + case '\n': content[count++] = '\\'; + content[count++] = 'n'; + break; + case '\r': content[count++] = '\\'; + content[count++] = 'r'; + break; + case '\t': content[count++] = '\\'; + content[count++] = 't'; + break; + case '\\': content[count++] = '\\'; + content[count++] = ch; + break; + case '"': content[count++] = '\\'; + content[count++] = ch; + break; + default: content[count++] = ch; + break; + } + } + content[count++] = '"'; + } + + @Override + public void wirteClassName(Class clazz) { + } + + @Override + public final void writeObjectB(int fieldCount, Object obj) { + writeTo('{'); + } + + @Override + public final void writeObjectE(Object obj) { + writeTo('}'); + } + + @Override + public final void writeField(boolean comma, Attribute attribute) { + if (comma) writeTo(','); + writeTo('"'); + writeSmallString(attribute.field()); + writeTo('"'); + writeTo(':'); + } + + @Override + public final void writeNull() { + writeTo('n', 'u', 'l', 'l'); + } + + @Override + public void writeArrayB(int size) { + writeTo('['); + } + + @Override + public void writeArrayMark() { + writeTo(','); + } + + @Override + public void writeArrayE() { + writeTo(']'); + } + + @Override + public void writeMapB(int size) { + writeTo('{'); + } + + @Override + public void writeMapMark() { + writeTo(':'); + } + + @Override + public void writeMapE() { + writeTo('}'); + } + + @Override + public void writeSmallString(String value) { + writeTo(false, value); + } +} diff --git a/src/com/wentch/redkale/net/Async.java b/src/com/wentch/redkale/net/Async.java new file mode 100644 index 000000000..df584a41a --- /dev/null +++ b/src/com/wentch/redkale/net/Async.java @@ -0,0 +1,23 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 当Service是Remote模式时, 用该注解标注在方法上可使数据变成异步传输, 该注解只能标注在返回类型为void的public方法上 + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +public @interface Async { + +} diff --git a/src/com/wentch/redkale/net/AsyncConnection.java b/src/com/wentch/redkale/net/AsyncConnection.java new file mode 100644 index 000000000..d6897fa1c --- /dev/null +++ b/src/com/wentch/redkale/net/AsyncConnection.java @@ -0,0 +1,272 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.util.concurrent.*; + +/** + * + * @author zhangjx + */ +public abstract class AsyncConnection implements AsynchronousByteChannel, AutoCloseable { + + protected AsyncPooledConnection pooledConnection; + + public abstract SocketAddress getRemoteAddress(); + + public abstract int getReadTimeoutSecond(); + + public abstract int getWriteTimeoutSecond(); + + public abstract void setReadTimeoutSecond(int readTimeoutSecond); + + public abstract void setWriteTimeoutSecond(int writeTimeoutSecond); + + public abstract void dispose(); //同close, 只是去掉throws IOException + + public static AsyncConnection create(final String protocol, final SocketAddress address) throws IOException { + return create(protocol, address, 0, 0); + } + + /** + * 创建客户端连接 + * + * @param protocol + * @param address + * @param readTimeoutSecond0 + * @param writeTimeoutSecond0 + * @return + * @throws java.io.IOException + */ + public static AsyncConnection create(final String protocol, final SocketAddress address, + final int readTimeoutSecond0, final int writeTimeoutSecond0) throws IOException { + if ("TCP".equalsIgnoreCase(protocol)) { + AsynchronousSocketChannel channel = AsynchronousSocketChannel.open(); + try { + channel.connect(address).get(3, TimeUnit.SECONDS); + } catch (Exception e) { + throw new IOException("AsyncConnection connect " + address, e); + } + return create(channel, readTimeoutSecond0, writeTimeoutSecond0); + } else if ("UDP".equalsIgnoreCase(protocol)) { + AsyncDatagramChannel channel = AsyncDatagramChannel.open(null); + channel.connect(address); + return create(channel, address, true, readTimeoutSecond0, writeTimeoutSecond0); + } else { + throw new RuntimeException("AsyncConnection not support protocol " + protocol); + } + } + + public static AsyncConnection create(final AsyncDatagramChannel ch, SocketAddress addr, final boolean client0) { + return create(ch, addr, client0, 0, 0); + } + + public static AsyncConnection create(final AsyncDatagramChannel ch, SocketAddress addr, + final boolean client0, final int readTimeoutSecond0, final int writeTimeoutSecond0) { + return new AsyncConnection() { + private int readTimeoutSecond; + + private int writeTimeoutSecond; + + private final AsyncDatagramChannel channel; + + private final SocketAddress remoteAddress; + + private final boolean client; + + { + this.channel = ch; + this.client = client0; + this.readTimeoutSecond = readTimeoutSecond0; + this.writeTimeoutSecond = writeTimeoutSecond0; + this.remoteAddress = addr; + } + + @Override + public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + if (readTimeoutSecond > 0) { + channel.read(dst, readTimeoutSecond, TimeUnit.SECONDS, attachment, handler); + } else { + channel.read(dst, attachment, handler); + } + } + + @Override + public void write(ByteBuffer src, A attachment, CompletionHandler handler) { + channel.send(src, remoteAddress, attachment, handler); + } + + @Override + public void setReadTimeoutSecond(int readTimeoutSecond) { + this.readTimeoutSecond = readTimeoutSecond; + } + + @Override + public void setWriteTimeoutSecond(int writeTimeoutSecond) { + this.writeTimeoutSecond = writeTimeoutSecond; + } + + @Override + public int getReadTimeoutSecond() { + return this.readTimeoutSecond; + } + + @Override + public int getWriteTimeoutSecond() { + return this.writeTimeoutSecond; + } + + @Override + public final SocketAddress getRemoteAddress() { + return remoteAddress; + } + + @Override + public final Future read(ByteBuffer dst) { + return channel.read(dst); + } + + @Override + public final Future write(ByteBuffer src) { + return channel.write(src); + } + + @Override + public final void close() throws IOException { + if (client) { + if (pooledConnection == null) { + channel.close(); + } else { + pooledConnection.fireConnectionClosed(); + } + } + } + + @Override + public void dispose() { + try { + this.close(); + } catch (IOException io) { + } + } + + @Override + public final boolean isOpen() { + return channel.isOpen(); + } + + }; + } + + public static AsyncConnection create(final AsynchronousSocketChannel ch) { + return create(ch, 0, 0); + } + + public static AsyncConnection create(final AsynchronousSocketChannel ch, final int readTimeoutSecond0, final int writeTimeoutSecond0) { + return new AsyncConnection() { + private int readTimeoutSecond; + + private int writeTimeoutSecond; + + private final AsynchronousSocketChannel channel; + + private final SocketAddress remoteAddress; + + { + this.channel = ch; + this.readTimeoutSecond = readTimeoutSecond0; + this.writeTimeoutSecond = writeTimeoutSecond0; + SocketAddress addr = null; + try { + addr = ch.getRemoteAddress(); + } catch (Exception e) { + //do nothing + } + this.remoteAddress = addr; + } + + @Override + public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + if (readTimeoutSecond > 0) { + channel.read(dst, readTimeoutSecond, TimeUnit.SECONDS, attachment, handler); + } else { + channel.read(dst, attachment, handler); + } + } + + @Override + public void write(ByteBuffer src, A attachment, CompletionHandler handler) { + if (writeTimeoutSecond > 0) { + channel.write(src, writeTimeoutSecond, TimeUnit.SECONDS, attachment, handler); + } else { + channel.write(src, attachment, handler); + } + } + + @Override + public void setReadTimeoutSecond(int readTimeoutSecond) { + this.readTimeoutSecond = readTimeoutSecond; + } + + @Override + public void setWriteTimeoutSecond(int writeTimeoutSecond) { + this.writeTimeoutSecond = writeTimeoutSecond; + } + + @Override + public int getReadTimeoutSecond() { + return this.readTimeoutSecond; + } + + @Override + public int getWriteTimeoutSecond() { + return this.writeTimeoutSecond; + } + + @Override + public final SocketAddress getRemoteAddress() { + return remoteAddress; + } + + @Override + public final Future read(ByteBuffer dst) { + return channel.read(dst); + } + + @Override + public final Future write(ByteBuffer src) { + return channel.write(src); + } + + @Override + public final void close() throws IOException { + if (pooledConnection == null) { + channel.close(); + } else { + pooledConnection.fireConnectionClosed(); + } + } + + @Override + public final boolean isOpen() { + return channel.isOpen(); + } + + @Override + public void dispose() { + try { + this.close(); + } catch (IOException io) { + } + } + }; + } + +} diff --git a/src/com/wentch/redkale/net/AsyncDatagramChannel.java b/src/com/wentch/redkale/net/AsyncDatagramChannel.java new file mode 100644 index 000000000..477afd464 --- /dev/null +++ b/src/com/wentch/redkale/net/AsyncDatagramChannel.java @@ -0,0 +1,1298 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.io.*; +import java.lang.invoke.*; +import java.lang.ref.SoftReference; +import java.lang.reflect.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.security.*; +import java.util.*; +import java.util.concurrent.*; +import sun.misc.Cleaner; +import sun.security.action.GetIntegerAction; + +/** + * + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public final class AsyncDatagramChannel implements AsynchronousByteChannel, MulticastChannel { + + private final DatagramChannel dc; + + private final AsynchronousChannelGroupProxy group; + + private final Object attachKey; + + private boolean closed; + + // used to coordinate timed and blocking reads + private final Object readLock = new Object(); + + // channel blocking mode (requires readLock) + private boolean isBlocking = true; + + // number of blocking readers (requires readLock) + private int blockingReaderCount; + + // true if timed read attempted while blocking read in progress (requires readLock) + private boolean transitionToNonBlocking; + + // true if a blocking read is cancelled (requires readLock) + private boolean blockingReadKilledByCancel; + + // temporary Selectors used by timed reads (requires readLock) + private Selector firstReader; + + private Set otherReaders; + + private static final sun.misc.Unsafe UNSAFE; + + private static final long fdoffset; + + static { + sun.misc.Unsafe usafe = null; + long fd = 0L; + try { + Field safeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); + safeField.setAccessible(true); + usafe = (sun.misc.Unsafe) safeField.get(null); + fd = usafe.objectFieldOffset(DatagramChannel.open().getClass().getDeclaredField("fd")); + } catch (Exception e) { + throw new RuntimeException(e); + } + UNSAFE = usafe; + fdoffset = fd; + } + + private AsyncDatagramChannel(ProtocolFamily family, AsynchronousChannelGroup group0) + throws IOException { + this.dc = (family == null) ? DatagramChannel.open() : DatagramChannel.open(family); + if (group0 == null) group0 = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)); + this.group = new AsynchronousChannelGroupProxy(group0); + + // attach this channel to the group as foreign channel + boolean registered = false; + try { + attachKey = group.attachForeignChannel(this, (FileDescriptor) UNSAFE.getObject(dc, fdoffset)); + registered = true; + } finally { + if (!registered) + dc.close(); + } + } + + public static AsyncDatagramChannel open(AsynchronousChannelGroup group) throws IOException { + return open(null, group); + } + + public static AsyncDatagramChannel open(ProtocolFamily family, AsynchronousChannelGroup group) + throws IOException { + return new AsyncDatagramChannel(family, group); + } + + @Override + public final void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + read(dst, 0L, TimeUnit.MILLISECONDS, attachment, handler); + } + + // throws RuntimeException if blocking read has been cancelled + private void ensureBlockingReadNotKilled() { + assert Thread.holdsLock(readLock); + if (blockingReadKilledByCancel) throw new RuntimeException("Reading not allowed due to cancellation"); + } + + // invoke prior to non-timed read/receive + private void beginNoTimeoutRead() { + synchronized (readLock) { + ensureBlockingReadNotKilled(); + if (isBlocking) blockingReaderCount++; + } + } + + // invoke after non-timed read/receive has completed + private void endNoTimeoutRead() { + synchronized (readLock) { + if (isBlocking) { + if (--blockingReaderCount == 0 && transitionToNonBlocking) { + // notify any threads waiting to make channel non-blocking + readLock.notifyAll(); + } + } + } + } + + // invoke prior to timed read + // returns the timeout remaining + private long prepareForTimedRead(PendingFuture result, long timeout) throws IOException { + synchronized (readLock) { + ensureBlockingReadNotKilled(); + if (isBlocking) { + transitionToNonBlocking = true; + while (blockingReaderCount > 0 && timeout > 0L && !result.isCancelled()) { + long st = System.currentTimeMillis(); + try { + readLock.wait(timeout); + } catch (InterruptedException e) { + } + timeout -= System.currentTimeMillis() - st; + } + if (blockingReaderCount == 0) { + // re-check that blocked read wasn't cancelled + ensureBlockingReadNotKilled(); + // no blocking reads so change channel to non-blocking + dc.configureBlocking(false); + isBlocking = false; + } + } + return timeout; + } + } + + // returns a temporary Selector + private Selector getSelector() throws IOException { + Selector sel = getTemporarySelector(dc); + synchronized (readLock) { + if (firstReader == null) { + firstReader = sel; + } else { + if (otherReaders == null) otherReaders = new HashSet<>(); + otherReaders.add(sel); + } + } + return sel; + } + + // releases a temporary Selector + private void releaseSelector(Selector sel) throws IOException { + synchronized (readLock) { + if (firstReader == sel) { + firstReader = null; + } else { + otherReaders.remove(sel); + } + } + releaseTemporarySelector(sel); + } + + // wakeup all Selectors currently in use + private void wakeupSelectors() { + synchronized (readLock) { + if (firstReader != null) + firstReader.wakeup(); + if (otherReaders != null) { + for (Selector sel : otherReaders) { + sel.wakeup(); + } + } + } + } + + public AsynchronousChannelGroupProxy group() { + return group; + } + + @Override + public boolean isOpen() { + return dc.isOpen(); + } + + public void onCancel(PendingFuture task) { + synchronized (readLock) { + if (blockingReaderCount > 0) { + blockingReadKilledByCancel = true; + readLock.notifyAll(); + return; + } + } + wakeupSelectors(); + } + + @Override + public void close() throws IOException { + synchronized (dc) { + if (closed) return; + closed = true; + } + // detach from group and close underlying channel + group.detachForeignChannel(attachKey); + dc.close(); + + // wakeup any threads blocked in timed read/receives + wakeupSelectors(); + } + + public AsyncDatagramChannel connect(SocketAddress remote) + throws IOException { + dc.connect(remote); + return this; + } + + public AsyncDatagramChannel disconnect() throws IOException { + dc.disconnect(); + return this; + } + + private static class WrappedMembershipKey extends MembershipKey { + + private final MulticastChannel channel; + + private final MembershipKey key; + + WrappedMembershipKey(MulticastChannel channel, MembershipKey key) { + this.channel = channel; + this.key = key; + } + + @Override + public boolean isValid() { + return key.isValid(); + } + + @Override + public void drop() { + key.drop(); + } + + @Override + public MulticastChannel channel() { + return channel; + } + + @Override + public InetAddress group() { + return key.group(); + } + + @Override + public NetworkInterface networkInterface() { + return key.networkInterface(); + } + + @Override + public InetAddress sourceAddress() { + return key.sourceAddress(); + } + + @Override + public MembershipKey block(InetAddress toBlock) throws IOException { + key.block(toBlock); + return this; + } + + @Override + public MembershipKey unblock(InetAddress toUnblock) { + key.unblock(toUnblock); + return this; + } + + @Override + public String toString() { + return key.toString(); + } + } + + @Override + public MembershipKey join(InetAddress group, + NetworkInterface interf) + throws IOException { + MembershipKey key = ((MulticastChannel) dc).join(group, interf); + return new WrappedMembershipKey(this, key); + } + + @Override + public MembershipKey join(InetAddress group, NetworkInterface interf, InetAddress source) throws IOException { + MembershipKey key = ((MulticastChannel) dc).join(group, interf, source); + return new WrappedMembershipKey(this, key); + } + + private Future implSend(ByteBuffer src, SocketAddress target, A attachment, CompletionHandler handler) { + int n = 0; + Throwable exc = null; + try { + n = dc.send(src, target); + } catch (IOException ioe) { + exc = ioe; + } + if (handler == null) + return CompletedFuture.withResult(n, exc); + Invoker.invoke(this, handler, attachment, n, exc); + return null; + } + + public Future send(ByteBuffer src, SocketAddress target) { + return implSend(src, target, null, null); + } + + public void send(ByteBuffer src, SocketAddress target, A attachment, CompletionHandler handler) { + if (handler == null) throw new NullPointerException("'handler' is null"); + implSend(src, target, attachment, handler); + } + + private Future implWrite(ByteBuffer src, A attachment, CompletionHandler handler) { + int n = 0; + Throwable exc = null; + try { + n = dc.write(src); + } catch (IOException ioe) { + exc = ioe; + } + if (handler == null) return CompletedFuture.withResult(n, exc); + Invoker.invoke(this, handler, attachment, n, exc); + return null; + + } + + @Override + public Future write(ByteBuffer src) { + return implWrite(src, null, null); + } + + @Override + public void write(ByteBuffer src, A attachment, CompletionHandler handler) { + if (handler == null) throw new NullPointerException("'handler' is null"); + implWrite(src, attachment, handler); + } + + /** + * Receive into the given buffer with privileges enabled and restricted by the given AccessControlContext (can be null). + */ + private SocketAddress doRestrictedReceive(final ByteBuffer dst, + AccessControlContext acc) + throws IOException { + if (acc == null) { + return dc.receive(dst); + } else { + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction() { + public SocketAddress run() throws IOException { + return dc.receive(dst); + } + }, acc); + } catch (PrivilegedActionException pae) { + Exception cause = pae.getException(); + if (cause instanceof SecurityException) + throw (SecurityException) cause; + throw (IOException) cause; + } + } + } + + private Future implReceive(final ByteBuffer dst, final long timeout, + final TimeUnit unit, A attachment, final CompletionHandler handler) { + if (dst.isReadOnly()) throw new IllegalArgumentException("Read-only buffer"); + if (timeout < 0L) throw new IllegalArgumentException("Negative timeout"); + if (unit == null) throw new NullPointerException(); + + // complete immediately if channel closed + if (!isOpen()) { + Throwable exc = new ClosedChannelException(); + if (handler == null) return CompletedFuture.withFailure(exc); + Invoker.invoke(this, handler, attachment, null, exc); + return null; + } + + final AccessControlContext acc = (System.getSecurityManager() == null) ? null : AccessController.getContext(); + final PendingFuture result = new PendingFuture(this, handler, attachment); + Runnable task = new Runnable() { + public void run() { + try { + SocketAddress remote = null; + long to; + if (timeout == 0L) { + beginNoTimeoutRead(); + try { + remote = doRestrictedReceive(dst, acc); + } finally { + endNoTimeoutRead(); + } + to = 0L; + } else { + to = prepareForTimedRead(result, unit.toMillis(timeout)); + if (to <= 0L) throw new InterruptedByTimeoutException(); + remote = doRestrictedReceive(dst, acc); + } + if (remote == null) { + Selector sel = getSelector(); + SelectionKey sk = null; + try { + sk = dc.register(sel, SelectionKey.OP_READ); + for (;;) { + if (!dc.isOpen()) throw new AsynchronousCloseException(); + if (result.isCancelled()) break; + long st = System.currentTimeMillis(); + int ns = sel.select(to); + if (ns > 0) { + remote = doRestrictedReceive(dst, acc); + if (remote != null) break; + } + sel.selectedKeys().remove(sk); + if (timeout != 0L) { + to -= System.currentTimeMillis() - st; + if (to <= 0) throw new InterruptedByTimeoutException(); + } + } + } finally { + if (sk != null) + sk.cancel(); + releaseSelector(sel); + } + } + result.setResult(remote); + } catch (Exception x) { + if (x instanceof ClosedChannelException) + x = new AsynchronousCloseException(); + result.setFailure(x); + } + Invoker.invokeUnchecked(result); + } + }; + try { + group.executeOnPooledThread(task); + } catch (RejectedExecutionException ree) { + throw new ShutdownChannelGroupException(); + } + return result; + } + + public Future receive(ByteBuffer dst) { + return implReceive(dst, 0L, TimeUnit.MILLISECONDS, null, null); + } + + public void receive(ByteBuffer dst, A attachment, CompletionHandler handler) { + receive(dst, 0L, TimeUnit.MILLISECONDS, attachment, handler); + } + + public void receive(ByteBuffer dst, long timeout, TimeUnit unit, A attachment, CompletionHandler handler) { + if (handler == null) throw new NullPointerException("'handler' is null"); + implReceive(dst, timeout, unit, attachment, handler); + } + + private Future implRead(final ByteBuffer dst, long timeout, TimeUnit unit, A attachment, CompletionHandler handler) { + if (dst.isReadOnly()) throw new IllegalArgumentException("Read-only buffer"); + if (timeout < 0L) throw new IllegalArgumentException("Negative timeout"); + if (unit == null) throw new NullPointerException(); + + // complete immediately if channel closed + if (!isOpen()) { + Throwable exc = new ClosedChannelException(); + if (handler == null) return CompletedFuture.withFailure(exc); + Invoker.invoke(this, handler, attachment, null, exc); + return null; + } + + // another thread may disconnect before read is initiated + if (!dc.isConnected()) throw new NotYetConnectedException(); + + final PendingFuture result = new PendingFuture(this, handler, attachment); + Runnable task = new Runnable() { + public void run() { + try { + int n = 0; + long to; + if (timeout == 0L) { + beginNoTimeoutRead(); + try { + n = dc.read(dst); + } finally { + endNoTimeoutRead(); + } + to = 0L; + } else { + to = prepareForTimedRead(result, unit.toMillis(timeout)); + if (to <= 0L) throw new InterruptedByTimeoutException(); + n = dc.read(dst); + } + if (n == 0) { + Selector sel = getSelector(); + SelectionKey sk = null; + try { + sk = dc.register(sel, SelectionKey.OP_READ); + for (;;) { + if (!dc.isOpen()) throw new AsynchronousCloseException(); + if (result.isCancelled()) break; + long st = System.currentTimeMillis(); + int ns = sel.select(to); + if (ns > 0) { + if ((n = dc.read(dst)) != 0) break; + } + sel.selectedKeys().remove(sk); + if (timeout != 0L) { + to -= System.currentTimeMillis() - st; + if (to <= 0) throw new InterruptedByTimeoutException(); + } + } + } finally { + if (sk != null) + sk.cancel(); + releaseSelector(sel); + } + } + result.setResult(n); + } catch (Exception x) { + if (x instanceof ClosedChannelException) x = new AsynchronousCloseException(); + result.setFailure(x); + } + Invoker.invokeUnchecked(result); + } + }; + try { + group.executeOnPooledThread(task); + } catch (RejectedExecutionException ree) { + throw new ShutdownChannelGroupException(); + } + return result; + } + + @Override + public Future read(ByteBuffer dst) { + return implRead(dst, 0L, TimeUnit.MILLISECONDS, null, null); + } + + public void read(ByteBuffer dst, long timeout, TimeUnit unit, A attachment, CompletionHandler handler) { + if (handler == null) throw new NullPointerException("'handler' is null"); + implRead(dst, timeout, unit, attachment, handler); + } + + @Override + public AsyncDatagramChannel bind(SocketAddress local) throws IOException { + dc.bind(local); + return this; + } + + @Override + public SocketAddress getLocalAddress() throws IOException { + return dc.getLocalAddress(); + } + + @Override + public AsyncDatagramChannel setOption(SocketOption name, T value) throws IOException { + dc.setOption(name, value); + return this; + } + + @Override + public T getOption(SocketOption name) throws IOException { + return dc.getOption(name); + } + + @Override + public Set> supportedOptions() { + return dc.supportedOptions(); + } + + public SocketAddress getRemoteAddress() throws IOException { + return dc.getRemoteAddress(); + } + + private static class SelectorWrapper { + + private Selector sel; + + private SelectorWrapper(Selector sel) { + this.sel = sel; + Cleaner.create(this, new Closer(sel)); + } + + private static class Closer implements Runnable { + + private Selector sel; + + private Closer(Selector sel) { + this.sel = sel; + } + + public void run() { + try { + sel.close(); + } catch (Exception th) { + //throw new Error(th); + } + } + } + + public Selector get() { + return sel; + } + } + + private static ThreadLocal> localSelector + = new ThreadLocal>(); + + // Hold a reference to the selWrapper object to prevent it from + // being cleaned when the temporary selector wrapped is on lease. + private static ThreadLocal localSelectorWrapper + = new ThreadLocal(); + + static Selector getTemporarySelector(SelectableChannel sc) + throws IOException { + SoftReference ref = localSelector.get(); + SelectorWrapper selWrapper = null; + Selector sel = null; + if (ref == null + || ((selWrapper = ref.get()) == null) + || ((sel = selWrapper.get()) == null) + || (sel.provider() != sc.provider())) { + sel = sc.provider().openSelector(); + selWrapper = new SelectorWrapper(sel); + localSelector.set(new SoftReference(selWrapper)); + } + localSelectorWrapper.set(selWrapper); + return sel; + } + + static void releaseTemporarySelector(Selector sel) + throws IOException { + // Selector should be empty + sel.selectNow(); // Flush cancelled keys + assert sel.keys().isEmpty() : "Temporary selector not empty"; + localSelectorWrapper.set(null); + } + +} + +final class AsynchronousChannelGroupProxy extends AsynchronousChannelGroup { + + private final AsynchronousChannelGroup group; + + private final MethodHandle executeOnPooledThread; + + private final MethodHandle attachForeignChannel; + + private final MethodHandle detachForeignChannel; + + public AsynchronousChannelGroupProxy(AsynchronousChannelGroup group) { + super(group.provider()); + this.group = group; + MethodHandle method1 = null, method2 = null, method3 = null; + try { + Method m = findGroupMethod(group.getClass(), "executeOnPooledThread", Runnable.class); + m.setAccessible(true); + method1 = MethodHandles.lookup().unreflect(m); + + m = findGroupMethod(group.getClass(), "attachForeignChannel", Channel.class, FileDescriptor.class); + m.setAccessible(true); + method2 = MethodHandles.lookup().unreflect(m); + + m = findGroupMethod(group.getClass(), "detachForeignChannel", Object.class); + m.setAccessible(true); + method3 = MethodHandles.lookup().unreflect(m); + } catch (Exception e) { + e.printStackTrace(); + } + this.executeOnPooledThread = method1; + this.attachForeignChannel = method2; + this.detachForeignChannel = method3; + } + + private static Method findGroupMethod(Class clazz, String methodname, Class... params) throws Exception { + if (clazz == Object.class) return null; + try { + return clazz.getDeclaredMethod(methodname, params); + } catch (NoSuchMethodException e) { + return findGroupMethod(clazz.getSuperclass(), methodname, params); + } + } + + @Override + public boolean isShutdown() { + return group.isShutdown(); + } + + @Override + public boolean isTerminated() { + return group.isTerminated(); + } + + @Override + public void shutdown() { + group.shutdown(); + } + + @Override + public void shutdownNow() throws IOException { + group.shutdownNow(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return group.awaitTermination(timeout, unit); + } + + Object attachForeignChannel(Channel channel, FileDescriptor fdo) throws IOException { + try { + return attachForeignChannel.invoke(group, channel, fdo); + } catch (Throwable e) { + throw new IOException(e); + } + } + + void detachForeignChannel(Object key) { + try { + detachForeignChannel.invoke(group, key); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + final void executeOnPooledThread(Runnable task) { + try { + executeOnPooledThread.invoke(group, task); + } catch (Throwable e) { + e.printStackTrace(); + } + } + +} + +final class PendingFuture implements Future { + + private static final CancellationException CANCELLED = new CancellationException(); + + private final AsynchronousChannel channel; + + private final CompletionHandler handler; + + private final A attachment; + + // true if result (or exception) is available + private volatile boolean haveResult; + + private volatile V result; + + private volatile Throwable exc; + + // latch for waiting (created lazily if needed) + private CountDownLatch latch; + + // optional timer task that is cancelled when result becomes available + private Future timeoutTask; + + // optional context object + private volatile Object context; + + PendingFuture(AsynchronousChannel channel, CompletionHandler handler, A attachment, Object context) { + this.channel = channel; + this.handler = handler; + this.attachment = attachment; + this.context = context; + } + + PendingFuture(AsynchronousChannel channel, CompletionHandler handler, A attachment) { + this.channel = channel; + this.handler = handler; + this.attachment = attachment; + } + + PendingFuture(AsynchronousChannel channel) { + this(channel, null, null); + } + + PendingFuture(AsynchronousChannel channel, Object context) { + this(channel, null, null, context); + } + + AsynchronousChannel channel() { + return channel; + } + + CompletionHandler handler() { + return handler; + } + + A attachment() { + return attachment; + } + + void setContext(Object context) { + this.context = context; + } + + Object getContext() { + return context; + } + + void setTimeoutTask(Future task) { + synchronized (this) { + if (haveResult) { + task.cancel(false); + } else { + this.timeoutTask = task; + } + } + } + + // creates latch if required; return true if caller needs to wait + private boolean prepareForWait() { + synchronized (this) { + if (haveResult) { + return false; + } else { + if (latch == null) + latch = new CountDownLatch(1); + return true; + } + } + } + + /** + * Sets the result, or a no-op if the result or exception is already set. + */ + void setResult(V res) { + synchronized (this) { + if (haveResult) + return; + result = res; + haveResult = true; + if (timeoutTask != null) + timeoutTask.cancel(false); + if (latch != null) + latch.countDown(); + } + } + + /** + * Sets the result, or a no-op if the result or exception is already set. + */ + void setFailure(Throwable x) { + if (!(x instanceof IOException) && !(x instanceof SecurityException)) + x = new IOException(x); + synchronized (this) { + if (haveResult) + return; + exc = x; + haveResult = true; + if (timeoutTask != null) + timeoutTask.cancel(false); + if (latch != null) + latch.countDown(); + } + } + + /** + * Sets the result + */ + void setResult(V res, Throwable x) { + if (x == null) { + setResult(res); + } else { + setFailure(x); + } + } + + @Override + public V get() throws ExecutionException, InterruptedException { + if (!haveResult) { + boolean needToWait = prepareForWait(); + if (needToWait) + latch.await(); + } + if (exc != null) { + if (exc == CANCELLED) + throw new CancellationException(); + throw new ExecutionException(exc); + } + return result; + } + + @Override + public V get(long timeout, TimeUnit unit) + throws ExecutionException, InterruptedException, TimeoutException { + if (!haveResult) { + boolean needToWait = prepareForWait(); + if (needToWait) + if (!latch.await(timeout, unit)) throw new TimeoutException(); + } + if (exc != null) { + if (exc == CANCELLED) + throw new CancellationException(); + throw new ExecutionException(exc); + } + return result; + } + + Throwable exception() { + return (exc != CANCELLED) ? exc : null; + } + + V value() { + return result; + } + + @Override + public boolean isCancelled() { + return (exc == CANCELLED); + } + + @Override + public boolean isDone() { + return haveResult; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + synchronized (this) { + if (haveResult) + return false; // already completed + + // notify channel + if (channel() instanceof AsyncDatagramChannel) + ((AsyncDatagramChannel) channel()).onCancel(this); + + // set result and cancel timer + exc = CANCELLED; + haveResult = true; + if (timeoutTask != null) + timeoutTask.cancel(false); + } + + // close channel if forceful cancel + if (mayInterruptIfRunning) { + try { + channel().close(); + } catch (IOException ignore) { + } + } + + // release waiters + if (latch != null) + latch.countDown(); + return true; + } +} + +final class CompletedFuture implements Future { + + private final V result; + + private final Throwable exc; + + private CompletedFuture(V result, Throwable exc) { + this.result = result; + this.exc = exc; + } + + static CompletedFuture withResult(V result) { + return new CompletedFuture(result, null); + } + + static CompletedFuture withFailure(Throwable exc) { + // exception must be IOException or SecurityException + if (!(exc instanceof IOException) && !(exc instanceof SecurityException)) + exc = new IOException(exc); + return new CompletedFuture(null, exc); + } + + static CompletedFuture withResult(V result, Throwable exc) { + if (exc == null) { + return withResult(result); + } else { + return withFailure(exc); + } + } + + @Override + public V get() throws ExecutionException { + if (exc != null) + throw new ExecutionException(exc); + return result; + } + + @Override + public V get(long timeout, TimeUnit unit) throws ExecutionException { + if (unit == null) + throw new NullPointerException(); + if (exc != null) + throw new ExecutionException(exc); + return result; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } +} + +class Invoker { + + private Invoker() { + } + + // maximum number of completion handlers that may be invoked on the current + // thread before it re-directs invocations to the thread pool. This helps + // avoid stack overflow and lessens the risk of starvation. + private static final int maxHandlerInvokeCount = AccessController.doPrivileged( + new GetIntegerAction("sun.nio.ch.maxCompletionHandlersOnStack", 16)); + + // Per-thread object with reference to channel group and a counter for + // the number of completion handlers invoked. This should be reset to 0 + // when all completion handlers have completed. + static class GroupAndInvokeCount { + + private final AsynchronousChannelGroup group; + + private int handlerInvokeCount; + + GroupAndInvokeCount(AsynchronousChannelGroup group) { + this.group = group; + } + + AsynchronousChannelGroup group() { + return group; + } + + int invokeCount() { + return handlerInvokeCount; + } + + void setInvokeCount(int value) { + handlerInvokeCount = value; + } + + void resetInvokeCount() { + handlerInvokeCount = 0; + } + + void incrementInvokeCount() { + handlerInvokeCount++; + } + } + + private static final ThreadLocal myGroupAndInvokeCount + = new ThreadLocal() { + @Override + protected GroupAndInvokeCount initialValue() { + return null; + } + }; + + /** + * Binds this thread to the given group + */ + static void bindToGroup(AsynchronousChannelGroup group) { + myGroupAndInvokeCount.set(new GroupAndInvokeCount(group)); + } + + /** + * Returns the GroupAndInvokeCount object for this thread. + */ + static GroupAndInvokeCount getGroupAndInvokeCount() { + return myGroupAndInvokeCount.get(); + } + + /** + * Returns true if the current thread is in a channel group's thread pool + */ + static boolean isBoundToAnyGroup() { + return myGroupAndInvokeCount.get() != null; + } + + /* + * Returns true if the current thread is in the given channel's thread pool + * and we haven't exceeded the maximum number of handler frames on the stack. + */ + static boolean mayInvokeDirect(GroupAndInvokeCount myGroupAndInvokeCount, + AsynchronousChannelGroup group) { + if ((myGroupAndInvokeCount != null) + && (myGroupAndInvokeCount.group() == group) + && (myGroupAndInvokeCount.invokeCount() < maxHandlerInvokeCount)) { + return true; + } + return false; + } + + /** + * Invoke handler without checking the thread identity or number of handlers on the thread stack. + */ + static void invokeUnchecked(CompletionHandler handler, + A attachment, + V value, + Throwable exc) { + if (exc == null) { + handler.completed(value, attachment); + } else { + handler.failed(exc, attachment); + } + + // clear interrupt + Thread.interrupted(); + } + + /** + * Invoke handler assuming thread identity already checked + */ + static void invokeDirect(GroupAndInvokeCount myGroupAndInvokeCount, + CompletionHandler handler, + A attachment, + V result, + Throwable exc) { + myGroupAndInvokeCount.incrementInvokeCount(); + Invoker.invokeUnchecked(handler, attachment, result, exc); + } + + /** + * Invokes the handler. If the current thread is in the channel group's thread pool then the handler is invoked directly, otherwise it is invoked indirectly. + */ + static void invoke(AsynchronousChannel channel, + CompletionHandler handler, + A attachment, + V result, + Throwable exc) { + boolean invokeDirect = false; + boolean identityOkay = false; + GroupAndInvokeCount thisGroupAndInvokeCount = myGroupAndInvokeCount.get(); + if (thisGroupAndInvokeCount != null) { + if ((thisGroupAndInvokeCount.group() == ((AsyncDatagramChannel) channel).group())) + identityOkay = true; + if (identityOkay + && (thisGroupAndInvokeCount.invokeCount() < maxHandlerInvokeCount)) { + // group match + invokeDirect = true; + } + } + if (invokeDirect) { + invokeDirect(thisGroupAndInvokeCount, handler, attachment, result, exc); + } else { + try { + invokeIndirectly(channel, handler, attachment, result, exc); + } catch (RejectedExecutionException ree) { + // channel group shutdown; fallback to invoking directly + // if the current thread has the right identity. + if (identityOkay) { + invokeDirect(thisGroupAndInvokeCount, + handler, attachment, result, exc); + } else { + throw new ShutdownChannelGroupException(); + } + } + } + } + + /** + * Invokes the handler indirectly via the channel group's thread pool. + */ + static void invokeIndirectly(AsynchronousChannel channel, + final CompletionHandler handler, + final A attachment, + final V result, + final Throwable exc) { + try { + ((AsyncDatagramChannel) channel).group().executeOnPooledThread(new Runnable() { + public void run() { + GroupAndInvokeCount thisGroupAndInvokeCount + = myGroupAndInvokeCount.get(); + if (thisGroupAndInvokeCount != null) + thisGroupAndInvokeCount.setInvokeCount(1); + invokeUnchecked(handler, attachment, result, exc); + } + }); + } catch (RejectedExecutionException ree) { + throw new ShutdownChannelGroupException(); + } + } + + /** + * Invokes the handler "indirectly" in the given Executor + */ + static void invokeIndirectly(final CompletionHandler handler, + final A attachment, + final V value, + final Throwable exc, + Executor executor) { + try { + executor.execute(new Runnable() { + public void run() { + invokeUnchecked(handler, attachment, value, exc); + } + }); + } catch (RejectedExecutionException ree) { + throw new ShutdownChannelGroupException(); + } + } + + /** + * Invokes the given task on the thread pool associated with the given channel. If the current thread is in the thread pool then the task is invoked directly. + */ + static void invokeOnThreadInThreadPool(AsyncDatagramChannel channel, + Runnable task) { + boolean invokeDirect; + GroupAndInvokeCount thisGroupAndInvokeCount = myGroupAndInvokeCount.get(); + AsynchronousChannelGroupProxy targetGroup = channel.group(); + if (thisGroupAndInvokeCount == null) { + invokeDirect = false; + } else { + invokeDirect = (thisGroupAndInvokeCount.group == targetGroup); + } + try { + if (invokeDirect) { + task.run(); + } else { + targetGroup.executeOnPooledThread(task); + } + } catch (RejectedExecutionException ree) { + throw new ShutdownChannelGroupException(); + } + } + + /** + * Invoke handler with completed result. This method does not check the thread identity or the number of handlers on the thread stack. + */ + static void invokeUnchecked(PendingFuture future) { + assert future.isDone(); + CompletionHandler handler = future.handler(); + if (handler != null) { + invokeUnchecked(handler, + future.attachment(), + future.value(), + future.exception()); + } + } + + /** + * Invoke handler with completed result. If the current thread is in the channel group's thread pool then the handler is invoked directly, otherwise it is invoked indirectly. + */ + static void invoke(PendingFuture future) { + assert future.isDone(); + CompletionHandler handler = future.handler(); + if (handler != null) { + invoke(future.channel(), + handler, + future.attachment(), + future.value(), + future.exception()); + } + } + + /** + * Invoke handler with completed result. The handler is invoked indirectly, via the channel group's thread pool. + */ + static void invokeIndirectly(PendingFuture future) { + assert future.isDone(); + CompletionHandler handler = future.handler(); + if (handler != null) { + invokeIndirectly(future.channel(), + handler, + future.attachment(), + future.value(), + future.exception()); + } + } +} diff --git a/src/com/wentch/redkale/net/AsyncPooledConnection.java b/src/com/wentch/redkale/net/AsyncPooledConnection.java new file mode 100644 index 000000000..3daeedb30 --- /dev/null +++ b/src/com/wentch/redkale/net/AsyncPooledConnection.java @@ -0,0 +1,50 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.io.IOException; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicLong; + +/** + * + * @author zhangjx + */ +public class AsyncPooledConnection implements AutoCloseable { + + private final Queue queue; + + private final AtomicLong usingCounter; + + private final AsyncConnection conn; + + public AsyncPooledConnection(Queue queue, AtomicLong usingCounter, AsyncConnection conn) { + this.conn = conn; + this.usingCounter = usingCounter; + this.queue = queue; + } + + public AsyncConnection getAsyncConnection() { + return conn; + } + + public void fireConnectionClosed() { + this.queue.add(this); + } + + @Override + public void close() throws IOException { + usingCounter.decrementAndGet(); + conn.close(); + } + + public void dispose() { + try { + this.close(); + } catch (IOException io) { + } + } +} diff --git a/src/com/wentch/redkale/net/AsyncWriteHandler.java b/src/com/wentch/redkale/net/AsyncWriteHandler.java new file mode 100644 index 000000000..6d0611b28 --- /dev/null +++ b/src/com/wentch/redkale/net/AsyncWriteHandler.java @@ -0,0 +1,74 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.util.logging.Level; + +/** + * + * @author zhangjx + * @param + */ +public final class AsyncWriteHandler implements CompletionHandler { + + protected final ByteBuffer buffer; + + protected final AsynchronousByteChannel channel; + + protected final Context context; + + protected final ByteBuffer buffer2; + + protected final A attachment; + + protected final CompletionHandler hander; + + public AsyncWriteHandler(Context context, ByteBuffer buffer, AsynchronousByteChannel channel) { + this(context, buffer, channel, null, null, null); + } + + public AsyncWriteHandler(Context context, ByteBuffer buffer, AsynchronousByteChannel channel, ByteBuffer buffer2, A attachment, CompletionHandler hander) { + this.buffer = buffer; + this.channel = channel; + this.context = context; + this.buffer2 = buffer2; + this.attachment = attachment; + this.hander = hander; + } + + @Override + public void completed(Integer result, A attachment) { + if (buffer.hasRemaining()) { + this.channel.write(buffer, attachment, this); + return; + } + if (context != null) context.offerBuffer(buffer); + if (hander != null) { + if (buffer2 == null) { + hander.completed(result, attachment); + } else { + this.channel.write(buffer2, attachment, hander); + } + } + } + + @Override + public void failed(Throwable exc, A attachment) { + if (context != null) context.offerBuffer(buffer); + if (hander == null) { + try { + this.channel.close(); + } catch (Exception e) { + context.logger.log(Level.FINE, "AsyncWriteHandler close channel erroneous", e); + } + } else { + hander.failed(exc, attachment); + } + } + +} diff --git a/src/com/wentch/redkale/net/BufferPool.java b/src/com/wentch/redkale/net/BufferPool.java new file mode 100644 index 000000000..bfb477f4a --- /dev/null +++ b/src/com/wentch/redkale/net/BufferPool.java @@ -0,0 +1,61 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.nio.ByteBuffer; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.*; + +/** + * + * @author zhangjx + */ +public final class BufferPool { + + private final int capacity; + + private final ArrayBlockingQueue queue; + + private final AtomicLong creatCounter; + + private final AtomicLong cycleCounter; + + public BufferPool(AtomicLong creatCounter, AtomicLong cycleCounter, int capacity) { + this(creatCounter, cycleCounter, capacity, 0); + } + + public BufferPool(AtomicLong creatCounter, AtomicLong cycleCounter, int capacity, int max) { + this.capacity = capacity; + this.queue = new ArrayBlockingQueue<>(Math.max(32, max)); + this.creatCounter = creatCounter; + this.cycleCounter = cycleCounter; + } + + public ByteBuffer poll() { + ByteBuffer result = queue.poll(); + if (result == null) { + creatCounter.incrementAndGet(); + result = ByteBuffer.allocateDirect(capacity); + } + return result; + } + + public void offer(final ByteBuffer e) { + if (e != null && !e.isReadOnly() && e.capacity() == this.capacity) { + cycleCounter.incrementAndGet(); + e.clear(); + queue.offer(e); + } + } + + public long getCreatCount() { + return creatCounter.longValue(); + } + + public long getCycleCount() { + return cycleCounter.longValue(); + } +} diff --git a/src/com/wentch/redkale/net/ChunkBuffer.java b/src/com/wentch/redkale/net/ChunkBuffer.java new file mode 100644 index 000000000..2a571df05 --- /dev/null +++ b/src/com/wentch/redkale/net/ChunkBuffer.java @@ -0,0 +1,228 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.nio.*; + +/** + * + * @author zhangjx + */ +public final class ChunkBuffer { + + final ByteBuffer buffer; + + private final BufferPool pool; + + ChunkBuffer(BufferPool pool, ByteBuffer buffer) { + this.pool = pool; + this.buffer = buffer; + } + + public void release() { + //pool.offer(this); + } + + public int limit() { + return buffer.limit(); + } + + public void limit(int limit) { + buffer.limit(limit); + } + + public int position() { + return buffer.position(); + } + + public void position(int position) { + buffer.position(position); + } + + public void clear() { + buffer.clear(); + } + + public ChunkBuffer flip() { + buffer.flip(); + return this; + } + + public int remaining() { + return buffer.remaining(); + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + public boolean isReadOnly() { + return buffer.isReadOnly(); + } + + public boolean hasArray() { + return buffer.hasArray(); + } + + public byte[] array() { + return buffer.array(); + } + + public int arrayOffset() { + return buffer.arrayOffset(); + } + + public boolean isDirect() { + return buffer.isDirect(); + } + + public ChunkBuffer slice() { + buffer.slice(); + return this; + } + + public ChunkBuffer duplicate() { + buffer.duplicate(); + return this; + } + + public ChunkBuffer asReadOnlyBuffer() { + buffer.asReadOnlyBuffer(); + return this; + } + + public byte get() { + return buffer.get(); + } + + public ChunkBuffer put(byte b) { + buffer.put(b); + return this; + } + + public byte get(int index) { + return buffer.get(index); + } + + public ChunkBuffer put(int index, byte b) { + buffer.put(index, b); + return this; + } + + public ChunkBuffer compact() { + buffer.compact(); + return this; + } + + public char getChar() { + return buffer.getChar(); + } + + public ChunkBuffer putChar(char value) { + buffer.putChar(value); + return this; + } + + public char getChar(int index) { + return buffer.getChar(index); + } + + public ChunkBuffer putChar(int index, char value) { + buffer.putChar(index, value); + return this; + } + + public short getShort() { + return buffer.getShort(); + } + + public ChunkBuffer putShort(short value) { + buffer.putShort(value); + return this; + } + + public short getShort(int index) { + return buffer.getShort(index); + } + + public ChunkBuffer putShort(int index, short value) { + buffer.putShort(index, value); + return this; + } + + public int getInt() { + return buffer.getInt(); + } + + public ChunkBuffer putInt(int value) { + buffer.putInt(value); + return this; + } + + public int getInt(int index) { + return buffer.getInt(index); + } + + public ChunkBuffer putInt(int index, int value) { + buffer.putInt(index, value); + return this; + } + + public long getLong() { + return buffer.getLong(); + } + + public ChunkBuffer putLong(long value) { + buffer.putLong(value); + return this; + } + + public long getLong(int index) { + return buffer.getLong(index); + } + + public ChunkBuffer putLong(int index, long value) { + buffer.putLong(index, value); + return this; + } + + public float getFloat() { + return buffer.getFloat(); + } + + public ChunkBuffer putFloat(float value) { + buffer.putFloat(value); + return this; + } + + public float getFloat(int index) { + return buffer.getFloat(index); + } + + public ChunkBuffer putFloat(int index, float value) { + buffer.putFloat(index, value); + return this; + } + + public double getDouble() { + return buffer.getDouble(); + } + + public ChunkBuffer putDouble(double value) { + buffer.putDouble(value); + return this; + } + + public double getDouble(int index) { + return buffer.getDouble(index); + } + + public ChunkBuffer putDouble(int index, double value) { + buffer.putDouble(index, value); + return this; + } + +} diff --git a/src/com/wentch/redkale/net/Context.java b/src/com/wentch/redkale/net/Context.java new file mode 100644 index 000000000..53e7f756b --- /dev/null +++ b/src/com/wentch/redkale/net/Context.java @@ -0,0 +1,104 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import com.wentch.redkale.watch.WatchFactory; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.ExecutorService; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public class Context { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + protected final long serverStartTime; + + protected final ExecutorService executor; + + protected final BufferPool bufferPool; + + protected final ResponsePool responsePool; + + protected final PrepareServlet prepare; + + private final InetSocketAddress address; + + protected final Charset charset; + + protected final int maxbody; + + protected final int readTimeoutSecond; + + protected final int writeTimeoutSecond; + + protected final Logger logger; + + protected final WatchFactory watch; + + public Context(long serverStartTime, Logger logger, ExecutorService executor, BufferPool bufferPool, ResponsePool responsePool, + final int maxbody, Charset charset, InetSocketAddress address, final PrepareServlet prepare, final WatchFactory watch, + final int readTimeoutSecond, final int writeTimeoutSecond) { + this.serverStartTime = serverStartTime; + this.logger = logger; + this.executor = executor; + this.bufferPool = bufferPool; + this.responsePool = responsePool; + this.maxbody = maxbody; + this.charset = UTF8.equals(charset) ? null : charset; + this.address = address; + this.prepare = prepare; + this.watch = watch; + this.readTimeoutSecond = readTimeoutSecond; + this.writeTimeoutSecond = writeTimeoutSecond; + } + + public int getMaxbody() { + return maxbody; + } + + public InetSocketAddress getServerAddress() { + return address; + } + + public long getServerStartTime() { + return serverStartTime; + } + + public Charset getCharset() { + return charset; + } + + public void submit(Runnable r) { + executor.submit(r); + } + + public ByteBuffer pollBuffer() { + return bufferPool.poll(); + } + + public void offerBuffer(ByteBuffer buffer) { + bufferPool.offer(buffer); + } + + public Logger getLogger() { + return logger; + } + + public int getReadTimeoutSecond() { + return readTimeoutSecond; + } + + public int getWriteTimeoutSecond() { + return writeTimeoutSecond; + } + +} diff --git a/src/com/wentch/redkale/net/PrepareRunner.java b/src/com/wentch/redkale/net/PrepareRunner.java new file mode 100644 index 000000000..43a6beb1a --- /dev/null +++ b/src/com/wentch/redkale/net/PrepareRunner.java @@ -0,0 +1,99 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.util.logging.Level; + +/** + * + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public final class PrepareRunner implements Runnable { + + private final AsyncConnection channel; + + private final Context context; + + private ByteBuffer data; + + public PrepareRunner(Context context, AsyncConnection channel, ByteBuffer data) { + this.context = context; + this.channel = channel; + this.data = data; + } + + @Override + public void run() { + final PrepareServlet prepare = context.prepare; + final ResponsePool responsePool = context.responsePool; + final ByteBuffer buffer = context.pollBuffer(); + if (data != null) { + final Response response = responsePool.poll(); + response.init(channel); + try { + prepare.prepare(data, response.request, response); + } catch (Throwable t) { + context.logger.log(Level.WARNING, "prepare servlet abort, forece to close channel ", t); + context.offerBuffer(buffer); + response.finish(true); + } + return; + } + try { + channel.read(buffer, null, new CompletionHandler() { + @Override + public void completed(Integer count, Void attachment1) { + if (count < 1 && buffer.remaining() == buffer.limit()) { + try { + context.offerBuffer(buffer); + channel.close(); + } catch (Exception e) { + context.logger.log(Level.FINEST, "PrepareRunner close channel erroneous on no read bytes", e); + } + return; + } +// { //测试 +// buffer.flip(); +// byte[] bytes = new byte[buffer.remaining()]; +// buffer.get(bytes); +// System.out.println(new String(bytes)); +// } + buffer.flip(); + final Response response = responsePool.poll(); + response.init(channel); + try { + prepare.prepare(buffer, response.request, response); + } catch (Throwable t) { + context.logger.log(Level.WARNING, "prepare servlet abort, forece to close channel ", t); + context.offerBuffer(buffer); + response.finish(true); + } + } + + @Override + public void failed(Throwable exc, Void attachment2) { + context.offerBuffer(buffer); + try { + channel.close(); + } catch (Exception e) { + } + if (exc != null) context.logger.log(Level.FINEST, "Servlet Handler read channel erroneous, forece to close channel ", exc); + } + }); + } catch (Exception te) { + context.offerBuffer(buffer); + try { + channel.close(); + } catch (Exception e) { + } + if (te != null) context.logger.log(Level.FINEST, "Servlet read channel erroneous, forece to close channel ", te); + } + } + +} diff --git a/src/com/wentch/redkale/net/PrepareServlet.java b/src/com/wentch/redkale/net/PrepareServlet.java new file mode 100644 index 000000000..d72ba36df --- /dev/null +++ b/src/com/wentch/redkale/net/PrepareServlet.java @@ -0,0 +1,72 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.atomic.*; +import java.util.logging.Level; + +/** + * + * @author zhangjx + * @param + * @param

+ */ +public abstract class PrepareServlet> implements Servlet { + + protected final AtomicLong executeCounter = new AtomicLong(); //执行请求次数 + + protected final AtomicLong illRequestCounter = new AtomicLong(); //错误请求次数 + + public final void prepare(final ByteBuffer buffer, final R request, final P response) throws IOException { + executeCounter.incrementAndGet(); + final int rs = request.readHeader(buffer); + if (rs < 0) { + response.context.offerBuffer(buffer); + illRequestCounter.incrementAndGet(); + response.finish(true); + } else if (rs == 0) { + response.context.offerBuffer(buffer); + this.execute(request, response); + } else { + buffer.clear(); + final AtomicInteger ai = new AtomicInteger(rs); + request.channel.read(buffer, buffer, new CompletionHandler() { + + @Override + public void completed(Integer result, ByteBuffer attachment) { + buffer.flip(); + ai.addAndGet(-buffer.remaining()); + request.readBody(buffer); + if (ai.get() > 0) { + buffer.clear(); + request.channel.read(buffer, buffer, this); + } else { + response.context.offerBuffer(buffer); + try { + execute(request, response); + } catch (Exception e) { + illRequestCounter.incrementAndGet(); + response.finish(true); + request.context.logger.log(Level.WARNING, "prepare servlet abort, forece to close channel ", e); + } + } + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + illRequestCounter.incrementAndGet(); + response.context.offerBuffer(buffer); + response.finish(true); + if (exc != null) request.context.logger.log(Level.FINER, "Servlet read channel erroneous, forece to close channel ", exc); + } + }); + } + } + +} diff --git a/src/com/wentch/redkale/net/ProtocolServer.java b/src/com/wentch/redkale/net/ProtocolServer.java new file mode 100644 index 000000000..51d6e9eca --- /dev/null +++ b/src/com/wentch/redkale/net/ProtocolServer.java @@ -0,0 +1,159 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.*; + +/** + * + * @author zhangjx + */ +public abstract class ProtocolServer { + + public abstract void open() throws IOException; + + public abstract void bind(SocketAddress local, int backlog) throws IOException; + + public abstract void setOption(SocketOption name, T value) throws IOException; + + public abstract void accept(); + + public abstract void close() throws IOException; + + public abstract AsynchronousChannelGroup getChannelGroup(); + + //--------------------------------------------------------------------- + public static ProtocolServer create(String protocol, Context context) { + if ("TCP".equalsIgnoreCase(protocol)) return new ProtocolTCPServer(context); + if ("UDP".equalsIgnoreCase(protocol)) return new ProtocolUDPServer(context); + throw new RuntimeException("ProtocolServer not support protocol " + protocol); + } + + private static final class ProtocolUDPServer extends ProtocolServer { + + private final Context context; + + private AsynchronousChannelGroup group; + + private AsyncDatagramChannel serverChannel; + + public ProtocolUDPServer(Context context) { + this.context = context; + } + + @Override + public void open() throws IOException { + this.group = AsynchronousChannelGroup.withCachedThreadPool(context.executor, 1); + this.serverChannel = AsyncDatagramChannel.open(group); + } + + @Override + public void bind(SocketAddress local, int backlog) throws IOException { + this.serverChannel.bind(local); + } + + @Override + public void setOption(SocketOption name, T value) throws IOException { + this.serverChannel.setOption(name, value); + } + + @Override + public void accept() { + final AsyncDatagramChannel serchannel = this.serverChannel; + final ByteBuffer buffer = this.context.pollBuffer(); + serchannel.receive(buffer, buffer, new CompletionHandler() { + + @Override + public void completed(final SocketAddress address, ByteBuffer attachment) { + final ByteBuffer buffer2 = context.pollBuffer(); + serchannel.receive(buffer2, buffer2, this); + attachment.flip(); + AsyncConnection conn = AsyncConnection.create(serchannel, address, false, context.readTimeoutSecond, context.writeTimeoutSecond); + context.submit(new PrepareRunner(context, conn, attachment)); + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + context.offerBuffer(attachment); + //if (exc != null) context.logger.log(Level.FINEST, AsyncDatagramChannel.class.getSimpleName() + " accept erroneous", exc); + } + }); + } + + @Override + public void close() throws IOException { + this.serverChannel.close(); + } + + @Override + public AsynchronousChannelGroup getChannelGroup() { + return this.group; + } + + } + + private static final class ProtocolTCPServer extends ProtocolServer { + + private final Context context; + + private AsynchronousChannelGroup group; + + private AsynchronousServerSocketChannel serverChannel; + + public ProtocolTCPServer(Context context) { + this.context = context; + } + + @Override + public void open() throws IOException { + group = AsynchronousChannelGroup.withCachedThreadPool(context.executor, 1); + this.serverChannel = AsynchronousServerSocketChannel.open(group); + } + + @Override + public void bind(SocketAddress local, int backlog) throws IOException { + this.serverChannel.bind(local, backlog); + } + + @Override + public void setOption(SocketOption name, T value) throws IOException { + this.serverChannel.setOption(name, value); + } + + @Override + public void accept() { + final AsynchronousServerSocketChannel serchannel = this.serverChannel; + serchannel.accept(null, new CompletionHandler() { + + @Override + public void completed(final AsynchronousSocketChannel channel, Void attachment) { + serchannel.accept(null, this); + context.submit(new PrepareRunner(context, AsyncConnection.create(channel, context.readTimeoutSecond, context.writeTimeoutSecond), null)); + } + + @Override + public void failed(Throwable exc, Void attachment) { + serchannel.accept(null, this); + //if (exc != null) context.logger.log(Level.FINEST, AsynchronousServerSocketChannel.class.getSimpleName() + " accept erroneous", exc); + } + }); + } + + @Override + public void close() throws IOException { + this.serverChannel.close(); + } + + @Override + public AsynchronousChannelGroup getChannelGroup() { + return this.group; + } + } + +} diff --git a/src/com/wentch/redkale/net/Request.java b/src/com/wentch/redkale/net/Request.java new file mode 100644 index 000000000..5005bc621 --- /dev/null +++ b/src/com/wentch/redkale/net/Request.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.nio.ByteBuffer; +import java.util.*; + +/** + * + * @author zhangjx + */ +public abstract class Request { + + protected final Context context; + + protected long createtime; + + protected boolean keepAlive; + + protected AsyncConnection channel; + + protected final Map attributes = new HashMap<>(); + + protected Request(Context context) { + this.context = context; + } + + /** + * 返回值: -1:数据不合法; 0:解析完毕; >0: 需再读取的字节数。 + * + * @param buffer + * @return + */ + protected abstract int readHeader(ByteBuffer buffer); + + protected abstract void readBody(ByteBuffer buffer); + + protected void recycle() { + createtime = 0; + keepAlive = false; + attributes.clear(); + channel = null; // close it by response + } + + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + @SuppressWarnings("unchecked") + public T getAttribute(String name) { + return (T) attributes.get(name); + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + + public Map getAttributes() { + return attributes; + } + + public Context getContext() { + return this.context; + } +} diff --git a/src/com/wentch/redkale/net/Response.java b/src/com/wentch/redkale/net/Response.java new file mode 100644 index 000000000..a498e9a4e --- /dev/null +++ b/src/com/wentch/redkale/net/Response.java @@ -0,0 +1,108 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.nio.ByteBuffer; +import java.nio.channels.*; + +/** + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +public abstract class Response { + + protected final Context context; + + protected final R request; + + protected AsyncConnection channel; + + protected final CompletionHandler finishHandler = new CompletionHandler() { + + @Override + public void completed(Integer result, ByteBuffer attachment) { + if (attachment.hasRemaining()) { + channel.write(attachment, attachment, this); + } else { + Response.this.context.offerBuffer(attachment); + Response.this.finish(); + } + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + Response.this.context.offerBuffer(attachment); + Response.this.finish(true); + } + + }; + + protected Response(Context context, final R request) { + this.context = context; + this.request = request; + } + + protected AsyncConnection removeChannel() { + AsyncConnection ch = this.channel; + this.channel = null; + return ch; + } + + protected void recycle() { + boolean keepAlive = request.keepAlive; + request.recycle(); + if (channel != null) { + if (keepAlive) { + this.context.submit(new PrepareRunner(context, channel, null)); + } else { + try { + if (channel.isOpen()) channel.close(); + } catch (Exception e) { + } + channel = null; + } + } + } + + protected void refuseAlive() { + this.request.keepAlive = false; + } + + protected void init(AsyncConnection channel) { + this.channel = channel; + this.request.channel = channel; + this.request.createtime = System.currentTimeMillis(); + } + + public void finish() { + this.finish(false); + } + + public void finish(boolean kill) { + //System.out.println("耗时: " + (System.currentTimeMillis() - request.createtime)); + if (kill) refuseAlive(); + this.context.responsePool.offer(this); + } + + public void finish(ByteBuffer buffer) { + finish(buffer, false); + } + + public void finish(ByteBuffer buffer, boolean kill) { + if (kill) refuseAlive(); + send(buffer, buffer, finishHandler); + } + + public void send(ByteBuffer buffer, A attachment, CompletionHandler handler) { + this.channel.write(buffer, attachment, handler); + } + + public Context getContext() { + return context; + } +} diff --git a/src/com/wentch/redkale/net/ResponsePool.java b/src/com/wentch/redkale/net/ResponsePool.java new file mode 100644 index 000000000..04d8aea77 --- /dev/null +++ b/src/com/wentch/redkale/net/ResponsePool.java @@ -0,0 +1,69 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.*; + +/** + * + * @author zhangjx + * @param + */ +public final class ResponsePool { + + public static interface ResponseFactory { + + T createResponse(); + } + + private final AtomicLong creatCounter; + + private final AtomicLong cycleCounter; + + private ResponseFactory factory; + + private final ArrayBlockingQueue queue; + + public ResponsePool(AtomicLong creatCounter, AtomicLong cycleCounter) { + this(creatCounter, cycleCounter, 0); + } + + public ResponsePool(AtomicLong creatCounter, AtomicLong cycleCounter, int max) { + this.queue = new ArrayBlockingQueue<>(Math.max(32, max)); + this.creatCounter = creatCounter; + this.cycleCounter = cycleCounter; + } + + public void setResponseFactory(ResponseFactory factory) { + this.factory = factory; + } + + public T poll() { + T result = queue.poll(); + if (result == null) { + creatCounter.incrementAndGet(); + result = factory.createResponse(); + } + return result; + } + + public void offer(final T e) { + if (e != null) { + cycleCounter.incrementAndGet(); + e.recycle(); + queue.offer(e); + } + } + + public long getCreatCount() { + return creatCounter.longValue(); + } + + public long getCycleCount() { + return cycleCounter.longValue(); + } +} diff --git a/src/com/wentch/redkale/net/SSLBuilder.java b/src/com/wentch/redkale/net/SSLBuilder.java new file mode 100644 index 000000000..4e13e3ddd --- /dev/null +++ b/src/com/wentch/redkale/net/SSLBuilder.java @@ -0,0 +1,38 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.security.*; +import javax.net.ssl.*; + +/** + * + * @author zhangjx + */ +public class SSLBuilder { + + private static char[] keypasswd; + + public static void main(String[] args) throws Exception { + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, keypasswd); + final String algorithm = System.getProperty("ssl.algorithm", KeyManagerFactory.getDefaultAlgorithm()); + final KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); + kmf.init(keyStore, keypasswd); + + final SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + //------------------------- + final SSLEngine sslEngine = sslContext.createSSLEngine(); + //sslEngine.setEnabledCipherSuites(null); + //sslEngine.setEnabledProtocols(null); + + sslEngine.setUseClientMode(false); + sslEngine.setWantClientAuth(false); + sslEngine.setNeedClientAuth(false); + + } +} diff --git a/src/com/wentch/redkale/net/Server.java b/src/com/wentch/redkale/net/Server.java new file mode 100644 index 000000000..3f05f17f6 --- /dev/null +++ b/src/com/wentch/redkale/net/Server.java @@ -0,0 +1,178 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.watch.WatchFactory; +import java.io.*; +import java.lang.reflect.Method; +import java.net.*; +import java.nio.charset.Charset; +import java.text.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public abstract class Server { + + public static final String RESNAME_ROOT = "SER_ROOT"; + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + //------------------------------------------------------------- + protected final long serverStartTime; + + protected final WatchFactory watch; + + protected final String protocol; + + protected AnyValue config; + + protected Charset charset; + + protected InetSocketAddress address; + + protected Context context; + + protected int backlog; + + protected ProtocolServer transport; + + protected int capacity; + + protected int threads; + + protected ExecutorService executor; + + protected int bufferPoolSize; + + protected int responsePoolSize; + + protected int maxbody; + + protected int readTimeoutSecond; + + protected int writeTimeoutSecond; + + private ScheduledThreadPoolExecutor scheduler; + + protected Server(long serverStartTime, String protocol, final WatchFactory watch) { + this.serverStartTime = serverStartTime; + this.protocol = protocol; + this.watch = watch; + } + + public void init(final AnyValue config) throws Exception { + Objects.requireNonNull(config); + this.config = config; + this.address = new InetSocketAddress(config.getValue("host", "0.0.0.0"), config.getIntValue("port", 80)); + this.charset = Charset.forName(config.getValue("charset", "UTF-8")); + this.backlog = config.getIntValue("backlog", 10240); + this.readTimeoutSecond = config.getIntValue("readTimeoutSecond", 0); + this.writeTimeoutSecond = config.getIntValue("writeTimeoutSecond", 0); + this.capacity = config.getIntValue("capacity", 8 * 1024); + this.maxbody = config.getIntValue("maxbody", 64 * 1024); + this.threads = config.getIntValue("threads", Runtime.getRuntime().availableProcessors() * 16); + this.bufferPoolSize = config.getIntValue("bufferPoolSize", Runtime.getRuntime().availableProcessors() * 512); + this.responsePoolSize = config.getIntValue("responsePoolSize", Runtime.getRuntime().availableProcessors() * 256); + final int port = this.address.getPort(); + final AtomicInteger counter = new AtomicInteger(); + final Format f = createFormat(); + this.executor = Executors.newFixedThreadPool(threads, (Runnable r) -> { + Thread t = new WorkThread(executor, r); + t.setName("Servlet-HTTP-" + port + "-Thread-" + f.format(counter.incrementAndGet())); + return t; + }); + } + + public void destroy(final AnyValue config) throws Exception { + if (scheduler != null) scheduler.shutdownNow(); + } + + public InetSocketAddress getSocketAddress() { + return address; + } + + public Logger getLogger() { + return this.logger; + } + + public void start() throws IOException { + this.context = this.createContext(); + this.context.prepare.init(this.context, config); + this.watch.inject(this.context.prepare); + this.transport = ProtocolServer.create(this.protocol, context); + this.transport.open(); + transport.setOption(StandardSocketOptions.SO_REUSEADDR, true); + transport.setOption(StandardSocketOptions.SO_RCVBUF, 8 * 1024); + transport.bind(address, backlog); + logger.info(this.getClass().getSimpleName() + " listen: " + address); + logger.info(this.getClass().getSimpleName() + " threads: " + threads + ", bufferPoolSize: " + bufferPoolSize + ", responsePoolSize: " + responsePoolSize); + transport.accept(); + logger.info(this.getClass().getSimpleName() + " started in " + (System.currentTimeMillis() - context.getServerStartTime()) + " ms"); + } + + protected abstract Context createContext(); + + public void shutdown() throws IOException { + long s = System.currentTimeMillis(); + logger.info(this.getClass().getSimpleName() + "-" + this.protocol + " shutdowning"); + try { + this.transport.close(); + } catch (Exception e) { + } + logger.info(this.getClass().getSimpleName() + "-" + this.protocol + " shutdow prepare servlet"); + this.context.prepare.destroy(this.context, config); + long e = System.currentTimeMillis() - s; + logger.info(this.getClass().getSimpleName() + " shutdown in " + e + " ms"); + } + + protected Format createFormat() { + String sf = "0"; + if (this.threads > 10) sf = "00"; + if (this.threads > 100) sf = "000"; + if (this.threads > 1000) sf = "0000"; + return new DecimalFormat(sf); + } + + public static void loadLib(final Logger logger, final String lib) throws Exception { + if (lib == null || lib.isEmpty()) return; + final Set set = new HashSet<>(); + for (String s : lib.split(";")) { + if (s.isEmpty()) continue; + if (s.endsWith("*")) { + File root = new File(s.substring(0, s.length() - 1)); + if (root.isDirectory()) { + for (File f : root.listFiles()) { + set.add(f.toURI().toURL()); + } + } + } else { + File f = new File(s); + if (f.canRead()) set.add(f.toURI().toURL()); + } + } + if (set.isEmpty()) return; + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl instanceof URLClassLoader) { + URLClassLoader loader = (URLClassLoader) cl; + final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + method.setAccessible(true); + for (URL url : set) { + method.invoke(loader, url); + //if (logger != null) logger.log(Level.INFO, "Server found ClassPath({0})", url); + } + } else { + Thread.currentThread().setContextClassLoader(new URLClassLoader(set.toArray(new URL[set.size()]), cl)); + } + } + +} diff --git a/src/com/wentch/redkale/net/Servlet.java b/src/com/wentch/redkale/net/Servlet.java new file mode 100644 index 000000000..4bb97ff39 --- /dev/null +++ b/src/com/wentch/redkale/net/Servlet.java @@ -0,0 +1,27 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import com.wentch.redkale.util.AnyValue; +import java.io.IOException; + +/** + * + * @author zhangjx + * @param + * @param

+ */ +public interface Servlet> { + + default void init(Context context, AnyValue config) { + } + + public void execute(R request, P response) throws IOException; + + default void destroy(Context context, AnyValue config) { + } + +} diff --git a/src/com/wentch/redkale/net/Transport.java b/src/com/wentch/redkale/net/Transport.java new file mode 100644 index 000000000..84dc68b12 --- /dev/null +++ b/src/com/wentch/redkale/net/Transport.java @@ -0,0 +1,138 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import com.wentch.redkale.watch.WatchFactory; +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +/** + * 传输客户端 + * + * @author zhangjx + */ +public final class Transport { + + protected SocketAddress[] remoteAddres; + + protected BufferPool bufferPool; + + protected String name; + + protected String protocol; + + protected final AsynchronousChannelGroup group; + + public Transport(String name, String protocol, WatchFactory watch, int bufferPoolSize, SocketAddress... addresses) { + this.name = name; + this.protocol = protocol; + AsynchronousChannelGroup g = null; + try { + final AtomicInteger counter = new AtomicInteger(); + ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 8, (Runnable r) -> { + Thread t = new Thread(r); + t.setDaemon(true); + t.setName("Transport-" + name + "-Thread-" + counter.incrementAndGet()); + return t; + }); + g = AsynchronousChannelGroup.withCachedThreadPool(executor, 1); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.group = g; + AtomicLong createBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber(Transport.class.getSimpleName() + "_" + protocol + ".Buffer.creatCounter"); + AtomicLong cycleBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber(Transport.class.getSimpleName() + "_" + protocol + ".Buffer.cycleCounter"); + this.bufferPool = new BufferPool(createBufferCounter, cycleBufferCounter, 8192, bufferPoolSize); + this.remoteAddres = addresses; + } + + public ByteBuffer pollBuffer() { + return bufferPool.poll(); + } + + public void offerBuffer(ByteBuffer buffer) { + bufferPool.offer(buffer); + } + + public AsyncConnection pollConnection() { + SocketAddress addr = remoteAddres[0]; + try { + if ("TCP".equalsIgnoreCase(protocol)) { + AsynchronousSocketChannel channel = AsynchronousSocketChannel.open(group); + channel.connect(addr).get(2, TimeUnit.SECONDS); + return AsyncConnection.create(channel, 0, 0); + } else { + AsyncDatagramChannel channel = AsyncDatagramChannel.open(group); + channel.connect(addr); + return AsyncConnection.create(channel, addr, true, 0, 0); + } + } catch (Exception ex) { + throw new RuntimeException("transport address = " + addr, ex); + } + } + + public void offerConnection(AsyncConnection conn) { + try { + conn.close(); + } catch (IOException io) { + } + } + + public void async(final ByteBuffer buffer, A att, final CompletionHandler handler) { + final AsyncConnection conn = pollConnection(); + conn.write(buffer, buffer, new CompletionHandler() { + + @Override + public void completed(Integer result, ByteBuffer attachment) { + buffer.clear(); + conn.read(buffer, buffer, new CompletionHandler() { + + @Override + public void completed(Integer result, ByteBuffer attachment) { + if (handler != null) handler.completed(result, att); + offerBuffer(buffer); + offerConnection(conn); + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + offerBuffer(buffer); + offerConnection(conn); + } + }); + + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + offerBuffer(buffer); + offerConnection(conn); + } + }); + } + + public ByteBuffer send(ByteBuffer buffer) { + AsyncConnection conn = pollConnection(); + final int readto = conn.getReadTimeoutSecond(); + final int writeto = conn.getWriteTimeoutSecond(); + try { + conn.write(buffer).get(writeto > 0 ? writeto : 5, TimeUnit.SECONDS); + buffer.clear(); + conn.read(buffer).get(readto > 0 ? readto : 5, TimeUnit.SECONDS); + buffer.flip(); + return buffer; + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + offerConnection(conn); + } + } + +} diff --git a/src/com/wentch/redkale/net/WorkThread.java b/src/com/wentch/redkale/net/WorkThread.java new file mode 100644 index 000000000..98b3c76ce --- /dev/null +++ b/src/com/wentch/redkale/net/WorkThread.java @@ -0,0 +1,27 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net; + +import java.util.concurrent.*; + +/** + * + * @author zhangjx + */ +public class WorkThread extends Thread { + + private final ExecutorService executor; + + public WorkThread(ExecutorService executor, Runnable runner) { + super(runner); + this.executor = executor; + this.setDaemon(true); + } + + public void submit(Runnable runner) { + executor.submit(runner); + } +} diff --git a/src/com/wentch/redkale/net/http/AuthIgnore.java b/src/com/wentch/redkale/net/http/AuthIgnore.java new file mode 100644 index 000000000..fc95f8774 --- /dev/null +++ b/src/com/wentch/redkale/net/http/AuthIgnore.java @@ -0,0 +1,22 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({METHOD, TYPE}) +@Retention(RUNTIME) +public @interface AuthIgnore { + +} diff --git a/src/com/wentch/redkale/net/http/BasedHttpServlet.java b/src/com/wentch/redkale/net/http/BasedHttpServlet.java new file mode 100644 index 000000000..25dd0c395 --- /dev/null +++ b/src/com/wentch/redkale/net/http/BasedHttpServlet.java @@ -0,0 +1,196 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.Response; +import com.wentch.redkale.net.Request; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.AnyValue; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.*; +import jdk.internal.org.objectweb.asm.*; +import static jdk.internal.org.objectweb.asm.Opcodes.*; + +/** + * + * @author zhangjx + */ +public abstract class BasedHttpServlet extends HttpServlet { + + private Map.Entry[] actions; + + @Override + public final void execute(HttpRequest request, HttpResponse response) throws IOException { + for (Map.Entry en : actions) { + if (request.getRequestURI().startsWith(en.getKey())) { + Entry entry = en.getValue(); + if (entry.ignore || authenticate(request, response)) entry.servlet.execute(request, response); + return; + } + } + throw new IOException(this.getClass().getName() + " not found method for URI(" + request.getRequestURI() + ")"); + } + + @Override + public void init(Context context, AnyValue config) { + String path = ((HttpContext) context).getContextPath(); + WebServlet ws = this.getClass().getAnnotation(WebServlet.class); + if (ws != null && !ws.fillurl()) path = ""; + HashMap map = load(); + this.actions = new Map.Entry[map.size()]; + int i = -1; + for (Map.Entry en : map.entrySet()) { + actions[++i] = new AbstractMap.SimpleEntry<>(path + en.getKey(), en.getValue()); + } + } + + public abstract boolean authenticate(HttpRequest request, HttpResponse response) throws IOException; + + private HashMap load() { + final boolean typeIgnore = this.getClass().getAnnotation(AuthIgnore.class) != null; + WebServlet module = this.getClass().getAnnotation(WebServlet.class); + final int serviceid = module == null ? 0 : module.moduleid(); + final HashMap map = new HashMap<>(); + Set nameset = new HashSet<>(); + for (final Method method : this.getClass().getMethods()) { + //----------------------------------------------- + String methodname = method.getName(); + if ("service".equals(methodname) || "execute".equals(methodname) || "authenticate".equals(methodname)) continue; + //----------------------------------------------- + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length != 2 || paramTypes[0] != HttpRequest.class + || paramTypes[1] != HttpResponse.class) continue; + //----------------------------------------------- + Class[] exps = method.getExceptionTypes(); + if (exps.length > 0 && (exps.length != 1 || exps[0] != IOException.class)) continue; + //----------------------------------------------- + String name = methodname; + int actionid = 0; + WebAction action = method.getAnnotation(WebAction.class); + if (action != null) { + actionid = action.actionid(); + name = action.url().trim(); + } + if (nameset.contains(name)) throw new RuntimeException(this.getClass().getSimpleName() + " has two same " + WebAction.class.getSimpleName() + "(" + name + ")"); + for (String n : nameset) { + if (n.contains(name) || name.contains(n)) { + throw new RuntimeException(this.getClass().getSimpleName() + " has two overlap " + WebAction.class.getSimpleName() + "(" + name + ", " + n + ")"); + } + } + nameset.add(name); + map.put(name, new Entry(typeIgnore, serviceid, actionid, name, method, createHttpServlet(method))); + } + return map; + } + + private HttpServlet createHttpServlet(final Method method) { + //------------------------------------------------------------------------------ + final String supDynName = HttpServlet.class.getName().replace('.', '/'); + final String interName = this.getClass().getName().replace('.', '/'); + final String interDesc = jdk.internal.org.objectweb.asm.Type.getDescriptor(this.getClass()); + final String requestSupDesc = jdk.internal.org.objectweb.asm.Type.getDescriptor(Request.class); + final String responseSupDesc = jdk.internal.org.objectweb.asm.Type.getDescriptor(Response.class); + final String requestDesc = jdk.internal.org.objectweb.asm.Type.getDescriptor(HttpRequest.class); + final String responseDesc = jdk.internal.org.objectweb.asm.Type.getDescriptor(HttpResponse.class); + String newDynName = interName + "_Dyn_" + method.getName(); + int i = 0; + for (;;) { + try { + Class.forName(newDynName.replace('/', '.')); + newDynName += "_" + (++i); + } catch (Exception ex) { + break; + } + } + //------------------------------------------------------------------------------ + ClassWriter cw = new ClassWriter(0); + FieldVisitor fv; + MethodVisitor mv; + AnnotationVisitor av0; + final String factfield = "factServlet"; + cw.visit(V1_8, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, newDynName, null, supDynName, null); + { + fv = cw.visitField(ACC_PUBLIC, factfield, interDesc, null, null); + fv.visitEnd(); + } + { //构造函数 + mv = (cw.visitMethod(ACC_PUBLIC, "", "()V", null, null)); + //mv.setDebug(true); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, supDynName, "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + { + mv = (cw.visitMethod(ACC_PUBLIC, "execute", "(" + requestDesc + responseDesc + ")V", null, new String[]{"java/io/IOException"})); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, factfield, interDesc); + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, interName, method.getName(), "(" + requestDesc + responseDesc + ")V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(3, 3); + mv.visitEnd(); + } + { + mv = cw.visitMethod(ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC, "execute", "(" + requestSupDesc + responseSupDesc + ")V", null, new String[]{"java/io/IOException"}); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitTypeInsn(CHECKCAST, HttpRequest.class.getName().replace('.', '/')); + mv.visitVarInsn(ALOAD, 2); + mv.visitTypeInsn(CHECKCAST, HttpResponse.class.getName().replace('.', '/')); + mv.visitMethodInsn(INVOKEVIRTUAL, newDynName, "execute", "(" + requestDesc + responseDesc + ")V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(3, 3); + mv.visitEnd(); + } + cw.visitEnd(); + //------------------------------------------------------------------------------ + byte[] bytes = cw.toByteArray(); + Class newClazz = new ClassLoader(this.getClass().getClassLoader()) { + public final Class loadClass(String name, byte[] b) { + return defineClass(name, b, 0, b.length); + } + }.loadClass(newDynName.replace('/', '.'), bytes); + try { + HttpServlet instance = (HttpServlet) newClazz.newInstance(); + instance.getClass().getField(factfield).set(instance, this); + return instance; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private static final class Entry { + + public Entry(boolean typeIgnore, int moduleid, int actionid, String name, Method method, HttpServlet servlet) { + this.moduleid = moduleid; + this.actionid = actionid; + this.name = name; + this.method = method; + this.servlet = servlet; + this.ignore = typeIgnore || method.getAnnotation(AuthIgnore.class) != null; + } + + public boolean isNeedCheck() { + return this.moduleid != 0 || this.actionid != 0; + } + + public final boolean ignore; + + public final int moduleid; + + public final int actionid; + + public final String name; + + public final Method method; + + public final HttpServlet servlet; + } +} diff --git a/src/com/wentch/redkale/net/http/ByteArray.java b/src/com/wentch/redkale/net/http/ByteArray.java new file mode 100644 index 000000000..c86cdbcf4 --- /dev/null +++ b/src/com/wentch/redkale/net/http/ByteArray.java @@ -0,0 +1,136 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.util.Utility; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * + * @author zhangjx + */ +public final class ByteArray { + + private byte[] content; + + private int count; + + public ByteArray() { + this(1024); + } + + public ByteArray(int size) { + content = new byte[Math.max(128, size)]; + } + + public void clear() { + this.count = 0; + } + + public int find(byte value) { + return find(0, value); + } + + public boolean equal(final byte[] bytes) { + if (bytes == null || count != bytes.length) return false; + for (int i = 0; i < count; i++) { + if (content[i] != bytes[i]) return false; + } + return true; + } + + public boolean isEmpty() { + return count == 0; + } + + public int count() { + return count; + } + + public void write(byte[] buf) { + System.arraycopy(this.content, 0, buf, 0, count); + } + + public int find(int offset, char value) { + return find(offset, (byte) value); + } + + public int find(int offset, byte value) { + return find(offset, -1, value); + } + + public int find(int offset, int limit, char value) { + return find(offset, limit, (byte) value); + } + + public int find(int offset, int limit, byte value) { + byte[] bytes = this.content; + int end = limit > 0 ? limit : count; + for (int i = offset; i < end; i++) { + if (bytes[i] == value) return i; + } + return -1; + } + + public void add(byte value) { + if (count >= content.length - 1) { + byte[] ns = new byte[content.length + 8]; + System.arraycopy(content, 0, ns, 0, count); + this.content = ns; + } + content[count++] = value; + } + + public void add(ByteBuffer buffer, int len) { + if (len < 1) return; + if (count >= content.length - len) { + byte[] ns = new byte[content.length + len]; + System.arraycopy(content, 0, ns, 0, count); + this.content = ns; + } + buffer.get(content, count, len); + count += len; + } + + @Override + public String toString() { + return new String(content, 0, count); + } + + public String toString(final int offset, int len, final Charset charset) { + if (charset == null) return new String(Utility.decodeUTF8(content, offset, len)); + return new String(content, offset, len, charset); + } + + public String toDecodeString(final int offset, int len, final Charset charset) { + int index = offset; + for (int i = offset; i < (offset + len); i++) { + if (content[i] == '+') { + content[index] = ' '; + } else if (content[i] == '%') { + content[index] = (byte) ((hexBit(content[++i]) * 16 + hexBit(content[++i]))); + } else { + content[index] = content[i]; + } + index++; + } + for (int i = index + 1; i < (offset + len); i++) { + content[i] = ' '; + } + len = index - offset; + if (charset == null) return new String(Utility.decodeUTF8(content, offset, len)); + return new String(content, offset, len, charset); + } + + private static int hexBit(byte b) { + if ('0' <= b && '9' >= b) return b - '0'; + if ('a' <= b && 'z' >= b) return b - 'a' + 10; + if ('A' <= b && 'Z' >= b) return b - 'A' + 10; + return b; + } + +} diff --git a/src/com/wentch/redkale/net/http/HttpContext.java b/src/com/wentch/redkale/net/http/HttpContext.java new file mode 100644 index 000000000..b5a3f530b --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpContext.java @@ -0,0 +1,69 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.convert.json.JsonConvert; +import com.wentch.redkale.convert.json.JsonFactory; +import com.wentch.redkale.net.PrepareServlet; +import com.wentch.redkale.net.ResponsePool; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.net.BufferPool; +import com.wentch.redkale.util.Utility; +import com.wentch.redkale.watch.WatchFactory; +import java.net.InetSocketAddress; +import java.nio.charset.Charset; +import java.security.SecureRandom; +import java.util.concurrent.ExecutorService; +import java.util.logging.Logger; + +/** + * + * @author zhangjx + */ +public final class HttpContext extends Context { + + protected final String contextPath; + + protected final JsonFactory jsonFactory; + + protected final SecureRandom random = new SecureRandom(); + + public HttpContext(long serverStartTime, Logger logger, ExecutorService executor, BufferPool bufferPool, + ResponsePool responsePool, int maxbody, Charset charset, InetSocketAddress address, + PrepareServlet prepare, WatchFactory watch, int readTimeoutSecond, int writeTimeoutSecond, String contextPath) { + super(serverStartTime, logger, executor, bufferPool, responsePool, maxbody, charset, + address, prepare, watch, readTimeoutSecond, writeTimeoutSecond); + this.contextPath = contextPath; + this.jsonFactory = JsonFactory.root(); + random.setSeed(Math.abs(System.nanoTime())); + } + + public String getContextPath() { + return this.contextPath; + } + + protected String createSessionid() { + byte[] bytes = new byte[16]; + random.nextBytes(bytes); + return new String(Utility.binToHex(bytes)); + } + + protected WatchFactory getWatchFactory() { + return watch; + } + + protected ExecutorService getExecutor() { + return executor; + } + + protected ResponsePool getResponsePool() { + return responsePool; + } + + public JsonConvert getJsonConvert() { + return jsonFactory.getConvert(); + } +} diff --git a/src/com/wentch/redkale/net/http/HttpPrepareServlet.java b/src/com/wentch/redkale/net/http/HttpPrepareServlet.java new file mode 100644 index 000000000..8c4e9c4f4 --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpPrepareServlet.java @@ -0,0 +1,163 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.PrepareServlet; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.util.Utility; +import com.wentch.redkale.util.AnyValue.DefaultAnyValue; +import com.wentch.redkale.watch.WatchFactory; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.AbstractMap.SimpleEntry; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.regex.Pattern; + +/** + * + * @author zhangjx + */ +public final class HttpPrepareServlet extends PrepareServlet { + + private ByteBuffer flashPolicyBuffer; + + private final List> servlets = new ArrayList<>(); + + private final Map strmaps = new HashMap<>(); + + private SimpleEntry, HttpServlet>[] regArray = new SimpleEntry[0]; + + private boolean forwardproxy = false; + + private final HttpServlet proxyServlet = new HttpProxyServlet(); + + private HttpServlet resourceHttpServlet = new HttpResourceServlet(); + + private String flashdomain = "*"; + + private String flashports = "80,443,$"; + + @Override + public void init(Context context, AnyValue config) { + this.servlets.stream().forEach((en) -> { + en.getKey().init(context, en.getValue()); + }); + final WatchFactory watch = ((HttpContext) context).getWatchFactory(); + if (watch != null) { + this.servlets.stream().forEach((en) -> { + watch.inject(en.getKey()); + }); + } + if (config != null) { + AnyValue ssConfig = config.getAnyValue("servlets"); + if (ssConfig != null) { + AnyValue resConfig = ssConfig.getAnyValue("resource-servlet"); + if (resConfig instanceof DefaultAnyValue) { + if (resConfig.getValue("webroot") == null) + ((DefaultAnyValue) resConfig).addValue("webroot", config.getValue("root")); + } + if (resConfig == null) { + DefaultAnyValue dresConfig = new DefaultAnyValue(); + dresConfig.addValue("webroot", config.getValue("root")); + resConfig = dresConfig; + } + this.resourceHttpServlet.init(context, resConfig); + } + this.flashdomain = config.getValue("crossdomain-domain", "*"); + this.flashports = config.getValue("crossdomain-ports", "80,443,$"); + this.forwardproxy = config.getBoolValue("forwardproxy", false); + if (forwardproxy) this.proxyServlet.init(context, config); + } + } + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + try { + if (request.flashPolicy) { + response.skipHeader(); + if (flashPolicyBuffer == null) { + flashPolicyBuffer = ByteBuffer.wrap(("" + + "" + + "" + + "").getBytes()).asReadOnlyBuffer(); + } + response.finish(flashPolicyBuffer.duplicate(), true); + return; + } + final String uri = request.getRequestURI(); + if (forwardproxy && (uri.charAt(0) != '/' || "CONNECT".equalsIgnoreCase(request.getMethod()))) { //正向代理 + proxyServlet.execute(request, response); + return; + } + HttpServlet servlet = this.strmaps.isEmpty() ? null : this.strmaps.get(uri); + if (servlet == null && this.regArray != null) { + for (SimpleEntry, HttpServlet> en : regArray) { + if (en.getKey().test(uri)) { + servlet = en.getValue(); + break; + } + } + } + if (servlet == null) servlet = this.resourceHttpServlet; + servlet.execute(request, response); + } catch (Exception e) { + request.getContext().getLogger().log(Level.WARNING, "Servlet occur, forece to close channel ", e); + response.finish(505, null); + } + } + + public void addHttpServlet(HttpServlet servlet, AnyValue conf, String... mappings) { + for (String mapping : mappings) { + if (contains(mapping, '.', '*', '{', '[', '(', '|', '^', '$', '+', '?', '\\')) { //是否是正则表达式)) + if (mapping.charAt(0) != '^') mapping = '^' + mapping; + if (mapping.endsWith("/*")) { + mapping = mapping.substring(0, mapping.length() - 1) + ".*"; + } else { + mapping += "$"; + } + if (regArray == null) { + regArray = new SimpleEntry[1]; + regArray[0] = new SimpleEntry<>(Pattern.compile(mapping).asPredicate(), servlet); + } else { + regArray = Arrays.copyOf(regArray, regArray.length + 1); + regArray[regArray.length - 1] = new SimpleEntry<>(Pattern.compile(mapping).asPredicate(), servlet); + } + } else if (mapping != null && !mapping.isEmpty()) { + strmaps.put(mapping, servlet); + } + } + this.servlets.add(new SimpleEntry<>(servlet, conf)); + } + + private static boolean contains(String string, char... values) { + if (string == null) return false; + for (char ch : Utility.charArray(string)) { + for (char ch2 : values) { + if (ch == ch2) return true; + } + } + return false; + } + + public void setResourceServlet(HttpServlet servlet) { + if (servlet != null) { + this.resourceHttpServlet = servlet; + } + } + + @Override + public void destroy(Context context, AnyValue config) { + this.resourceHttpServlet.destroy(context, config); + this.servlets.stream().forEach((en) -> { + en.getKey().destroy(context, en.getValue()); + }); + } + +} diff --git a/src/com/wentch/redkale/net/http/HttpProxyServlet.java b/src/com/wentch/redkale/net/http/HttpProxyServlet.java new file mode 100644 index 000000000..6429fa8e7 --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpProxyServlet.java @@ -0,0 +1,197 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.AsyncConnection; +import com.wentch.redkale.util.AnyValue; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; + +/** + * 在appliation.xml中的HTTP类型的server节点加上forwardproxy="true"表示该HttpServer支持正向代理 + * + * @author zhangjx + */ +public final class HttpProxyServlet extends HttpServlet { + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + response.skipHeader(); + if ("CONNECT".equalsIgnoreCase(request.getMethod())) { + connect(request, response); + return; + } + String url = request.getRequestURI(); + url = url.substring(url.indexOf("://") + 3); + url = url.substring(url.indexOf('/')); + final ByteBuffer buffer = response.getContext().pollBuffer(); + buffer.put((request.getMethod() + " " + url + " HTTP/1.1\r\n").getBytes()); + for (AnyValue.Entry en : request.header.getStringEntrys()) { + if (!en.name.startsWith("Proxy-")) { + buffer.put((en.name + ": " + en.getValue() + "\r\n").getBytes()); + } + } + if (request.getHost() != null) { + buffer.put(("Host: " + request.getHost() + "\r\n").getBytes()); + } + if (request.getContentType() != null) { + buffer.put(("Content-Type: " + request.getContentType() + "\r\n").getBytes()); + } + if (request.getContentLength() > 0) { + buffer.put(("Content-Length: " + request.getContentLength() + "\r\n").getBytes()); + } + buffer.put(HttpResponse.LINE); + buffer.flip(); + final AsyncConnection remote = AsyncConnection.create("TCP", request.getHostSocketAddress(), 6, 6); + remote.write(buffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + if (buffer.hasRemaining()) { + remote.write(buffer, attachment, this); + return; + } + response.getContext().offerBuffer(buffer); + new ProxyCompletionHandler(remote, request, response).completed(0, null); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.getContext().offerBuffer(buffer); + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + } + + private void connect(HttpRequest request, HttpResponse response) throws IOException { + final AsyncConnection remote = AsyncConnection.create("TCP", HttpRequest.parseSocketAddress(request.getRequestURI()), 6, 6); + final ByteBuffer buffer0 = response.getContext().pollBuffer(); + buffer0.put("HTTP/1.1 200 Connection established\r\nConnection: close\r\n\r\n".getBytes()); + buffer0.flip(); + response.send(buffer0, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + if (buffer0.hasRemaining()) { + response.send(buffer0, attachment, this); + return; + } + response.getContext().offerBuffer(buffer0); + new ProxyCompletionHandler(remote, request, response).completed(0, null); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.getContext().offerBuffer(buffer0); + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + + } + + private static class ProxyCompletionHandler implements CompletionHandler { + + private AsyncConnection remote; + + private HttpRequest request; + + private HttpResponse response; + + public ProxyCompletionHandler(AsyncConnection remote, HttpRequest request, HttpResponse response) { + this.remote = remote; + this.request = request; + this.response = response; + } + + @Override + public void completed(Integer result0, Void v0) { + final ByteBuffer rbuffer = request.getContext().pollBuffer(); + remote.read(rbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + rbuffer.flip(); + CompletionHandler parent = this; + response.send(rbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + rbuffer.clear(); + remote.read(rbuffer, attachment, parent); + } + + @Override + public void failed(Throwable exc, Void attachment) { + parent.failed(exc, attachment); + } + }); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.getContext().offerBuffer(rbuffer); + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + + final ByteBuffer qbuffer = request.getContext().pollBuffer(); + request.getChannel().read(qbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + qbuffer.flip(); + CompletionHandler parent = this; + remote.write(qbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + qbuffer.clear(); + request.getChannel().read(qbuffer, null, parent); + } + + @Override + public void failed(Throwable exc, Void attachment) { + parent.failed(exc, attachment); + } + }); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.getContext().offerBuffer(qbuffer); + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + } + + @Override + public void failed(Throwable exc, Void v) { + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + } + +} diff --git a/src/com/wentch/redkale/net/http/HttpRequest.java b/src/com/wentch/redkale/net/http/HttpRequest.java new file mode 100644 index 000000000..346aff4d7 --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpRequest.java @@ -0,0 +1,437 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.convert.json.JsonFactory; +import com.wentch.redkale.convert.json.JsonConvert; +import com.wentch.redkale.net.AsyncConnection; +import com.wentch.redkale.net.Request; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.AnyValue.DefaultAnyValue; +import java.io.*; +import java.net.*; +import java.nio.*; +import java.nio.channels.*; +import java.nio.charset.*; + +/** + * + * @author zhangjx + */ +public final class HttpRequest extends Request { + + protected static final String SESSIONID_NAME = "JSESSIONID"; + + private static final byte[] flashRequestContent1 = "\0".getBytes(); + + private static final byte[] flashRequestContent2 = "".getBytes(); + + private String method; + + private String protocol; + + protected String requestURI; + + private long contentLength = -1; + + private String contentType; + + private String host; + + private String connection; + + protected String cookiestr; + + private HttpCookie[] cookies; + + protected String newsessionid; + + protected final JsonConvert convert; + + protected final DefaultAnyValue header = new DefaultAnyValue(); + + protected final DefaultAnyValue params = new DefaultAnyValue(); + + private final ByteArray array = new ByteArray(); + + protected boolean flashPolicy = false; + + protected boolean boundary = false; + + protected HttpRequest(Context context, JsonFactory factory) { + super(context); + this.convert = factory.getConvert(); + } + + protected void setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + } + + protected boolean isKeepAlive() { + return this.keepAlive; + } + + protected AsyncConnection getChannel() { + return this.channel; + } + + @Override + protected int readHeader(final ByteBuffer buffer) { + if (!readLine(buffer, array)) { + if (array.equal(flashRequestContent1) || array.equal(flashRequestContent2)) { //兼容 flash socket + this.flashPolicy = true; + return 0; + } + return -1; + } + Charset charset = this.context.getCharset(); + int index = 0; + int offset = array.find(index, ' '); + if (offset <= 0) return -1; + this.method = array.toString(index, offset, charset).trim(); + index = ++offset; + offset = array.find(index, ' '); + if (offset <= 0) return -1; + int off = array.find(index, '#'); + if (off > 0) offset = off; + int qst = array.find(index, offset, (byte) '?'); + if (qst > 0) { + this.requestURI = array.toDecodeString(index, qst - index, charset).trim(); + addParameter(array, qst + 1, offset - qst - 1); + } else { + this.requestURI = array.toDecodeString(index, offset - index, charset).trim(); + } + if (this.requestURI.contains("../")) return -1; + index = ++offset; + this.protocol = array.toString(index, array.count() - index, charset).trim(); + while (readLine(buffer, array)) { + if (array.count() < 2) break; + index = 0; + offset = array.find(index, ':'); + if (offset <= 0) return -1; + String name = array.toString(index, offset, charset).trim(); + index = offset + 1; + String value = array.toString(index, array.count() - index, charset).trim(); + switch (name) { + case "Content-Type": + this.contentType = value; + break; + case "Content-Length": + this.contentLength = Long.decode(value); + break; + case "Host": + this.host = value; + break; + case "Cookie": + if (this.cookiestr == null || this.cookiestr.isEmpty()) { + this.cookiestr = value; + } else { + this.cookiestr += ";" + value; + } + break; + case "Connection": + this.connection = value; + this.keepAlive = "Keep-Alive".equalsIgnoreCase(value); + break; + default: + header.addValue(name, value); + } + } + array.clear(); + if (buffer.hasRemaining()) array.add(buffer, buffer.remaining()); + if (this.contentType != null && this.contentType.contains("boundary=")) { + this.boundary = true; + } + if (this.contentLength > 0 && (this.contentType == null || !this.boundary)) { + if (this.contentLength > context.getMaxbody()) return -1; + int lr = (int) this.contentLength - array.count(); + return lr > 0 ? lr : 0; + } + return 0; + } + + @Override + protected void readBody(ByteBuffer buffer) { + array.add(buffer, buffer.remaining()); + } + + private void parseBody() { + if (this.boundary || array.isEmpty()) return; + addParameter(array, 0, array.count()); + array.clear(); + } + + private void addParameter(final ByteArray array, final int offset, final int len) { + if (len < 1) return; + Charset charset = this.context.getCharset(); + int limit = offset + len; + int keypos = array.find(offset, limit, '='); + int valpos = array.find(offset, limit, '&'); + if (keypos <= 0 || (valpos >= 0 && valpos < keypos)) { + if (valpos > 0) addParameter(array, valpos + 1, limit - valpos - 1); + return; + } + String name = array.toDecodeString(offset, keypos - offset, charset); + ++keypos; + String value = array.toDecodeString(keypos, (valpos < 0) ? (limit - keypos) : (valpos - keypos), charset); + this.params.addValue(name, value); + if (valpos >= 0) { + addParameter(array, valpos + 1, limit - valpos - 1); + } + } + + private boolean readLine(ByteBuffer buffer, ByteArray bytes) { + byte lasted = '\r'; + bytes.clear(); + for (;;) { + if (!buffer.hasRemaining()) { + if (lasted != '\r') bytes.add(lasted); + return false; + } + byte b = buffer.get(); + if (b == -1 || (lasted == '\r' && b == '\n')) break; + if (lasted != '\r') bytes.add(lasted); + lasted = b; + } + return true; + } + + @Override + public HttpContext getContext() { + return (HttpContext) this.context; + } + + public String getRemoteAddr() { + return String.valueOf(getRemoteAddress()); + } + + public SocketAddress getRemoteAddress() { + return this.channel.getRemoteAddress(); + } + + @Override + public String toString() { + parseBody(); + return this.getClass().getSimpleName() + "{method:" + this.method + ", requestURI:" + this.requestURI + + ", contentType:" + this.contentType + ", connection:" + this.connection + ", protocol:" + this.protocol + + ", contentLength:" + this.contentLength + ", cookiestr:" + this.cookiestr + + ", host:" + this.host + ", params:" + this.params + ", header:" + this.header + "}"; + } + + public final MultiContext getMultiContext() { + return new MultiContext(context.getCharset(), this.getContentType(), + new BufferedInputStream(Channels.newInputStream(this.channel), Math.max(array.count(), 8192)) { + { + array.write(this.buf); + this.count = array.count(); + } + }); + } + + @Override + protected void recycle() { + this.cookiestr = null; + this.cookies = null; + this.newsessionid = null; + this.method = null; + this.protocol = null; + this.requestURI = null; + this.contentType = null; + this.host = null; + this.connection = null; + this.contentLength = -1; + this.boundary = false; + this.flashPolicy = false; + + this.header.clear(); + this.params.clear(); + super.recycle(); + } + + public String getSessionid(boolean create) { + String sessionid = getCookie(SESSIONID_NAME, null); + if (create && (sessionid == null || sessionid.isEmpty())) { + sessionid = ((HttpContext) context).createSessionid(); + this.newsessionid = sessionid; + } + return sessionid; + } + + public String changeSessionid() { + this.newsessionid = ((HttpContext) context).createSessionid(); + return newsessionid; + } + + public void invalidateSession() { + this.newsessionid = ""; //为空表示删除sessionid + } + + public HttpCookie[] getCookies() { + if (this.cookies == null) this.cookies = parseCookies(this.cookiestr); + return this.cookies; + } + + public String getCookie(String name) { + return getCookie(name, null); + } + + public String getCookie(String name, String dfvalue) { + for (HttpCookie cookie : getCookies()) { + if (name.equals(cookie.getName())) return cookie.getValue(); + } + return dfvalue; + } + + private static HttpCookie[] parseCookies(String cookiestr) { + if (cookiestr == null || cookiestr.isEmpty()) return new HttpCookie[0]; + String str = cookiestr.replaceAll("(^;)|(;$)", "").replaceAll(";+", ";"); + if (str.isEmpty()) return new HttpCookie[0]; + String[] strs = str.split(";"); + HttpCookie[] cookies = new HttpCookie[strs.length]; + for (int i = 0; i < strs.length; i++) { + String s = strs[i]; + int pos = s.indexOf('='); + String v = (pos < 0 ? "" : s.substring(pos + 1)); + if (v.indexOf('"') == 0 && v.lastIndexOf('"') == v.length() - 1) v = v.substring(1, v.length() - 1); + cookies[i] = new HttpCookie((pos < 0 ? s : s.substring(0, pos)), v); + } + return cookies; + } + + public String getConnection() { + return connection; + } + + public String getMethod() { + return method; + } + + public String getProtocol() { + return protocol; + } + + public String getHost() { + return host; + } + + protected static InetSocketAddress parseSocketAddress(String host) { + if (host == null || host.isEmpty()) return null; + int pos = host.indexOf(':'); + String hostname = pos < 0 ? host : host.substring(0, pos); + int port = pos < 0 ? 80 : Integer.parseInt(host.substring(pos + 1)); + return new InetSocketAddress(hostname, port); + } + + protected InetSocketAddress getHostSocketAddress() { + return parseSocketAddress(host); + } + + /** + * 截取getRequestURI最后的一个/后面的部分 + * + * @return + */ + public String getRequstURILastPath() { + if (requestURI == null) return ""; + return requestURI.substring(requestURI.lastIndexOf('/') + 1); + } + + public String getRequestURI() { + return requestURI; + } + + public long getContentLength() { + return contentLength; + } + + public String getContentType() { + return contentType; + } + + //------------------------------------------------------------------------------ + public String getHeader(String name) { + return header.getValue(name); + } + + public T getJsonHeader(Class clazz, String name) { + String v = getHeader(name); + return v == null || v.isEmpty() ? null : convert.convertFrom(clazz, v); + } + + public boolean getBooleanHeader(String name, boolean defaultValue) { + return header.getBoolValue(name, defaultValue); + } + + public short getShortHeader(String name, short defaultValue) { + return header.getShortValue(name, defaultValue); + } + + public int getIntHeader(String name, int defaultValue) { + return header.getIntValue(name, defaultValue); + } + + public long getLongHeader(String name, long defaultValue) { + return header.getLongValue(name, defaultValue); + } + + public float getFloatHeader(String name, float defaultValue) { + return header.getFloatValue(name, defaultValue); + } + + public double getDoubleHeader(String name, double defaultValue) { + return header.getDoubleValue(name, defaultValue); + } + + public String getHeader(String name, String defaultValue) { + return header.getValue(name, defaultValue); + } + + //------------------------------------------------------------------------------ + public String getParameter(String name) { + parseBody(); + return params.getValue(name); + } + + public T getJsonParameter(Class clazz, String name) { + String v = getParameter(name); + return v == null || v.isEmpty() ? null : convert.convertFrom(clazz, v); + } + + public boolean getBooleanParameter(String name, boolean defaultValue) { + parseBody(); + return params.getBoolValue(name, defaultValue); + } + + public short getShortParameter(String name, short defaultValue) { + parseBody(); + return params.getShortValue(name, defaultValue); + } + + public int getIntParameter(String name, int defaultValue) { + parseBody(); + return params.getIntValue(name, defaultValue); + } + + public long getLongParameter(String name, long defaultValue) { + parseBody(); + return params.getLongValue(name, defaultValue); + } + + public float getFloatParameter(String name, float defaultValue) { + parseBody(); + return params.getFloatValue(name, defaultValue); + } + + public double getDoubleParameter(String name, double defaultValue) { + parseBody(); + return params.getDoubleValue(name, defaultValue); + } + + public String getParameter(String name, String defaultValue) { + parseBody(); + return params.getValue(name, defaultValue); + } +} diff --git a/src/com/wentch/redkale/net/http/HttpResourceServlet.java b/src/com/wentch/redkale/net/http/HttpResourceServlet.java new file mode 100644 index 000000000..d8e8a610d --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpResourceServlet.java @@ -0,0 +1,328 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.AnyValue; +import java.io.*; +import java.nio.*; +import java.nio.channels.*; +import java.nio.file.*; +import static java.nio.file.StandardWatchEventKinds.*; +import java.util.*; +import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import java.util.function.*; +import java.util.logging.*; +import java.util.regex.*; + +/** + * + * @author zhangjx + */ +public final class HttpResourceServlet extends HttpServlet { + + private static final Logger logger = Logger.getLogger(HttpResourceServlet.class.getSimpleName()); + + //缓存总大小, 默认128M + protected long cachelimit = 128 * 1024 * 1024L; + + protected final LongAdder cachedLength = new LongAdder(); + + //最大可缓存的文件大小, 大于该值的文件将不被缓存 + protected long cachelengthmax = 1 * 1024 * 1024; + + protected File root = new File("./root/"); + + protected final ConcurrentHashMap files = new ConcurrentHashMap<>(); + + protected WatchService watcher; + + protected final ConcurrentHashMap keymaps = new ConcurrentHashMap<>(); + + protected SimpleEntry[] locationRewrites; + + protected Thread watchThread; + + protected Predicate ranges; + + @Override + public void init(Context context, AnyValue config) { + if (config != null) { + String rootstr = config.getValue("webroot", "root").trim(); + if (rootstr.indexOf(':') < 0 && rootstr.indexOf('/') != 0 && System.getProperty("APP_HOME") != null) { + rootstr = new File(System.getProperty("APP_HOME"), rootstr).getPath(); + } + String rangesValue = config.getValue("ranges"); + this.ranges = rangesValue != null ? Pattern.compile(rangesValue).asPredicate() : null; + try { + this.root = new File(rootstr).getCanonicalFile(); + } catch (IOException ioe) { + this.root = new File(rootstr); + } + AnyValue cacheconf = config.getAnyValue("caches"); + if (cacheconf != null) { + this.cachelimit = parseLenth(cacheconf.getValue("limit"), 128 * 1024 * 1024L); + this.cachelengthmax = parseLenth(cacheconf.getValue("lengthmax"), 1 * 1024 * 1024L); + } + List> locations = new ArrayList<>(); + for (AnyValue av : config.getAnyValues("rewrite")) { + if ("location".equals(av.getValue("type"))) { + String m = av.getValue("match"); + String f = av.getValue("forward"); + if (m != null && f != null) { + locations.add(new SimpleEntry<>(Pattern.compile(m), f)); + } + } + } + this.locationRewrites = locations.isEmpty() ? null : locations.toArray(new SimpleEntry[locations.size()]); + } + if (this.cachelimit < 1) return; + if (this.root != null) { + try { + this.watcher = this.root.toPath().getFileSystem().newWatchService(); + this.watchThread = new Thread() { + + @Override + public void run() { + try { + final String rootstr = root.getCanonicalPath(); + while (!this.isInterrupted()) { + final WatchKey key = watcher.take(); + final Path parent = keymaps.get(key); + if (parent == null) { + key.cancel(); + continue; + } + key.pollEvents().stream().forEach((event) -> { + try { + Path path = parent.resolve((Path) event.context()); + final String uri = path.toString().substring(rootstr.length()).replace('\\', '/'); + //logger.log(Level.FINEST, "file(" + uri + ") happen " + event.kind() + " event"); + Thread.sleep(1000L); //等待update file完毕 + if (event.kind() == ENTRY_DELETE) { + files.remove(uri); + } else if (event.kind() == ENTRY_MODIFY) { + FileEntry en = files.get(uri); + if (en != null) en.update(); + } + } catch (Exception ex) { + logger.log(Level.FINE, event.context() + " occur erroneous", ex); + } + }); + key.reset(); + } + } catch (Exception e) { + } + } + }; + this.watchThread.setName("Servlet-ResourceWatch-Thread"); + this.watchThread.setDaemon(true); + this.watchThread.start(); + } catch (IOException ex) { + logger.log(Level.WARNING, HttpResourceServlet.class.getSimpleName() + " start watch-thread error", ex); + } + } + } + + @Override + public void destroy(Context context, AnyValue config) { + if (this.watcher != null) { + try { + this.watcher.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + if (this.watchThread != null && this.watchThread.isAlive()) { + this.watchThread.interrupt(); + } + } + + private static long parseLenth(String value, long defValue) { + if (value == null) return defValue; + value = value.toUpperCase().replace("B", ""); + if (value.endsWith("G")) return Long.decode(value.replace("G", "")) * 1024 * 1024 * 1024; + if (value.endsWith("M")) return Long.decode(value.replace("M", "")) * 1024 * 1024; + if (value.endsWith("K")) return Long.decode(value.replace("K", "")) * 1024; + return Long.decode(value); + } + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + String uri = request.getRequestURI(); + if (locationRewrites != null) { + for (SimpleEntry entry : locationRewrites) { + Matcher matcher = entry.getKey().matcher(uri); + if (matcher.find()) { + StringBuffer sb = new StringBuffer(uri.length()); + matcher.appendReplacement(sb, entry.getValue()); + matcher.appendTail(sb); + uri = sb.toString(); + break; + } + } + } + if (uri.length() == 0 || uri.equals("/")) uri = "/index.html"; + //System.out.println(request); + response.skipHeader(); + FileEntry entry = watcher == null ? createFileEntry(uri) : files.get(uri); + if (entry == null) { + entry = createFileEntry(uri); + if (entry != null && watcher != null) files.put(uri, entry); + } + if (entry == null) { + response.finish404(); + } else { + entry.send(request, response); + } + } + + private FileEntry createFileEntry(String uri) { + File file = new File(root, uri); + if (!file.isFile() || !file.canRead()) return null; + FileEntry en = new FileEntry(this, uri, file); + if (watcher == null) return en; + try { + Path p = file.getParentFile().toPath(); + keymaps.put(p.register(watcher, ENTRY_MODIFY, ENTRY_DELETE), p); + } catch (IOException e) { + logger.log(Level.INFO, HttpResourceServlet.class.getSimpleName() + " create FileEntry(" + uri + ") erroneous", e); + } + return en; + } + + private static final class FileEntry { + + private final LongAdder counter = new LongAdder(); + + private final File file; + + private final HttpResourceServlet servlet; + + private final String uri; + + private String mimeType; + + private long length; + + private String etag; + + private byte[] header; + + private ByteBuffer content; + + public FileEntry(final HttpResourceServlet servlet, String uri, File file) { + this.servlet = servlet; + this.uri = uri; + this.file = file; + this.mimeType = MimeType.getByFilename(file.getName()); + if (this.mimeType == null) this.mimeType = "application/octet-stream"; + update(); + } + + public void update() { + this.length = file.length(); + this.etag = file.lastModified() + "-" + this.length; + this.header = ("HTTP/1.1 200 OK\r\nContent-Type:" + mimeType + "\r\nETag:" + etag + (servlet.ranges != null && servlet.ranges.test(uri) ? "\r\nAccept-Ranges:bytes" : "") + "\r\nContent-Length:" + length + "\r\n\r\n").getBytes(); + if (this.content != null) { + this.servlet.cachedLength.add(0L - this.content.remaining()); + this.content = null; + } + if (this.length > this.servlet.cachelengthmax) return; + if (this.servlet.cachedLength.longValue() + this.length > this.servlet.cachelimit) return; + try { + FileInputStream in = new FileInputStream(file); + ByteArrayOutputStream out = new ByteArrayOutputStream((int) file.length()); + byte[] bytes = new byte[10240]; + int pos; + while ((pos = in.read(bytes)) != -1) { + out.write(bytes, 0, pos); + } + in.close(); + ByteBuffer buf = ByteBuffer.allocateDirect((int) (this.header.length + file.length())); + buf.put(this.header); + buf.put(out.toByteArray()); + buf.flip(); + this.content = buf.asReadOnlyBuffer(); + this.servlet.cachedLength.add(this.content.remaining()); + } catch (Exception e) { + logger.log(Level.INFO, HttpResourceServlet.class.getSimpleName() + " update FileEntry(" + file + ") erroneous", e); + } + } + + @Override + protected void finalize() throws Throwable { + if (this.content != null) this.servlet.cachedLength.add(0L - this.content.remaining()); + super.finalize(); + } + + public long getCachedLength() { + return this.content == null ? 0L : this.content.remaining(); + } + + public void send(HttpRequest request, final HttpResponse response) throws IOException { + counter.increment(); + final String match = request.getHeader("If-None-Match"); + if (match != null && this.etag.equals(match)) { + response.finish304(); + return; + } + final boolean acceptRange = (servlet.ranges != null && servlet.ranges.test(request.getRequestURI())); + String range = acceptRange ? request.getHeader("Range") : null; + if (acceptRange) { + String ifRange = request.getHeader("If-Range"); + if (ifRange != null && !this.etag.equals(ifRange)) range = null; + } + if (content != null && range == null) { + response.finish(content.duplicate()); + return; + } + final HttpContext context = response.getContext(); + final ByteBuffer buffer = context.pollBuffer(); + if (range != null && (!range.startsWith("bytes=") || range.indexOf(',') >= 0)) range = null; + if (range == null) { + buffer.put(header); + buffer.flip(); + response.send(buffer, file); + return; + } + range = range.substring("bytes=".length()); + int pos = range.indexOf('-'); + final long start = pos == 0 ? 0 : Integer.parseInt(range.substring(0, pos)); + final long end = (pos == range.length() - 1) ? -1 : Long.parseLong(range.substring(pos + 1)); + long clen = end > 0 ? (end - start + 1) : (file.length() - start); + buffer.put(("HTTP/1.1 206 Partial Content\r\nContent-Type:" + mimeType + "\r\nAccept-Ranges:bytes\r\nContent-Range:bytes " + start + "-" + (end > 0 ? end : length - 1) + "/" + length + "\r\nContent-Length:" + clen + "\r\n\r\n").getBytes()); + buffer.flip(); + final ByteBuffer body = this.content; + if (body == null) { + response.send(buffer, file, start, end > 0 ? clen : end); + } else { + final ByteBuffer body2 = body.duplicate(); + body2.position((int) (this.header.length + start)); + if (end > 0) body2.limit((int) (body2.position() + end - start + 1)); + response.send(buffer, buffer, new CompletionHandler() { + + @Override + public void completed(Integer result, ByteBuffer attachment) { + response.getContext().offerBuffer(attachment); + response.finish(body2); + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + if (attachment.limit() != attachment.capacity()) { + response.getContext().offerBuffer(attachment); + } + response.finish(true); + } + }); + } + + } + } +} diff --git a/src/com/wentch/redkale/net/http/HttpResponse.java b/src/com/wentch/redkale/net/http/HttpResponse.java new file mode 100644 index 000000000..12047ddfb --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpResponse.java @@ -0,0 +1,437 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.*; +import com.wentch.redkale.util.AnyValue.DefaultAnyValue; +import com.wentch.redkale.util.AnyValue.Entry; +import com.wentch.redkale.util.*; +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.nio.*; +import java.nio.channels.*; +import java.nio.file.*; +import java.text.*; +import java.util.*; + +/** + * + * @author zhangjx + */ +public final class HttpResponse extends Response { + + private static final ByteBuffer buffer304 = ByteBuffer.wrap("HTTP/1.1 304 Not Modified\r\n\r\n".getBytes()).asReadOnlyBuffer(); + + private static final ByteBuffer buffer404 = ByteBuffer.wrap("HTTP/1.1 404 Not Found\r\nContent-Length:0\r\n\r\n".getBytes()).asReadOnlyBuffer(); + + protected static final byte[] LINE = new byte[]{'\r', '\n'}; + + private static final Set options = new HashSet<>(); + + private static final DateFormat GMT_DATE_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss z", Locale.ENGLISH); + + private static final Map httpCodes = new HashMap<>(); + + static { + options.add(StandardOpenOption.READ); + GMT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + + httpCodes.put(100, "Continue"); + httpCodes.put(101, "Switching Protocols"); + + httpCodes.put(200, "OK"); + httpCodes.put(201, "Created"); + httpCodes.put(202, "Accepted"); + httpCodes.put(203, "Non-Authoritative Information"); + httpCodes.put(204, "No Content"); + httpCodes.put(205, "Reset Content"); + httpCodes.put(206, "Partial Content"); + + httpCodes.put(300, "Multiple Choices"); + httpCodes.put(301, "Moved Permanently"); + httpCodes.put(302, "Found"); + httpCodes.put(303, "See Other"); + httpCodes.put(304, "Not Modified"); + httpCodes.put(305, "Use Proxy"); + httpCodes.put(307, "Temporary Redirect"); + + httpCodes.put(400, "Bad Request"); + httpCodes.put(401, "Unauthorized"); + httpCodes.put(402, "Payment Required"); + httpCodes.put(403, "Forbidden"); + httpCodes.put(404, "Not Found"); + httpCodes.put(405, "Method Not Allowed"); + httpCodes.put(406, "Not Acceptable"); + httpCodes.put(407, "Proxy Authentication Required"); + httpCodes.put(408, "Request Timeout"); + httpCodes.put(409, "Conflict"); + httpCodes.put(410, "Gone"); + httpCodes.put(411, "Length Required"); + httpCodes.put(412, "Precondition Failed"); + httpCodes.put(413, "Request Entity Too Large"); + httpCodes.put(414, "Request URI Too Long"); + httpCodes.put(415, "Unsupported Media Type"); + httpCodes.put(416, "Requested Range Not Satisfiable"); + httpCodes.put(417, "Expectation Failed"); + + httpCodes.put(500, "Internal Server Error"); + httpCodes.put(501, "Not Implemented"); + httpCodes.put(502, "Bad Gateway"); + httpCodes.put(503, "Service Unavailable"); + httpCodes.put(504, "Gateway Timeout"); + httpCodes.put(505, "HTTP Version Not Supported"); + } + + private int status = 200; + + private String contentType = "text/plain; charset=utf-8"; + + private long contentLength = -1; + + private HttpCookie[] cookies; + + private boolean headsended = false; + + private final DefaultAnyValue header = new DefaultAnyValue(); + + protected HttpResponse(HttpContext context, HttpRequest request) { + super(context, request); + } + + @Override + protected AsyncConnection removeChannel() { + return super.removeChannel(); + } + + @Override + protected void recycle() { + this.status = 200; + this.contentLength = -1; + this.contentType = null; + this.cookies = null; + this.headsended = false; + this.header.clear(); + super.recycle(); + } + + protected String getHttpCode(int status) { + return httpCodes.get(status); + } + + protected String getHttpCode(int status, String defValue) { + String v = httpCodes.get(status); + return v == null ? defValue : v; + } + + @Override + public HttpContext getContext() { + return (HttpContext) context; + } + + public void addCookie(HttpCookie... cookies) { + if (this.cookies == null) { + this.cookies = cookies; + } else { + HttpCookie[] news = new HttpCookie[this.cookies.length + cookies.length]; + System.arraycopy(this.cookies, 0, news, 0, this.cookies.length); + System.arraycopy(cookies, 0, news, this.cookies.length, cookies.length); + this.cookies = news; + } + } + + public void sendJson(Object obj) { + this.contentType = "text/plain; charset=utf-8"; + sendString(request.convert.convertTo(obj)); + } + + public void sendJson(Type type, Object obj) { + this.contentType = "text/plain; charset=utf-8"; + sendString(request.convert.convertTo(type, obj)); + } + + public void sendJson(Object... objs) { + this.contentType = "text/plain; charset=utf-8"; + sendString(request.convert.convertTo(objs)); + } + + public void sendString(String obj) { + if (obj == null) obj = "null"; + if (context.getCharset() == null) { + final char[] chars = Utility.charArray(obj); + this.contentLength = Utility.encodeUTF8Length(chars); + final ByteBuffer headbuf = createHeader(); + ByteBuffer buf2 = Utility.encodeUTF8(headbuf, (int) this.contentLength, chars); + headbuf.flip(); + if (buf2 == null) { + super.send(headbuf, headbuf, finishHandler); + } else { + super.send(headbuf, buf2, new AsyncWriteHandler<>(this.context, headbuf, this.channel, buf2, buf2, finishHandler)); + } + } else { + ByteBuffer buffer = context.getCharset().encode(obj); + this.contentLength = buffer.remaining(); + send(buffer, buffer, finishHandler); + } + } + + public void finish(int status, String message) { + this.status = status; + if (status != 200) super.refuseAlive(); + if (message == null || message.isEmpty()) { + ByteBuffer headbuf = createHeader(); + headbuf.flip(); + super.send(headbuf, headbuf, finishHandler); + } else { + sendString(message); + } + } + + public void finish304() { + finish(buffer304.duplicate()); + } + + public void finish404() { + finish(buffer404.duplicate()); + } + + @Override + public void send(ByteBuffer buffer, A attachment, CompletionHandler handler) { + if (!this.headsended) { + ByteBuffer headbuf = createHeader(); + headbuf.flip(); + if (buffer == null) { + super.send(headbuf, attachment, handler); + } else { + super.send(headbuf, attachment, new AsyncWriteHandler<>(this.context, headbuf, this.channel, buffer, attachment, handler)); + } + } else { + super.send(buffer, attachment, handler); + } + } + + public void send(File file) throws IOException { + send(file, null); + } + + protected void send(final File file, final ByteBuffer fileBody) throws IOException { + if (file == null || !file.isFile() || !file.canRead()) { + finish404(); + return; + } + final long length = file.length(); + final String match = request.getHeader("If-None-Match"); + if (match != null && (file.lastModified() + "-" + length).equals(match)) { + finish304(); + return; + } + this.contentLength = file.length(); + if (this.contentType == null) this.contentType = MimeType.getByFilename(file.getName()); + if (this.contentType == null) this.contentType = "application/octet-stream"; + String range = request.getHeader("Range"); + if (range != null && (!range.startsWith("bytes=") || range.indexOf(',') >= 0)) range = null; + long start = -1; + long len = -1; + if (range != null) { + range = range.substring("bytes=".length()); + int pos = range.indexOf('-'); + start = pos == 0 ? 0 : Integer.parseInt(range.substring(0, pos)); + long end = (pos == range.length() - 1) ? -1 : Long.parseLong(range.substring(pos + 1)); + long clen = end > 0 ? (end - start + 1) : (file.length() - start); + this.status = 206; + addHeader("Accept-Ranges", "bytes"); + addHeader("Content-Range", "bytes " + start + "-" + (end > 0 ? end : length - 1) + "/" + length); + this.contentLength = clen; + len = end > 0 ? clen : end; + } + ByteBuffer buffer = createHeader(); + buffer.flip(); + if (fileBody == null) { + send(buffer, file, start, len); + } else { + final ByteBuffer body = fileBody.duplicate().asReadOnlyBuffer(); + if (start >= 0) { + body.position((int) start); + if (len > 0) body.limit((int) (body.position() + len)); + } + send(buffer, buffer, new CompletionHandler() { + + @Override + public void completed(Integer result, ByteBuffer attachment) { + context.offerBuffer(attachment); + finish(body); + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + if (attachment.limit() != attachment.capacity()) { + context.offerBuffer(attachment); + } + finish(true); + } + }); + } + } + + protected void send(ByteBuffer buffer, File file) throws IOException { + send(buffer, file, -1L, -1L); + } + + protected void send(ByteBuffer buffer, File file, long offset, long length) throws IOException { + send(buffer, buffer, new TransferFileHandler(AsynchronousFileChannel.open(file.toPath(), options, ((HttpContext) context).getExecutor()), offset, length)); + } + + private ByteBuffer createHeader() { + this.headsended = true; + ByteBuffer buffer = this.context.pollBuffer(); + buffer.put(("HTTP/1.1 " + this.status + " " + (this.status == 200 ? "OK" : httpCodes.get(this.status)) + "\r\n").getBytes()); + + buffer.put(("Content-Type: " + (this.contentType == null ? "text/plain; charset=utf-8" : this.contentType) + "\r\n").getBytes()); + + if (this.contentLength > 0) { + buffer.put(("Content-Length: " + this.contentLength + "\r\n").getBytes()); + } + if (!this.request.isKeepAlive()) { + buffer.put("Connection: close\r\n".getBytes()); + } + for (Entry en : this.header.getStringEntrys()) { + buffer.put((en.name + ": " + en.getValue() + "\r\n").getBytes()); + } + if (request.newsessionid != null) { + if (request.newsessionid.isEmpty()) { + buffer.put(("Set-Cookie: " + HttpRequest.SESSIONID_NAME + "=; path=/; Max-Age=0; HttpOnly\r\n").getBytes()); + } else { + buffer.put(("Set-Cookie: " + HttpRequest.SESSIONID_NAME + "=" + request.newsessionid + "; path=/; HttpOnly\r\n").getBytes()); + } + } + if (this.cookies != null) { + for (HttpCookie cookie : this.cookies) { + if (cookie == null) continue; + buffer.put(("Set-Cookie: " + genString(cookie) + "\r\n").getBytes()); + } + } + buffer.put(LINE); + return buffer; + } + + private CharSequence genString(HttpCookie cookie) { + StringBuilder sb = new StringBuilder(); + sb.append(cookie.getName()).append("=\"").append(cookie.getValue()).append('"').append("; Version=1"); + if (cookie.getPath() != null) sb.append("; Path=").append(cookie.getPath()); + if (cookie.getDomain() != null) sb.append("; Domain=").append(cookie.getDomain()); + if (cookie.getPortlist() != null) sb.append("; Port=").append(cookie.getPortlist()); + if (cookie.getMaxAge() > 0) { + sb.append("; Max-Age=").append(cookie.getMaxAge()); + synchronized (GMT_DATE_FORMAT) { + sb.append("; Expires=").append(GMT_DATE_FORMAT.format(new Date(System.currentTimeMillis() + cookie.getMaxAge() * 1000))); + } + } + if (cookie.getSecure()) sb.append("; Secure"); + if (cookie.isHttpOnly()) sb.append("; HttpOnly"); + return sb; + } + + public void skipHeader() { + this.headsended = true; + } + + public void setHeader(String name, Object value) { + this.header.setValue(name, String.valueOf(value)); + } + + public void addHeader(String name, Object value) { + this.header.addValue(name, String.valueOf(value)); + } + + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + return this.status; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public long getContentLength() { + return contentLength; + } + + public void setContentLength(long contentLength) { + this.contentLength = contentLength; + } + + protected final class TransferFileHandler implements CompletionHandler { + + private final AsynchronousFileChannel filechannel; + + private final long max; //需要读取的字节数, -1表示读到文件结尾 + + private long count;//读取文件的字节数 + + private long position = 0; + + private boolean next = false; + + private boolean read = true; + + public TransferFileHandler(AsynchronousFileChannel channel) { + this.filechannel = channel; + this.max = -1; + } + + public TransferFileHandler(AsynchronousFileChannel channel, long offset, long len) { + this.filechannel = channel; + this.position = offset <= 0 ? 0 : offset; + this.max = len; + } + + @Override + public void completed(Integer result, ByteBuffer attachment) { + if (result < 0 || (max > 0 && count >= max)) { + failed(null, attachment); + return; + } + if (read) { + read = false; + if (next) { + position += result; + } else { + next = true; + } + attachment.clear(); + filechannel.read(attachment, position, attachment, this); + } else { + read = true; + if (max > 0) { + count += result; + if (count > max) { + attachment.limit((int) (attachment.position() + max - count)); + } + } + attachment.flip(); + send(attachment, attachment, this); + } + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + getContext().offerBuffer(attachment); + finish(true); + try { + filechannel.close(); + } catch (IOException e) { + } + } + + } +} diff --git a/src/com/wentch/redkale/net/http/HttpServer.java b/src/com/wentch/redkale/net/http/HttpServer.java new file mode 100644 index 000000000..37629c145 --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpServer.java @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.net.Server; +import com.wentch.redkale.net.ResponsePool; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.net.BufferPool; +import com.wentch.redkale.watch.WatchFactory; +import java.util.*; +import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.atomic.*; + +/** + * + * @author zhangjx + */ +public final class HttpServer extends Server { + + private final Map, String[]> servlets = new HashMap<>(); + + private String contextPath; + + public HttpServer() { + this(System.currentTimeMillis(), null); + } + + public HttpServer(long serverStartTime, final WatchFactory watch) { + super(serverStartTime, "TCP", watch); + } + + @Override + public void init(AnyValue config) throws Exception { + super.init(config); + AnyValue conf = config == null ? null : config.getAnyValue("servlets"); + this.contextPath = conf == null ? "" : conf.getValue("prefix", ""); + } + + public void addHttpServlet(HttpServlet servlet, AnyValue conf, String... mappings) { + this.servlets.put(new SimpleEntry<>(servlet, conf), mappings); + } + + @Override + @SuppressWarnings("unchecked") + protected Context createContext() { + final int port = this.address.getPort(); + AtomicLong createBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("HTTP_" + port + ".Buffer.creatCounter"); + AtomicLong cycleBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("HTTP_" + port + ".Buffer.cycleCounter"); + BufferPool bufferPool = new BufferPool(createBufferCounter, cycleBufferCounter, Math.max(this.capacity, 8 * 1024), this.bufferPoolSize); + HttpPrepareServlet prepare = new HttpPrepareServlet(); + this.servlets.entrySet().stream().forEach((en) -> { + prepare.addHttpServlet(en.getKey().getKey(), en.getKey().getValue(), en.getValue()); + }); + this.servlets.clear(); + AtomicLong createResponseCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("HTTP_" + port + ".Response.creatCounter"); + AtomicLong cycleResponseCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("HTTP_" + port + ".Response.cycleCounter"); + HttpContext httpcontext = new HttpContext(this.serverStartTime, this.logger, executor, bufferPool, + new ResponsePool(createResponseCounter, cycleResponseCounter, this.responsePoolSize), + this.maxbody, this.charset, this.address, prepare, this.watch, this.readTimeoutSecond, this.writeTimeoutSecond, contextPath); + httpcontext.getResponsePool().setResponseFactory(() -> new HttpResponse(httpcontext, new HttpRequest(httpcontext, httpcontext.jsonFactory))); + return httpcontext; + } + +} diff --git a/src/com/wentch/redkale/net/http/HttpServlet.java b/src/com/wentch/redkale/net/http/HttpServlet.java new file mode 100644 index 000000000..7c440b26b --- /dev/null +++ b/src/com/wentch/redkale/net/http/HttpServlet.java @@ -0,0 +1,26 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.Servlet; + +/** + * + * @author zhangjx + */ +public abstract class HttpServlet implements Servlet { + + @Override + public final boolean equals(Object obj) { + return obj != null && obj.getClass() == this.getClass(); + } + + @Override + public final int hashCode() { + return this.getClass().hashCode(); + } + +} diff --git a/src/com/wentch/redkale/net/http/MimeType.java b/src/com/wentch/redkale/net/http/MimeType.java new file mode 100644 index 000000000..3bb971ed0 --- /dev/null +++ b/src/com/wentch/redkale/net/http/MimeType.java @@ -0,0 +1,255 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.util.*; + +/** + * + * @author zhangjx + */ +public class MimeType { + + private final static Map contentTypes = new HashMap<>(); + + static { + contentTypes.put("abs", "audio/x-mpeg"); + contentTypes.put("ai", "application/postscript"); + contentTypes.put("aif", "audio/x-aiff"); + contentTypes.put("aifc", "audio/x-aiff"); + contentTypes.put("aiff", "audio/x-aiff"); + contentTypes.put("aim", "application/x-aim"); + contentTypes.put("art", "image/x-jg"); + contentTypes.put("asf", "video/x-ms-asf"); + contentTypes.put("asx", "video/x-ms-asf"); + contentTypes.put("au", "audio/basic"); + contentTypes.put("avi", "video/x-msvideo"); + contentTypes.put("avx", "video/x-rad-screenplay"); + contentTypes.put("bcpio", "application/x-bcpio"); + contentTypes.put("bin", "application/octet-stream"); + contentTypes.put("bmp", "image/bmp"); + contentTypes.put("body", "text/html"); + contentTypes.put("cdf", "application/x-cdf"); + contentTypes.put("cer", "application/x-x509-ca-cert"); + contentTypes.put("class", "application/java"); + contentTypes.put("cpio", "application/x-cpio"); + contentTypes.put("csh", "application/x-csh"); + contentTypes.put("css", "text/css"); + contentTypes.put("dib", "image/bmp"); + contentTypes.put("doc", "application/msword"); + contentTypes.put("dtd", "application/xml-dtd"); + contentTypes.put("dv", "video/x-dv"); + contentTypes.put("dvi", "application/x-dvi"); + contentTypes.put("eps", "application/postscript"); + contentTypes.put("etx", "text/x-setext"); + contentTypes.put("exe", "application/octet-stream"); + contentTypes.put("gif", "image/gif"); + contentTypes.put("gk", "application/octet-stream"); + contentTypes.put("gtar", "application/x-gtar"); + contentTypes.put("gz", "application/x-gzip"); + contentTypes.put("hdf", "application/x-hdf"); + contentTypes.put("hqx", "application/mac-binhex40"); + contentTypes.put("htc", "text/x-component"); + contentTypes.put("htm", "text/html"); + contentTypes.put("html", "text/html"); + contentTypes.put("hqx", "application/mac-binhex40"); + contentTypes.put("ief", "image/ief"); + contentTypes.put("jad", "text/vnd.sun.j2me.app-descriptor"); + contentTypes.put("jar", "application/java-archive"); + contentTypes.put("java", "text/plain"); + contentTypes.put("jnlp", "application/x-java-jnlp-file"); + contentTypes.put("jpe", "image/jpeg"); + contentTypes.put("jpeg", "image/jpeg"); + contentTypes.put("jpg", "image/jpeg"); + contentTypes.put("js", "text/javascript"); + contentTypes.put("kar", "audio/x-midi"); + contentTypes.put("latex", "application/x-latex"); + contentTypes.put("log", "text/plain"); + contentTypes.put("m3u", "audio/x-mpegurl"); + contentTypes.put("mac", "image/x-macpaint"); + contentTypes.put("man", "application/x-troff-man"); + contentTypes.put("mathml", "application/mathml+xml"); + contentTypes.put("me", "application/x-troff-me"); + contentTypes.put("mid", "audio/x-midi"); + contentTypes.put("midi", "audio/x-midi"); + contentTypes.put("mif", "application/x-mif"); + contentTypes.put("mov", "video/quicktime"); + contentTypes.put("movie", "video/x-sgi-movie"); + contentTypes.put("mp1", "audio/x-mpeg"); + contentTypes.put("mp2", "audio/x-mpeg"); + contentTypes.put("mp3", "audio/x-mpeg"); + contentTypes.put("mpa", "audio/x-mpeg"); + contentTypes.put("mp4", "video/mp4"); + contentTypes.put("ogv", "video/ogv"); + contentTypes.put("webm", "video/webm"); + contentTypes.put("flv", "video/x-flv"); + contentTypes.put("mpe", "video/mpeg"); + contentTypes.put("mpeg", "video/mpeg"); + contentTypes.put("mpega", "audio/x-mpeg"); + contentTypes.put("mpg", "video/mpeg"); + contentTypes.put("mpv2", "video/mpeg2"); + contentTypes.put("ms", "application/x-wais-source"); + contentTypes.put("nc", "application/x-netcdf"); + contentTypes.put("oda", "application/oda"); + contentTypes.put("ogg", "application/ogg"); + contentTypes.put("out", "text/plain"); + contentTypes.put("pbm", "image/x-portable-bitmap"); + contentTypes.put("pct", "image/pict"); + contentTypes.put("pdf", "application/pdf"); + contentTypes.put("pgm", "image/x-portable-graymap"); + contentTypes.put("pic", "image/pict"); + contentTypes.put("pict", "image/pict"); + contentTypes.put("pls", "audio/x-scpls"); + contentTypes.put("png", "image/png"); + contentTypes.put("pnm", "image/x-portable-anymap"); + contentTypes.put("pnt", "image/x-macpaint"); + contentTypes.put("ppm", "image/x-portable-pixmap"); + contentTypes.put("ppt", "application/powerpoint"); + contentTypes.put("ps", "application/postscript"); + contentTypes.put("psd", "image/x-photoshop"); + contentTypes.put("qt", "video/quicktime"); + contentTypes.put("qti", "image/x-quicktime"); + contentTypes.put("qtif", "image/x-quicktime"); + contentTypes.put("ras", "image/x-cmu-raster"); + contentTypes.put("rdf", "application/rdf+xml"); + contentTypes.put("rgb", "image/x-rgb"); + contentTypes.put("rm", "application/vnd.rn-realmedia"); + contentTypes.put("roff", "application/x-troff"); + contentTypes.put("rtf", "application/rtf"); + contentTypes.put("rtx", "text/richtext"); + contentTypes.put("sh", "application/x-sh"); + contentTypes.put("shar", "application/x-shar"); + contentTypes.put("shtml", "text/x-server-parsed-html"); + contentTypes.put("sit", "application/x-stuffit"); + contentTypes.put("smf", "audio/x-midi"); + contentTypes.put("snd", "audio/basic"); + contentTypes.put("src", "application/x-wais-source"); + contentTypes.put("sv4cpio", "application/x-sv4cpio"); + contentTypes.put("sv4crc", "application/x-sv4crc"); + contentTypes.put("svg", "image/svg+xml"); + contentTypes.put("svgz", "image/svg+xml"); + contentTypes.put("swf", "application/x-shockwave-flash"); + contentTypes.put("t", "application/x-troff"); + contentTypes.put("tar", "application/x-tar"); + contentTypes.put("tcl", "application/x-tcl"); + contentTypes.put("tex", "application/x-tex"); + contentTypes.put("texi", "application/x-texinfo"); + contentTypes.put("texinfo", "application/x-texinfo"); + contentTypes.put("tif", "image/tiff"); + contentTypes.put("tiff", "image/tiff"); + contentTypes.put("tr", "application/x-troff"); + contentTypes.put("tsv", "text/tab-separated-values"); + contentTypes.put("txt", "text/plain"); + contentTypes.put("ulw", "audio/basic"); + contentTypes.put("ustar", "application/x-ustar"); + contentTypes.put("xbm", "image/x-xbitmap"); + contentTypes.put("xml", "application/xml"); + contentTypes.put("xpm", "image/x-xpixmap"); + contentTypes.put("xsl", "application/xml"); + contentTypes.put("xslt", "application/xslt+xml"); + contentTypes.put("xwd", "image/x-xwindowdump"); + contentTypes.put("vsd", "application/x-visio"); + contentTypes.put("vxml", "application/voicexml+xml"); + contentTypes.put("wav", "audio/x-wav"); + contentTypes.put("wbmp", "image/vnd.wap.wbmp"); + contentTypes.put("wml", "text/vnd.wap.wml"); + contentTypes.put("wmlc", "application/vnd.wap.wmlc"); + contentTypes.put("wmls", "text/vnd.wap.wmls"); + contentTypes.put("wmlscriptc", "application/vnd.wap.wmlscriptc"); + contentTypes.put("wrl", "x-world/x-vrml"); + contentTypes.put("xht", "application/xhtml+xml"); + contentTypes.put("xhtml", "application/xhtml+xml"); + contentTypes.put("xls", "application/vnd.ms-excel"); + contentTypes.put("xul", "application/vnd.mozilla.xul+xml"); + contentTypes.put("Z", "application/x-compress"); + contentTypes.put("z", "application/x-compress"); + contentTypes.put("zip", "application/zip"); + } + + /** + * @param extension the extension + * + * @return the content type associated with extension. If no association is found, this method will return text/plain + */ + public static String get(String extension) { + return get(extension, "text/plain"); + } + + /** + * @param extension the extension + * @param defaultCt the content type to return if there is no known association for the specified extension + * + * @return the content type associated with extension or if no associate is found, returns defaultCt + */ + public static String get(String extension, String defaultCt) { + final String mime = contentTypes.get(extension); + return mime == null ? defaultCt : mime; + } + + /** + * @param extension the extension + * + * @return true if the specified extension has been registered otherwise, returns false + */ + public static boolean contains(String extension) { + return contentTypes.containsKey(extension); + } + + /** + *

+ * Associates the specified extension and content type

+ * + * @param extension the extension + * @param contentType the content type associated with the extension + */ + public static void add(String extension, String contentType) { + if (extension != null && extension.length() != 0 + && contentType != null && contentType.length() != 0) { + + contentTypes.put(extension, contentType); + } + } + + /** + * @param fileName the filename + * + * @return the content type associated with extension of the given filename or if no associate is found, returns null + */ + public static String getByFilename(String fileName) { + String extn = getExtension(fileName); + if (extn != null) { + return get(extn); + } else { + // no extension, no content type + return null; + } + } + + /** + * Get extension of file, without fragment id + */ + private static String getExtension(String fileName) { + // play it safe and get rid of any fragment id + // that might be there + int length = fileName.length(); + + int newEnd = fileName.lastIndexOf('#'); + if (newEnd == -1) { + newEnd = length; + } + // Instead of creating a new string. + // if (i != -1) { + // fileName = fileName.substring(0, i); + // } + int i = fileName.lastIndexOf('.', newEnd); + if (i != -1) { + return fileName.substring(i + 1, newEnd); + } else { + // no extension, no content type + return null; + } + } +} diff --git a/src/com/wentch/redkale/net/http/MultiContext.java b/src/com/wentch/redkale/net/http/MultiContext.java new file mode 100644 index 000000000..8eacb9096 --- /dev/null +++ b/src/com/wentch/redkale/net/http/MultiContext.java @@ -0,0 +1,264 @@ +/* + * To change this license header, choose License Headers input Project Properties. + * To change this template file, choose Tools | Templates + * and open the template input the editor. + */ +package com.wentch.redkale.net.http; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.*; +import java.util.concurrent.atomic.*; +import java.util.logging.*; +import java.util.regex.Pattern; + +/** + * + * @author zhangjx + */ +public final class MultiContext { + + private static final Logger logger = Logger.getLogger(MultiContext.class.getSimpleName()); + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private final String contentType; + + private final InputStream in; + + private final Charset charset; + + private final String boundary; + + private final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + private final Map parameters = new HashMap<>(); + + private final Pattern fielnamePattern; + + private static final Iterable emptyIterable = () -> new Iterator() { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public MultiPart next() { + return null; + } + }; + + public MultiContext(final String contentType, final InputStream in) { + this(null, contentType, in); + } + + public MultiContext(final Charset charsetName, final String contentType, final InputStream in) { + this(charsetName, contentType, in, null); + } + + public MultiContext(final String contentType, final InputStream in, String extregex) { + this(null, contentType, in, extregex); + } + + public MultiContext(final Charset charsetName, final String contentType, final InputStream in, String fielnameRegex) { + this.charset = charsetName == null ? UTF8 : charsetName; + this.contentType = contentType.trim(); + this.boundary = parseBoundary(this.contentType); + this.in = in instanceof BufferedInputStream ? in : new BufferedInputStream(in); + this.fielnamePattern = fielnameRegex == null || fielnameRegex.isEmpty() ? null : Pattern.compile(fielnameRegex); + } + + public Map getParameters() { + return parameters; + } + + public String getParameter(String name) { + return getParameters().get(name); + } + + public final String getParameter(String name, String defaultValue) { + String value = this.getParameter(name); + return value == null ? defaultValue : value; + } + + public final int getIntParameter(String name, int defaultValue) { + String value = this.getParameter(name); + try { + return value == null ? defaultValue : Integer.decode(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public final long getLongParameter(String name, long defaultValue) { + String value = this.getParameter(name); + try { + return value == null ? defaultValue : Long.decode(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private String parseBoundary(String contentType) { + if (!contentType.startsWith("multipart/")) { + return null; + } + for (String str : contentType.split(";")) { + int pos = str.indexOf("boundary="); + if (pos >= 0) return str.substring(pos + "boundary=".length()).trim(); + } + return null; + } + + public boolean isMultipart() { + return this.boundary != null; + } + + public Iterable listMultiPart() throws IOException { + if (!isMultipart()) return emptyIterable; + final boolean debug = false; + final String boundarystr = "--" + this.boundary; + final Pattern fielnameReg = this.fielnamePattern; + final String endboundary = boundarystr + "--"; + final byte[] boundarray = ("\n" + boundarystr).getBytes(); + final byte[] buffer = new byte[boundarray.length]; + final InputStream input = this.in; + final Map params = this.parameters; + final AtomicBoolean finaled = new AtomicBoolean(false); + return () -> new Iterator() { + + private String boundaryline; + + private MultiPart lastentry; + + @Override + public boolean hasNext() { + try { + if (lastentry != null) { + lastentry.skip(); + if (finaled.get()) return false; + } + if (boundaryline == null) boundaryline = readLine(); + //if (debug) System.out.print("boundaryline=" + boundaryline + " "); + if (endboundary.equals(boundaryline) || !boundarystr.equals(boundaryline)) { //结尾或异常 + lastentry = null; + return false; + } + final String disposition = readLine(); + //if (debug) System.out.println("disposition=" + disposition); + if (disposition.contains("; filename=\"")) { //是上传文件 + String contentType = readLine(); + //if (debug) System.out.println("file.contentType=" + contentType); + contentType = contentType.substring(contentType.indexOf(':') + 1).trim(); + readLine(); //读掉空白行 + String name = parseValue(disposition, "name"); + String filename = parseValue(disposition, "filename"); + if (filename == null || filename.isEmpty()) { //没有上传 + readLine(); //读掉空白行 + this.boundaryline = null; + this.lastentry = null; + return this.hasNext(); + } else { + int p1 = filename.lastIndexOf('/'); + if (p1 < 0) p1 = filename.lastIndexOf('\\'); + if (p1 >= 0) filename = filename.substring(p1 + 1); + } + final AtomicLong counter = new AtomicLong(0); + InputStream source = new InputStream() { + + private int bufposition = buffer.length; + + private boolean end; + + @Override + public int read() throws IOException { + if (end) return -1; + final byte[] buf = buffer; + int ch = (this.bufposition < buf.length) ? (buf[this.bufposition++] & 0xff) : input.read(); + if ((ch == '\r' && readBuffer())) return -1; + counter.incrementAndGet(); + return ch; + } + + private boolean readBuffer() throws IOException { + final byte[] buf = buffer; + final int pos = this.bufposition; + int s = 0; + for (int i = pos; i < buf.length; i++) { + buf[s++] = buf[i]; + } + int readed = 0; + while ((readed += input.read(buf, s + readed, pos - readed)) != pos); + this.bufposition = 0; + if (Arrays.equals(boundarray, buf)) { + this.end = true; + int c1 = input.read(); + int c2 = input.read(); + finaled.set(c1 == '-' && c2 == '-'); + return true; + } + return false; + } + + @Override + public long skip(long count) throws IOException { + if (end) return -1; + if (count <= 0) return 0; + long s = 0; + while (read() != -1) { + s++; + if (--count <= 0) break; + } + return s; + } + }; + this.lastentry = new MultiPart(filename, name, contentType, counter, source); + if (fielnameReg != null && !fielnameReg.matcher(filename).matches()) { + return this.hasNext(); + } + return true; + } else { //不是文件 + readLine(); //读掉空白 + params.put(parseValue(disposition, "name"), readLine()); + this.boundaryline = null; + this.lastentry = null; + return this.hasNext(); + } + } catch (IOException ex) { + logger.log(Level.FINER, "listMultiPart abort", ex); + return false; + } + } + + @Override + public MultiPart next() { + return lastentry; + } + + }; + } + + private String readLine() throws IOException { + int lasted = '\r'; + buf.reset(); + for (;;) { + int b = in.read(); + if (b == -1 || (lasted == '\r' && b == '\n')) break; + if (lasted != '\r') buf.write(lasted); + lasted = b; + } + if (buf.size() == 0) return ""; + return buf.toString(this.charset.name()).trim(); + } + + private static String parseValue(final String str, String name) { + if (str == null) return null; + final String key = "; " + name + "=\""; + int pos = str.indexOf(key); + if (pos < 0) return null; + String sub = str.substring(pos + key.length()); + return sub.substring(0, sub.indexOf('"')); + } + +} diff --git a/src/com/wentch/redkale/net/http/MultiPart.java b/src/com/wentch/redkale/net/http/MultiPart.java new file mode 100644 index 000000000..fd2f40c03 --- /dev/null +++ b/src/com/wentch/redkale/net/http/MultiPart.java @@ -0,0 +1,115 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.io.*; +import java.util.concurrent.atomic.AtomicLong; + +/** + * + * @author zhangjx + */ +public final class MultiPart { + + private final String filename; + + private final String name; + + private final String contentType; + + private final InputStream in; + + private final AtomicLong received; + + MultiPart(String filename, String name, String contentType, AtomicLong received, InputStream in) { + this.filename = filename; + this.name = name; + this.in = in; + this.contentType = contentType; + this.received = received; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{" + "name=" + name + ", filename=" + filename + ", contentType=" + contentType + ", received=" + received + '}'; + } + + public boolean save(File file) throws IOException { + return save(Long.MAX_VALUE, file); + } + + public boolean save(long max, File file) throws IOException { + OutputStream out = new FileOutputStream(file); + boolean rs = save(max, out); + out.close(); + return rs; + } + + public byte[] getContentBytes() throws IOException { + return getContentBytes(Long.MAX_VALUE); + } + + /** + * 将文件流读进bytes, 如果超出max指定的值则返回null + * + * @param max 最大长度限制 + * @return + * @throws IOException + */ + public byte[] getContentBytes(long max) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + return save(max, out) ? out.toByteArray() : null; + } + + public boolean save(OutputStream out) throws IOException { + return save(Long.MAX_VALUE, out); + } + + /** + * 将文件流写进out, 如果超出max指定的值则中断并返回false + * + * @param max 最大长度限制 + * @param out + * @return + * @throws IOException + */ + public boolean save(long max, OutputStream out) throws IOException { + byte[] bytes = new byte[4096]; + int pos; + InputStream in0 = this.getInputStream(); + while ((pos = in0.read(bytes)) != -1) { + if (max < 0) return false; + out.write(bytes, 0, pos); + max -= pos; + } + return true; + } + + public String getContentType() { + return contentType; + } + + public String getFilename() { + return filename; + } + + public String getName() { + return name; + } + + public InputStream getInputStream() { + return in; + } + + public long getReceived() { + return received.get(); + } + + public void skip() throws IOException { + in.skip(Long.MAX_VALUE); + } + +} diff --git a/src/com/wentch/redkale/net/http/WebAction.java b/src/com/wentch/redkale/net/http/WebAction.java new file mode 100644 index 000000000..f497c973a --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebAction.java @@ -0,0 +1,22 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.lang.annotation.*; + +/** + * + * @author zhangjx + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebAction { + + int actionid() default 0; + + String url(); +} diff --git a/src/com/wentch/redkale/net/http/WebServlet.java b/src/com/wentch/redkale/net/http/WebServlet.java new file mode 100644 index 000000000..f60950177 --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebServlet.java @@ -0,0 +1,26 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.lang.annotation.*; + +/** + * + * @author zhangjx + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebServlet { + + String name() default ""; + + boolean fillurl() default true; + + String[] value() default {}; + + int moduleid() default 0; +} diff --git a/src/com/wentch/redkale/net/http/WebSocket.java b/src/com/wentch/redkale/net/http/WebSocket.java new file mode 100644 index 000000000..fa583c92f --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebSocket.java @@ -0,0 +1,120 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * + * @author zhangjx + */ +public abstract class WebSocket { + + WebSocketRunner runner; + + WebSocketEngine engine; + + WebSocketGroup group; + + String sessionid; + + long groupid; + + private final Map attributes = new ConcurrentHashMap<>(); + + protected WebSocket() { + } + + //---------------------------------------------------------------- + public final void send(WebSocketPacket packet) { + if (this.runner != null) this.runner.sendMessage(packet); + } + + public final void close() { + if (this.runner != null) this.runner.closeRunner(); + } + + public final void send(String text) { + send(text, true); + } + + private void send(String text, boolean last) { + send(new WebSocketPacket(text, last)); + } + + @SuppressWarnings("unchecked") + public final T getAttribute(String name) { + return (T) attributes.get(name); + } + + public final void removeAttribute(String name) { + attributes.remove(name); + } + + public final void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + public final long getGroupid() { + return groupid; + } + + public final String getSessionid() { + return sessionid; + } + + //------------------------------------------------------------------- + protected final WebSocketGroup getWebSocketGroup() { + return group; + } + + protected final WebSocketGroup getWebSocketGroup(long groupid) { + return engine.getWebSocketGroup(groupid); + } + + //------------------------------------------------------------------- + + /** + * 返回sessionid, null表示连接不合法或异常 + * + * @param request + * @return + */ + public String onOpen(final HttpRequest request) { + return request.getSessionid(false); + } + + /** + * 返回GroupID, 负数表示异常 + * + * @return + */ + public long createGroupid() { + return 0; + } + + /** + * WebSocket流程顺序: onOpen、createGroupid、onConnected、onMessage/onFragment+、onClose + */ + public void onConnected() { + } + + public void onMessage(String text) { + } + + public void onMessage(byte[] bytes) { + } + + public void onFragment(String text, boolean last) { + } + + public void onFragment(byte[] bytes, boolean last) { + } + + public void onClose(int code, String reason) { + } +} diff --git a/src/com/wentch/redkale/net/http/WebSocketEngine.java b/src/com/wentch/redkale/net/http/WebSocketEngine.java new file mode 100644 index 000000000..24fa32e6d --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebSocketEngine.java @@ -0,0 +1,44 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.util.*; +import java.util.concurrent.*; + +/** + * + * @author zhangjx + */ +public final class WebSocketEngine { + + private final Map containers = new ConcurrentHashMap<>(); + + WebSocketEngine() { + } + + void add(WebSocket socket) { + WebSocketGroup group = containers.get(socket.groupid); + if (group == null) { + group = new WebSocketGroup(socket.groupid); + containers.put(socket.groupid, group); + } + group.add(socket); + } + + void remove(WebSocket socket) { + WebSocketGroup group = containers.get(socket.groupid); + if (group == null) return; + group.remove(socket); + if (group.isEmpty()) containers.remove(socket.groupid); + } + + public WebSocketGroup getWebSocketGroup(long groupid) { + return containers.get(groupid); + } + + void close() { + } +} diff --git a/src/com/wentch/redkale/net/http/WebSocketGroup.java b/src/com/wentch/redkale/net/http/WebSocketGroup.java new file mode 100644 index 000000000..bbf849726 --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebSocketGroup.java @@ -0,0 +1,61 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; + +/** + * + * @author zhangjx + */ +public final class WebSocketGroup { + + private final long groupid; + + private final List list = new CopyOnWriteArrayList<>(); + + private final Map attributes = new HashMap<>(); + + WebSocketGroup(long groupid) { + this.groupid = groupid; + } + + public long getGroupid() { + return groupid; + } + + public Stream getWebSockets() { + return list.stream(); + } + + void remove(WebSocket socket) { + list.remove(socket); + } + + void add(WebSocket socket) { + socket.group = this; + list.add(socket); + } + + public final boolean isEmpty() { + return list.isEmpty(); + } + + @SuppressWarnings("unchecked") + public final T getAttribute(String name) { + return (T) attributes.get(name); + } + + public final void removeAttribute(String name) { + attributes.remove(name); + } + + public final void setAttribute(String name, Object value) { + attributes.put(name, value); + } +} diff --git a/src/com/wentch/redkale/net/http/WebSocketPacket.java b/src/com/wentch/redkale/net/http/WebSocketPacket.java new file mode 100644 index 000000000..b1e799cec --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebSocketPacket.java @@ -0,0 +1,102 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.util.Utility; + +/** + * + * @author zhangjx + */ +public final class WebSocketPacket { + + public static enum PacketType { + + TEXT(0x01), BINARY(0x02), CLOSE(0x08), PING(0x09), PONG(0x0A); + + private final int value; + + private PacketType(int v) { + this.value = v; + } + + public int getValue() { + return value; + } + + public static PacketType valueOf(int v) { + switch (v) { + case 0x01: return TEXT; + case 0x02: return BINARY; + case 0x08: return CLOSE; + case 0x09: return PING; + case 0x0A: return PONG; + default: return null; + } + } + } + + protected PacketType type; + + protected String payload; + + protected byte[] bytes; + + protected boolean last = true; + + public WebSocketPacket() { + } + + public WebSocketPacket(String payload) { + this(payload, true); + } + + public WebSocketPacket(String payload, boolean fin) { + this.type = PacketType.TEXT; + this.payload = payload; + this.last = fin; + } + + public WebSocketPacket(byte[] data) { + this(PacketType.BINARY, data, true); + } + + public WebSocketPacket(byte[] data, boolean fin) { + this(PacketType.BINARY, data, fin); + } + + public WebSocketPacket(PacketType type, byte[] data) { + this(type, data, true); + } + + public WebSocketPacket(PacketType type, byte[] data, boolean fin) { + this.type = type; + if (type == PacketType.TEXT) { + this.payload = new String(Utility.decodeUTF8(data)); + } else { + this.bytes = data; + } + this.last = fin; + } + + public byte[] getContent() { + if (this.type == PacketType.TEXT) return Utility.encodeUTF8(getPayload()); + return this.bytes; + } + + public String getPayload() { + return payload; + } + + public byte[] getBytes() { + return bytes; + } + + public boolean isLast() { + return last; + } + +} diff --git a/src/com/wentch/redkale/net/http/WebSocketRunner.java b/src/com/wentch/redkale/net/http/WebSocketRunner.java new file mode 100644 index 000000000..c76fcb5c5 --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebSocketRunner.java @@ -0,0 +1,405 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.AsyncConnection; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.net.http.WebSocketPacket.PacketType; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.security.SecureRandom; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public class WebSocketRunner implements Runnable { + + private final WebSocketEngine engine; + + private final AsyncConnection channel; + + private final WebSocket webSocket; + + protected final Context context; + + private ByteBuffer readBuffer; + + private ByteBuffer writeBuffer; + + protected boolean closed = false; + + private AtomicBoolean writing = new AtomicBoolean(); + + private final Coder coder = new Coder(); + + private final BlockingQueue queue = new ArrayBlockingQueue(1024); + + public WebSocketRunner(Context context, WebSocket webSocket, AsyncConnection channel) { + this.context = context; + this.engine = webSocket.engine; + this.webSocket = webSocket; + this.channel = channel; + webSocket.runner = this; + this.coder.logger = context.getLogger(); + this.coder.debugable = false; + this.readBuffer = context.pollBuffer(); + this.writeBuffer = context.pollBuffer(); + } + + @Override + public void run() { + final boolean debug = this.coder.debugable; + try { + webSocket.onConnected(); + channel.setReadTimeoutSecond(300); //读取超时5分钟 + if (channel.isOpen()) { + channel.read(readBuffer, null, new CompletionHandler() { + @Override + public void completed(Integer count, Void attachment1) { + if (count < 1) { + closeRunner(); + return; + } + if (readBuffer == null) return; + readBuffer.flip(); + try { + WebSocketPacket packet = coder.decode(readBuffer); + if (packet == null) { + failed(null, attachment1); + return; + } + if (readBuffer != null) { + readBuffer.clear(); + channel.read(readBuffer, null, this); + } + if (packet.type == PacketType.TEXT) { + webSocket.onMessage(packet.getPayload()); + } else if (packet.type == PacketType.BINARY) { + webSocket.onMessage(packet.getBytes()); + } + } catch (Exception e) { + closeRunner(); + if (debug) context.getLogger().log(Level.FINEST, "WebSocketRunner abort on read WebSocketPacket, force to close channel", e); + } + } + + @Override + public void failed(Throwable exc, Void attachment2) { + closeRunner(); + if (exc != null) { + context.getLogger().log(Level.FINEST, "WebSocketRunner read WebSocketPacket failed, force to close channel", exc); + } + } + }); + } else { + closeRunner(); + } + } catch (Exception e) { + context.getLogger().log(Level.FINEST, "WebSocketRunner abort on read bytes from channel, force to close channel", e); + closeRunner(); + } + } + + public void sendMessage(WebSocketPacket packet) { + if (packet == null || closed) return; + + //System.out.println("推送消息"); + final byte[] bytes = coder.encode(packet); + if (writing.getAndSet(true)) { + queue.add(bytes); + return; + } + final boolean debug = this.coder.debugable; + if (writeBuffer == null) return; + writeBuffer.clear(); + writeBuffer.put(bytes); + writeBuffer.flip(); + try { + channel.write(writeBuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + if (writeBuffer == null || closed) return; + try { + if (writeBuffer.hasRemaining()) { + if (debug) context.getLogger().log(Level.FINEST, "WebSocketRunner write completed reemaining: " + writeBuffer.remaining()); + channel.write(writeBuffer, attachment, this); + return; + } + byte[] bs = queue.poll(); + if (bs != null && writeBuffer != null) { + writeBuffer.clear(); + writeBuffer.put(bytes); + writeBuffer.flip(); + channel.write(writeBuffer, null, this); + return; + } + } catch (NullPointerException e) { + } catch (Exception e) { + context.getLogger().log(Level.WARNING, "WebSocket sendMessage abort on rewrite, force to close channel", e); + closeRunner(); + } + writing.set(false); + } + + @Override + public void failed(Throwable exc, Void attachment) { + writing.set(false); + closeRunner(); + if (exc != null) { + context.getLogger().log(Level.FINE, "WebSocket sendMessage on CompletionHandler failed, force to close channel", exc); + } + } + }); + } catch (Exception t) { + writing.set(false); + context.getLogger().log(Level.FINE, "WebSocket sendMessage abort, force to close channel", t); + } + } + + public void closeRunner() { + if (closed) return; + closed = true; + try { + channel.close(); + } catch (Throwable t) { + } + context.offerBuffer(readBuffer); + context.offerBuffer(writeBuffer); + readBuffer = null; + writeBuffer = null; + engine.remove(webSocket); + webSocket.onClose(0, null); + } + + private static final class Masker { + + public static final int MASK_SIZE = 4; + + private ByteBuffer buffer; + + private byte[] mask; + + private int index = 0; + + public Masker(ByteBuffer buffer) { + this.buffer = buffer; + } + + public Masker() { + generateMask(); + } + + public byte get() { + return buffer.get(); + } + + public byte[] get(final int size) { + byte[] bytes = new byte[size]; + buffer.get(bytes); + return bytes; + } + + public byte unmask() { + final byte b = get(); + return mask == null ? b : (byte) (b ^ mask[index++ % MASK_SIZE]); + } + + public byte[] unmask(int count) { + byte[] bytes = get(count); + if (mask != null) { + for (int i = 0; i < bytes.length; i++) { + bytes[i] ^= mask[index++ % MASK_SIZE]; + } + } + + return bytes; + } + + public void generateMask() { + mask = new byte[MASK_SIZE]; + new SecureRandom().nextBytes(mask); + } + + public void mask(byte[] bytes, int location, byte b) { + bytes[location] = mask == null ? b : (byte) (b ^ mask[index++ % MASK_SIZE]); + } + + public void mask(byte[] target, int location, byte[] bytes) { + if (bytes != null && target != null) { + for (int i = 0; i < bytes.length; i++) { + target[location + i] = mask == null + ? bytes[i] + : (byte) (bytes[i] ^ mask[index++ % MASK_SIZE]); + } + } + } + + public byte[] maskAndPrepend(byte[] packet) { + byte[] masked = new byte[packet.length + MASK_SIZE]; + System.arraycopy(getMask(), 0, masked, 0, MASK_SIZE); + mask(masked, MASK_SIZE, packet); + return masked; + } + + public void setBuffer(ByteBuffer buffer) { + this.buffer = buffer; + } + + public byte[] getMask() { + return mask; + } + + public void readMask() { + mask = get(MASK_SIZE); + } + } + + private static final class Coder { + + protected byte inFragmentedType; + + protected byte outFragmentedType; + + protected final boolean maskData = false; + + protected boolean processingFragment; + + private boolean debugable; + + private Logger logger; + + public WebSocketPacket decode(final ByteBuffer buffer) { + final boolean debug = this.debugable; + if (debug) logger.log(Level.FINEST, "read web socket message's length = " + buffer.remaining()); + if (buffer.remaining() < 2) return null; + byte opcode = buffer.get(); + final boolean last = (opcode & 0b1000000) != 0; + if (false && (opcode & 0b01110000) != 0) { //暂时不校验 + if (debug) logger.log(Level.FINE, "rsv1 rsv2 rsv3 must be 0, but not (" + opcode + ")"); + return null; //rsv1 rsv2 rsv3 must be 0 + } + //0x00 表示一个后续帧 + //0x01 表示一个文本帧 + //0x02 表示一个二进制帧 + //0x03-07 为以后的非控制帧保留 + //0x8 表示一个连接关闭 + //0x9 表示一个ping + //0xA 表示一个pong + //0x0B-0F 为以后的控制帧保留 + final boolean control = (opcode & 0x08) == 0x08; //是否控制帧 + //final boolean continuation = opcode == 0; + PacketType type = PacketType.valueOf(opcode & 0xf); + if (type == PacketType.CLOSE) { + if (debug) logger.log(Level.FINEST, " receive close command from websocket client"); + return null; + } + byte lengthCode = buffer.get(); + final Masker masker = new Masker(buffer); + final boolean masked = (lengthCode & 0x80) == 0x80; + if (masked) lengthCode ^= 0x80; //mask + int length; + if (lengthCode <= 125) { + length = lengthCode; + } else { + if (control) { + if (debug) logger.log(Level.FINE, " receive control command from websocket client"); + return null; + } + + final int lengthBytes = lengthCode == 126 ? 2 : 8; + if (buffer.remaining() < lengthBytes) { + if (debug) logger.log(Level.FINE, " read illegal message length from websocket, expect " + lengthBytes + " but " + buffer.remaining()); + return null; + } + length = toInt(masker.unmask(lengthBytes)); + } + if (masked) { + if (buffer.remaining() < Masker.MASK_SIZE) { + if (debug) logger.log(Level.FINE, " read illegal masker length from websocket, expect " + Masker.MASK_SIZE + " but " + buffer.remaining()); + return null; + } + masker.readMask(); + } + if (buffer.remaining() < length) { + if (debug) logger.log(Level.FINE, " read illegal remaining length from websocket, expect " + length + " but " + buffer.remaining()); + return null; + } + final byte[] data = masker.unmask(length); + if (data.length != length) { + if (debug) logger.log(Level.FINE, " read illegal unmask length from websocket, expect " + length + " but " + data.length); + return null; + } + return new WebSocketPacket(type, data, last); + } + + public byte[] encode(WebSocketPacket frame) { + byte opcode = (byte) (frame.type.getValue() | 0x80); + final byte[] bytes = frame.getContent(); + final byte[] lengthBytes = encodeLength(bytes.length); + + int length = 1 + lengthBytes.length + bytes.length + (maskData ? Masker.MASK_SIZE : 0); + int payloadStart = 1 + lengthBytes.length + (maskData ? Masker.MASK_SIZE : 0); + final byte[] packet = new byte[length]; + packet[0] = opcode; + System.arraycopy(lengthBytes, 0, packet, 1, lengthBytes.length); + if (maskData) { + Masker masker = new Masker(); + packet[1] |= 0x80; + masker.mask(packet, payloadStart, bytes); + System.arraycopy(masker.getMask(), 0, packet, payloadStart - Masker.MASK_SIZE, Masker.MASK_SIZE); + } else { + System.arraycopy(bytes, 0, packet, payloadStart, bytes.length); + } + return packet; + } + + private static byte[] encodeLength(final long length) { + byte[] lengthBytes; + if (length <= 125) { + lengthBytes = new byte[1]; + lengthBytes[0] = (byte) length; + } else { + byte[] b = toArray(length); + if (length <= 0xFFFF) { + lengthBytes = new byte[3]; + lengthBytes[0] = 126; + System.arraycopy(b, 6, lengthBytes, 1, 2); + } else { + lengthBytes = new byte[9]; + lengthBytes[0] = 127; + System.arraycopy(b, 0, lengthBytes, 1, 8); + } + } + return lengthBytes; + } + + private static byte[] toArray(long length) { + long value = length; + byte[] b = new byte[8]; + for (int i = 7; i >= 0 && value > 0; i--) { + b[i] = (byte) (value & 0xFF); + value >>= 8; + } + return b; + } + + private static int toInt(byte[] bytes) { + int value = 0; + for (int i = 0; i < bytes.length; i++) { + value <<= 8; + value ^= (int) bytes[i] & 0xFF; + } + return value; + } + + } + +} diff --git a/src/com/wentch/redkale/net/http/WebSocketServlet.java b/src/com/wentch/redkale/net/http/WebSocketServlet.java new file mode 100644 index 000000000..9aa5b5934 --- /dev/null +++ b/src/com/wentch/redkale/net/http/WebSocketServlet.java @@ -0,0 +1,103 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.http; + +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.AnyValue; +import java.io.*; +import java.nio.channels.*; +import java.security.*; +import java.util.*; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public abstract class WebSocketServlet extends HttpServlet { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + private final MessageDigest digest = getMessageDigest(); + + private static MessageDigest getMessageDigest() { + try { + return MessageDigest.getInstance("SHA-1"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected final WebSocketEngine engine = new WebSocketEngine(); + + @Override + public void destroy(Context context, AnyValue conf) { + super.destroy(context, conf); + engine.close(); + } + + @Override + public final void execute(HttpRequest request, HttpResponse response) throws IOException { + final boolean debug = logger.isLoggable(Level.FINER); + if (!"GET".equalsIgnoreCase(request.getMethod()) + || !request.getConnection().contains("Upgrade") + || !"websocket".equalsIgnoreCase(request.getHeader("Upgrade"))) { + if (debug) logger.finer("WebSocket connect abort, (Not GET Method) or (Connection != Upgrade) or (Upgrade != websocket). request=" + request); + response.finish(true); + return; + } + String key = request.getHeader("Sec-WebSocket-Key"); + if (key == null) { + if (debug) logger.finer("WebSocket connect abort, Not found Sec-WebSocket-Key header. request=" + request); + response.finish(true); + return; + } + final WebSocket webSocket = this.createWebSocket(); + webSocket.engine = engine; + String sessionid = webSocket.onOpen(request); + if (sessionid == null) { + if (debug) logger.finer("WebSocket connect abort, Not found sessionid. request=" + request); + response.finish(true); + return; + } + webSocket.sessionid = sessionid; + request.setKeepAlive(true); + byte[] bytes = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes(); + synchronized (digest) { + bytes = digest.digest(bytes); + } + key = Base64.getEncoder().encodeToString(bytes); + response.setStatus(101); + response.setHeader("Connection", "Upgrade"); + response.addHeader("Upgrade", "websocket"); + response.addHeader("Sec-WebSocket-Accept", key); + response.send(null, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + HttpContext context = response.getContext(); + long groupid = webSocket.createGroupid(); + if (groupid < 0) { + if (debug) logger.finer("WebSocket connect abort, Create groupid abort"); + response.finish(true); + return; + } + webSocket.groupid = groupid; + engine.add(webSocket); + context.submit(new WebSocketRunner(context, webSocket, response.removeChannel())); + response.finish(true); + } + + @Override + public void failed(Throwable exc, Void attachment) { + logger.log(Level.FINER, "WebSocket connect abort, Response send abort", exc); + response.finish(true); + } + }); + } + + protected abstract WebSocket createWebSocket(); +} diff --git a/src/com/wentch/redkale/net/sncp/ServiceEntry.java b/src/com/wentch/redkale/net/sncp/ServiceEntry.java new file mode 100644 index 000000000..b44fcec5b --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/ServiceEntry.java @@ -0,0 +1,83 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.service.Service; +import com.wentch.redkale.util.AnyValue; +import java.util.*; + +/** + * + * @author zhangjx + */ +public final class ServiceEntry { + + private final Class serviceClass; + + private final Service service; + + private final AnyValue conf; + + private final List names = new ArrayList<>(); + + public ServiceEntry(Class serviceClass, Service service, AnyValue conf, String name) { + this.serviceClass = serviceClass == null ? service.getClass() : serviceClass; + this.service = service; + this.conf = conf; + this.names.add(name); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof ServiceEntry)) return false; + ServiceEntry other = (ServiceEntry) obj; + if (this.serviceClass != other.serviceClass) return false; + return (this.service == other.service); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 97 * hash + Objects.hashCode(this.serviceClass); + hash = 97 * hash + Objects.hashCode(this.service); + return hash; + } + + public Class getServiceClass() { + return serviceClass; + } + + public Service getService() { + return service; + } + + public AnyValue getServiceConf() { + return conf; + } + + public void initService() { + service.init(conf); + } + + public void destroyService() { + service.destroy(conf); + } + + public List getNames() { + return names; + } + + public void addName(String name) { + this.names.add(name); + } + + public boolean containsName(String name) { + return names.contains(name); + } + +} diff --git a/src/com/wentch/redkale/net/sncp/Sncp.java b/src/com/wentch/redkale/net/sncp/Sncp.java new file mode 100644 index 000000000..c4aa123ad --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/Sncp.java @@ -0,0 +1,313 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.service.Service; +import com.wentch.redkale.service.RemoteOn; +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.util.Utility; +import com.wentch.redkale.util.DebugMethodVisitor; +import com.wentch.redkale.util.TwoLong; +import com.wentch.redkale.convert.bson.BsonConvert; +import com.wentch.redkale.net.Transport; +import com.wentch.redkale.net.sncp.SncpClient.SncpAction; +import jdk.internal.org.objectweb.asm.*; +import static jdk.internal.org.objectweb.asm.Opcodes.*; + +/** + * + * @author zhangjx + */ +public abstract class Sncp { + + private static final byte[] hashes = new byte[255]; + + static { + //0-9:48-57 A-Z:65-90 a-z:97-122 $:36 _:95 + byte index = 0; + hashes['_'] = index++; + hashes['$'] = index++; + for (int i = '0'; i <= '9'; i++) { + hashes[i] = index++; + } + for (int i = 'A'; i <= 'Z'; i++) { + hashes[i] = index++; + } + for (int i = 'a'; i <= 'z'; i++) { + hashes[i] = index++; + } + } + + private Sncp() { + } + + public static long hash(final Class clazz) { + if (clazz == null) return Long.MIN_VALUE; + long rs = hash(clazz.getSimpleName()); + return (rs < Integer.MAX_VALUE) ? rs | 0xF00000000L : rs; + } + + public static TwoLong hash(final java.lang.reflect.Method method) { + if (method == null) return new TwoLong(-1L, -1L); + long rs1 = hash(method.getName()); + if (rs1 < Integer.MAX_VALUE) { + rs1 |= (method.getParameterCount() + 0L) << 32; + } + rs1 = (rs1 < Integer.MAX_VALUE) ? rs1 | 0xF00000000L : rs1; + long rs2 = hash(wrapName(method), true); + if (rs2 < Integer.MAX_VALUE) { + rs2 |= (method.getParameterCount() + 0L) << 32; + } + rs2 = (rs2 < Integer.MAX_VALUE) ? rs2 | 0xF00000000L : rs2; + return new TwoLong(rs1, rs2); + } + + private static String wrapName(final java.lang.reflect.Method method) { + final Class[] params = method.getParameterTypes(); + if (params.length == 0) return method.getName() + "00"; + int c = 0; + for (Class clzz : params) { + c |= clzz.getSimpleName().charAt(0); + } + return method.getName() + hashes[0xff & params.length] + hashes[0xff & c]; + } + + public static long hash(final String name) { + return hash(name, false); + } + + public static long hash(final String name, boolean reverse) { + if (name == null) return Long.MIN_VALUE; + if (name.isEmpty()) return 0; + char[] chars = Utility.charArray(name); + long rs = 0L; + if (reverse) { + int start = Math.max(chars.length - 10, 0); + for (int i = chars.length - 1; i >= start; i--) { + rs = (rs << 6) | hashes[0xff & chars[i]]; + } + } else { + int end = Math.min(chars.length, 11); + for (int i = 0; i < end; i++) { + rs = (rs << 6) | hashes[0xff & chars[i]]; + } + } + return Math.abs(rs); + } + + + /* + * public final class DynRemoteTestService extends TestService{ + * + * @Resource + * private BsonConvert convert; + * + * @Resource(name="xxxx") + * private Transport transport; + * + * public SncpClient client; + * + * @Override + * public boolean testChange(TestBean bean) { + * return client.remote(convert, transport, 0, bean); + * } + * + * @Override + * public TestBean findTestBean(long id) { + * return client.remote(convert, transport, 1, id); + * } + * + * @Override + * public void runTestBean(long id, TestBean bean) { + * return client.remote(convert, transport, 2, id, bean); + * } + */ + /** + * + * @param + * @param serviceName + * @param remote + * @param serviceClass + * @return + */ + @SuppressWarnings("unchecked") + public static T createRemoteService(final String serviceName, final Class serviceClass, final String remote) { + if (serviceClass == null) return null; + if (!Service.class.isAssignableFrom(serviceClass)) return null; + int mod = serviceClass.getModifiers(); + if (!java.lang.reflect.Modifier.isPublic(mod)) return null; + if (java.lang.reflect.Modifier.isAbstract(mod)) return null; + final String supDynName = serviceClass.getName().replace('.', '/'); + final String clientName = SncpClient.class.getName().replace('.', '/'); + final String clientDesc = Type.getDescriptor(SncpClient.class); + final String convertDesc = Type.getDescriptor(BsonConvert.class); + final String transportDesc = Type.getDescriptor(Transport.class); + final String anyValueDesc = Type.getDescriptor(AnyValue.class); + ClassLoader loader = Sncp.class.getClassLoader(); + String newDynName = supDynName.substring(0, supDynName.lastIndexOf('/') + 1) + "DynRemote" + serviceClass.getSimpleName(); + try { + return (T) Class.forName(newDynName.replace('/', '.')).newInstance(); + } catch (Exception ex) { + } + final SncpClient client = new SncpClient(serviceName, serviceClass); + //------------------------------------------------------------------------------ + ClassWriter cw = new ClassWriter(0); + FieldVisitor fv; + DebugMethodVisitor mv; + AnnotationVisitor av0; + + cw.visit(V1_8, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, newDynName, null, supDynName, null); + { + av0 = cw.visitAnnotation(Type.getDescriptor(RemoteOn.class), true); + av0.visitEnd(); + } + { + fv = cw.visitField(ACC_PRIVATE, "convert", convertDesc, null, null); + av0 = fv.visitAnnotation("Ljavax/annotation/Resource;", true); + av0.visitEnd(); + fv.visitEnd(); + } + { + fv = cw.visitField(ACC_PRIVATE, "transport", transportDesc, null, null); + av0 = fv.visitAnnotation("Ljavax/annotation/Resource;", true); + av0.visit("name", remote == null ? "" : remote); + av0.visitEnd(); + fv.visitEnd(); + } + { + fv = cw.visitField(ACC_PUBLIC, "client", clientDesc, null, null); + fv.visitEnd(); + } + { //构造函数 + mv = new DebugMethodVisitor(cw.visitMethod(ACC_PUBLIC, "", "()V", null, null)); + //mv.setDebug(true); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, supDynName, "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + { //init + mv = new DebugMethodVisitor(cw.visitMethod(ACC_PUBLIC, "init", "(" + anyValueDesc + ")V", null, null)); + mv.visitInsn(RETURN); + mv.visitMaxs(0, 2); + mv.visitEnd(); + } + { //destroy + mv = new DebugMethodVisitor(cw.visitMethod(ACC_PUBLIC, "destroy", "(" + anyValueDesc + ")V", null, null)); + mv.visitInsn(RETURN); + mv.visitMaxs(0, 2); + mv.visitEnd(); + } + int i = -1; + for (final SncpAction entry : client.actions) { + final int index = ++i; + final java.lang.reflect.Method method = entry.method; + { + mv = new DebugMethodVisitor(cw.visitMethod(ACC_PUBLIC, method.getName(), Type.getMethodDescriptor(method), null, null)); + //mv.setDebug(true); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "client", clientDesc); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "convert", convertDesc); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "transport", transportDesc); + if (index <= 5) { + mv.visitInsn(ICONST_0 + index); + } else { + mv.visitIntInsn(BIPUSH, index); + } + + { //传参数 + int paramlen = entry.paramTypes.length; + if (paramlen <= 5) { + mv.visitInsn(ICONST_0 + paramlen); + } else { + mv.visitIntInsn(BIPUSH, paramlen); + } + mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); + java.lang.reflect.Type[] paramtypes = entry.paramTypes; + int insn = 0; + for (int j = 0; j < paramtypes.length; j++) { + final java.lang.reflect.Type pt = paramtypes[j]; + mv.visitInsn(DUP); + insn++; + if (j <= 5) { + mv.visitInsn(ICONST_0 + j); + } else { + mv.visitIntInsn(BIPUSH, j); + } + if (pt instanceof Class && ((Class) pt).isPrimitive()) { + if (pt == long.class) { + mv.visitVarInsn(LLOAD, insn++); + } else if (pt == float.class) { + mv.visitVarInsn(FLOAD, insn++); + } else if (pt == double.class) { + mv.visitVarInsn(DLOAD, insn++); + } else { + mv.visitVarInsn(ILOAD, insn); + } + Class bigclaz = java.lang.reflect.Array.get(java.lang.reflect.Array.newInstance((Class) pt, 1), 0).getClass(); + mv.visitMethodInsn(INVOKESTATIC, bigclaz.getName().replace('.', '/'), "valueOf", "(" + Type.getDescriptor((Class) pt) + ")" + Type.getDescriptor(bigclaz), false); + } else { + mv.visitVarInsn(ALOAD, insn); + } + //mv.visitVarInsn(ALOAD, 1); + mv.visitInsn(AASTORE); + } + } + + mv.visitMethodInsn(INVOKEVIRTUAL, clientName, "remote", "(" + convertDesc + transportDesc + "I[Ljava/lang/Object;)Ljava/lang/Object;", false); + //mv.visitMethodInsn(INVOKEVIRTUAL, convertName, "convertFrom", convertFromDesc, false); + if (method.getGenericReturnType() == void.class) { + mv.visitInsn(POP); + mv.visitInsn(RETURN); + } else { + Class returnclz = method.getReturnType(); + Class bigPrimitiveClass = returnclz.isPrimitive() ? java.lang.reflect.Array.get(java.lang.reflect.Array.newInstance(returnclz, 1), 0).getClass() : returnclz; + mv.visitTypeInsn(CHECKCAST, (returnclz.isPrimitive() ? bigPrimitiveClass : returnclz).getName().replace('.', '/')); + if (returnclz.isPrimitive()) { + String bigPrimitiveName = bigPrimitiveClass.getName().replace('.', '/'); + try { + java.lang.reflect.Method pm = bigPrimitiveClass.getMethod(returnclz.getSimpleName() + "Value"); + mv.visitMethodInsn(INVOKEVIRTUAL, bigPrimitiveName, pm.getName(), Type.getMethodDescriptor(pm), false); + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + if (returnclz == long.class) { + mv.visitInsn(LRETURN); + } else if (returnclz == float.class) { + mv.visitInsn(FRETURN); + } else if (returnclz == double.class) { + mv.visitInsn(DRETURN); + } else { + mv.visitInsn(IRETURN); + } + } else { + mv.visitInsn(ARETURN); + } + } + mv.visitMaxs(20, 20); + mv.visitEnd(); + } + } + cw.visitEnd(); + byte[] bytes = cw.toByteArray(); + Class newClazz = new ClassLoader(loader) { + public final Class loadClass(String name, byte[] b) { + return defineClass(name, b, 0, b.length); + } + }.loadClass(newDynName.replace('/', '.'), bytes); + try { + T rs = (T) newClazz.newInstance(); + newClazz.getField("client").set(rs, client); + return rs; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + } +} diff --git a/src/com/wentch/redkale/net/sncp/SncpClient.java b/src/com/wentch/redkale/net/sncp/SncpClient.java new file mode 100644 index 000000000..92dee61ff --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpClient.java @@ -0,0 +1,178 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.net.Async; +import com.wentch.redkale.net.Transport; +import com.wentch.redkale.convert.bson.BsonConvert; +import com.wentch.redkale.service.MultiService; +import com.wentch.redkale.service.RemoteOn; +import static com.wentch.redkale.net.sncp.SncpRequest.HEADER_SIZE; +import com.wentch.redkale.util.TwoLong; +import java.lang.reflect.*; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.logging.Logger; + +/** + * + * @author zhangjx + */ +public final class SncpClient { + + private final Logger logger = Logger.getLogger(SncpClient.class.getSimpleName()); + + protected static final class SncpAction { + + protected final TwoLong actionid; + + protected final Method method; + + protected final Type resultTypes; //void 必须设为 null + + protected final Type[] paramTypes; + + protected final boolean async; + + public SncpAction(Method method, TwoLong actionid) { + this.actionid = actionid; + Type rt = method.getGenericReturnType(); + if (rt instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) rt; + if (tv.getBounds().length == 1) rt = tv.getBounds()[0]; + } + this.resultTypes = rt == void.class ? null : rt; + this.paramTypes = method.getGenericParameterTypes(); + this.method = method; + this.async = method.getReturnType() == void.class && method.getAnnotation(Async.class) != null; + } + + @Override + public String toString() { + return "{" + actionid + "," + (method == null ? "null" : method.getName()) + "}"; + } + } + + protected final long nameid; + + protected final long serviceid; + + protected final SncpAction[] actions; + + public SncpClient(final String serviceName, final Class serviceClass) { + if (serviceName.length() > 10) throw new RuntimeException(serviceClass + " @Resource name(" + serviceName + ") too long , must less 11"); + this.nameid = Sncp.hash(MultiService.class.isAssignableFrom(serviceClass) ? serviceName : ""); + this.serviceid = Sncp.hash(serviceClass); + final List methodens = new ArrayList<>(); + //------------------------------------------------------------------------------ + Set actionids = new HashSet<>(); + for (java.lang.reflect.Method method : serviceClass.getDeclaredMethods()) { + if (method.isSynthetic()) continue; + final int mod = method.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) continue; + if (Modifier.isStatic(mod)) continue; + if (Modifier.isFinal(mod)) continue; + if (method.getName().equals("getClass") || method.getName().equals("toString")) continue; + if (method.getName().equals("equals") || method.getName().equals("hashCode")) continue; + if (method.getName().equals("notify") || method.getName().equals("notifyAll") || method.getName().equals("wait")) continue; + if (method.getName().equals("init") || method.getName().equals("destroy")) continue; + Method onMethod = getOnMethod(serviceClass, method); + SncpAction en = new SncpAction(method, onMethod == null ? Sncp.hash(method) : Sncp.hash(onMethod)); + if (actionids.contains(en.actionid)) { + throw new RuntimeException(serviceClass.getName() + " have one more same action(Method=" + method + ", actionid=" + en.actionid + ")"); + } + methodens.add(en); + actionids.add(en.actionid); + } + this.actions = methodens.toArray(new SncpAction[methodens.size()]); + logger.fine("Load " + this.getClass().getSimpleName() + "(serviceClass = " + serviceClass.getName() + ", serviceid =" + serviceid + ", serviceName =" + serviceName + ", actions = " + methodens + ")"); + } + + public static Method getOnMethod(final Class serviceClass, Method method) { + Method onMethod = null; + if (method.getAnnotation(RemoteOn.class) != null) { + char[] ms = method.getName().toCharArray(); + ms[0] = Character.toUpperCase(ms[0]); + try { + onMethod = serviceClass.getMethod("on" + new String(ms), method.getParameterTypes()); + if (onMethod.getReturnType() != method.getReturnType()) { + throw new RuntimeException(serviceClass.getName() + " (Method=" + method + ") and (Method=" + onMethod + ") has not same returnType"); + } + if (!Modifier.isFinal(onMethod.getModifiers())) { + throw new RuntimeException(serviceClass.getName() + " (Method=" + method + ") is not final"); + } + } catch (NoSuchMethodException e) { + throw new RuntimeException(serviceClass.getName() + " not found (Public Method=" + "on" + new String(ms) + "but " + method + " has @" + RemoteOn.class.getSimpleName()); + } + } + return onMethod; + } + + public T remote(final BsonConvert convert, Transport transport, final int index, final Object... params) { + return convert.convertFrom(actions[index].resultTypes, send(convert, transport, index, params)); + } + + private byte[] send(final BsonConvert convert, Transport transport, final int index, Object... params) { + int bodyLength = 2; + Type[] myparamtypes = actions[index].paramTypes; + byte[][] bytesarray = new byte[params.length][]; + for (int i = 0; i < bytesarray.length; i++) { + bytesarray[i] = convert.convertTo(myparamtypes[i], params[i]); + bodyLength += 2 + bytesarray[i].length; + } + ByteBuffer buffer = transport.pollBuffer(); + if ((HEADER_SIZE + bodyLength) > buffer.limit()) { + throw new RuntimeException("send buffer size too large(" + (HEADER_SIZE + bodyLength) + ")"); + } + final SncpAction action = actions[index]; + final long seqid = System.nanoTime(); + final TwoLong actionid = action.actionid; + { + //---------------------head---------------------------------- + buffer.putLong(seqid); //序列号 + buffer.putChar((char) HEADER_SIZE); //header长度 + buffer.putLong(this.serviceid); + buffer.putLong(this.nameid); + buffer.putLong(actionid.getFirst()); + buffer.putLong(actionid.getSecond()); + buffer.put((byte) 0); //剩下还有多少帧数据, 0表示只有当前一帧数据 + buffer.putInt(0); //结果码, 请求方固定传0 + buffer.putChar((char) bodyLength); //body长度 + //---------------------body---------------------------------- + buffer.putChar((char) bytesarray.length); //参数数组大小 + for (byte[] bs : bytesarray) { + buffer.putChar((char) bs.length); + buffer.put(bs); + } + buffer.flip(); + } + if (action.async) { + transport.async(buffer, null, null); + return null; + } + buffer = transport.send(buffer); + + long rseqid = buffer.getLong(); + if (rseqid != seqid) throw new RuntimeException("sncp send seqid = " + seqid + ", but receive seqid =" + rseqid); + if (buffer.getChar() != HEADER_SIZE) throw new RuntimeException("sncp buffer receive header.length not " + HEADER_SIZE); + long rserviceid = buffer.getLong(); + if (rserviceid != serviceid) throw new RuntimeException("sncp send serviceid = " + serviceid + ", but receive serviceid =" + rserviceid); + long rnameid = buffer.getLong(); + if (rnameid != nameid) throw new RuntimeException("sncp send nameid = " + nameid + ", but receive nameid =" + rnameid); + long ractionid1 = buffer.getLong(); + long ractionid2 = buffer.getLong(); + if (!actionid.compare(ractionid1, ractionid2)) throw new RuntimeException("sncp send actionid = " + actionid + ", but receive actionid =(" + ractionid1 + "_" + ractionid2 + ")"); + int frame = buffer.get(); + int retcode = buffer.getInt(); + if (retcode != 0) throw new RuntimeException("remote service deal error (receive retcode =" + retcode + ")"); + int bodylen = buffer.getChar(); + byte[] bytes = new byte[bodylen]; + buffer.get(bytes); + transport.offerBuffer(buffer); + return bytes; + } + +} diff --git a/src/com/wentch/redkale/net/sncp/SncpContext.java b/src/com/wentch/redkale/net/sncp/SncpContext.java new file mode 100644 index 000000000..fc0f674ad --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpContext.java @@ -0,0 +1,51 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.convert.bson.BsonConvert; +import com.wentch.redkale.convert.bson.BsonFactory; +import com.wentch.redkale.net.ResponsePool; +import com.wentch.redkale.net.BufferPool; +import com.wentch.redkale.net.PrepareServlet; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.watch.WatchFactory; +import java.net.InetSocketAddress; +import java.nio.charset.Charset; +import java.util.concurrent.ExecutorService; +import java.util.logging.Logger; + +/** + * + * @author zhangjx + */ +public final class SncpContext extends Context { + + protected final BsonFactory bsonFactory; + + public SncpContext(long serverStartTime, Logger logger, ExecutorService executor, BufferPool bufferPool, + ResponsePool responsePool, int maxbody, Charset charset, InetSocketAddress address, + PrepareServlet prepare, WatchFactory watch, int readTimeoutSecond, int writeTimeoutSecond) { + super(serverStartTime, logger, executor, bufferPool, responsePool, maxbody, charset, + address, prepare, watch, readTimeoutSecond, writeTimeoutSecond); + this.bsonFactory = BsonFactory.root(); + } + + protected WatchFactory getWatchFactory() { + return watch; + } + + protected ExecutorService getExecutor() { + return executor; + } + + protected ResponsePool getResponsePool() { + return responsePool; + } + + public BsonConvert getBsonConvert() { + return bsonFactory.getConvert(); + } +} diff --git a/src/com/wentch/redkale/net/sncp/SncpDynServlet.java b/src/com/wentch/redkale/net/sncp/SncpDynServlet.java new file mode 100644 index 000000000..6d8b5796c --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpDynServlet.java @@ -0,0 +1,318 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.util.DebugMethodVisitor; +import com.wentch.redkale.util.TwoLong; +import com.wentch.redkale.convert.bson.BsonConvert; +import static com.wentch.redkale.net.sncp.SncpClient.getOnMethod; +import com.wentch.redkale.service.Service; +import java.io.*; +import java.lang.reflect.*; +import java.util.*; +import java.util.logging.*; +import javax.annotation.Resource; +import jdk.internal.org.objectweb.asm.*; +import static jdk.internal.org.objectweb.asm.Opcodes.*; +import jdk.internal.org.objectweb.asm.Type; + +/** + * + * @author zhangjx + */ +public class SncpDynServlet extends SncpServlet { + + private final Logger logger = Logger.getLogger(SncpDynServlet.class.getSimpleName()); + + private final long nameid; + + private final long serviceid; + + private final HashMap actions = new HashMap<>(); + + public SncpDynServlet(final BsonConvert convert, final String serviceName, final Service service, final AnyValue conf) { + this.conf = conf; + final Class serviceClass = service.getClass(); + this.nameid = Sncp.hash(serviceName); + this.serviceid = Sncp.hash(serviceClass); + Set actionids = new HashSet<>(); + for (java.lang.reflect.Method method : serviceClass.getMethods()) { + if (method.isSynthetic()) continue; + if (Modifier.isStatic(method.getModifiers())) continue; + if (Modifier.isFinal(method.getModifiers())) continue; + if (method.getName().equals("getClass") || method.getName().equals("toString")) continue; + if (method.getName().equals("equals") || method.getName().equals("hashCode")) continue; + if (method.getName().equals("notify") || method.getName().equals("notifyAll") || method.getName().equals("wait")) continue; + if (method.getName().equals("init") || method.getName().equals("destroy")) continue; + Method onMethod = getOnMethod(serviceClass, method); + if (onMethod != null) method = onMethod; + final TwoLong actionid = Sncp.hash(method); + SncpServletAction action = SncpServletAction.create(service, actionid, method); + action.convert = convert; + if (actionids.contains(actionid)) { + throw new RuntimeException(serviceClass.getName() + + " have action(Method=" + method + ", actionid=" + actionid + ") same to (" + actions.get(actionid).method + ")"); + } + actions.put(actionid, action); + actionids.add(actionid); + } + if (!logger.isLoggable(Level.FINE)) return; + StringBuilder sb = new StringBuilder(); + sb.append("{"); + actions.forEach((x, y) -> sb.append('{').append(x).append(',').append(y.method.getName()).append("},")); + sb.append("}"); + logger.fine(this.getClass().getSimpleName() + "(serviceClass = " + serviceClass.getName() + ", serviceid =" + serviceid + ", serviceName =" + serviceName + ", actions = " + sb + ") loaded"); + } + + @Override + public long getNameid() { + return nameid; + } + + @Override + public long getServiceid() { + return serviceid; + } + + @Override + public void execute(SncpRequest request, SncpResponse response) throws IOException { + SncpServletAction action = actions.get(request.getActionid()); + if (action == null) { + response.finish(SncpResponse.RETCODE_ILLACTIONID, null); //无效actionid + } else { + byte[] rs = null; + try { + rs = action.action(request.getParamBytes()); + } catch (Throwable t) { + response.getContext().getLogger().log(Level.INFO, "sncp execute error(" + request + ")", t); + response.finish(SncpResponse.RETCODE_THROWEXCEPTION, null); + } + response.finish(0, rs); + } + } + + public static abstract class SncpServletAction { + + public Method method; + + @Resource + protected BsonConvert convert; + + protected java.lang.reflect.Type[] paramTypes; //index=0表示返回参数的type, void的返回参数类型为null + + public abstract byte[] action(byte[][] bytes) throws Throwable; + + /* + * + * public class TestService implements Service { + * public boolean change(TestBean bean, String name, int id) { + * + * } + * } + * + * public class DynActionTestService_change extends SncpServletAction { + * + * public TestService service; + * + * @Override + * public byte[] action(byte[][] bytes) throws Throwable { + * TestBean arg1 = convert.convertFrom(paramTypes[1], bytes[1]); + * String arg2 = convert.convertFrom(paramTypes[2], bytes[2]); + * int arg3 = convert.convertFrom(paramTypes[3], bytes[3]); + * Object rs = service.change(arg1, arg2, arg3); + * return convert.convertTo(paramTypes[0], rs); + * } + * } + */ + /** + * + * @param service + * @param actionid + * @param method + * @return + */ + @SuppressWarnings("unchecked") + public static SncpServletAction create(final Service service, final TwoLong actionid, final Method method) { + final Class serviceClass = service.getClass(); + final String supDynName = SncpServletAction.class.getName().replace('.', '/'); + final String serviceName = serviceClass.getName().replace('.', '/'); + final String convertName = BsonConvert.class.getName().replace('.', '/'); + final String serviceDesc = Type.getDescriptor(serviceClass); + String newDynName = serviceName.substring(0, serviceName.lastIndexOf('/') + 1) + + "DynAction" + serviceClass.getSimpleName() + "_" + method.getName() + "_" + actionid; + while (true) { + try { + Class.forName(newDynName.replace('/', '.')); + newDynName += "_"; + } catch (Exception ex) { + break; + } + } + //------------------------------------------------------------- + ClassWriter cw = new ClassWriter(0); + FieldVisitor fv; + DebugMethodVisitor mv; + + cw.visit(V1_8, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, newDynName, null, supDynName, null); + + { + { + fv = cw.visitField(ACC_PUBLIC, "service", serviceDesc, null, null); + fv.visitEnd(); + } + fv.visitEnd(); + } + { // constructor方法 + mv = new DebugMethodVisitor(cw.visitMethod(ACC_PUBLIC, "", "()V", null, null)); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, supDynName, "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + String convertFromDesc = "(Ljava/lang/reflect/Type;[B)Ljava/lang/Object;"; + try { + convertFromDesc = Type.getMethodDescriptor(BsonConvert.class.getMethod("convertFrom", java.lang.reflect.Type.class, byte[].class)); + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + { // action方法 + mv = new DebugMethodVisitor(cw.visitMethod(ACC_PUBLIC, "action", "([[B)[B", null, new String[]{"java/lang/Throwable"})); + //mv.setDebug(true); + int iconst = ICONST_1; + int intconst = 1; + int store = 2; + final Class[] paramClasses = method.getParameterTypes(); + int[][] codes = new int[paramClasses.length][2]; + for (int i = 0; i < paramClasses.length; i++) { //参数 + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "convert", Type.getDescriptor(BsonConvert.class)); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "paramTypes", "[Ljava/lang/reflect/Type;"); + if (iconst > ICONST_5) { + mv.visitIntInsn(BIPUSH, intconst); + } else { + mv.visitInsn(iconst); // + } + mv.visitInsn(AALOAD); + mv.visitVarInsn(ALOAD, 1); + if (iconst > ICONST_5) { + mv.visitIntInsn(BIPUSH, intconst); + } else { + mv.visitInsn(iconst); // + } + mv.visitInsn(AALOAD); + mv.visitMethodInsn(INVOKEVIRTUAL, convertName, "convertFrom", convertFromDesc, false); + int load = ALOAD; + int v = 0; + if (paramClasses[i].isPrimitive()) { + int storecode = ISTORE; + load = ILOAD; + if (paramClasses[i] == long.class) { + storecode = LSTORE; + load = LLOAD; + v = 1; + } else if (paramClasses[i] == float.class) { + storecode = FSTORE; + load = FLOAD; + v = 1; + } else if (paramClasses[i] == double.class) { + storecode = DSTORE; + load = DLOAD; + v = 1; + } + Class bigPrimitiveClass = Array.get(Array.newInstance(paramClasses[i], 1), 0).getClass(); + String bigPrimitiveName = bigPrimitiveClass.getName().replace('.', '/'); + try { + Method pm = bigPrimitiveClass.getMethod(paramClasses[i].getSimpleName() + "Value"); + mv.visitTypeInsn(CHECKCAST, bigPrimitiveName); + mv.visitMethodInsn(INVOKEVIRTUAL, bigPrimitiveName, pm.getName(), Type.getMethodDescriptor(pm), false); + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + mv.visitVarInsn(storecode, store); + } else { + mv.visitTypeInsn(CHECKCAST, paramClasses[i].getName().replace('.', '/')); + mv.visitVarInsn(ASTORE, store); // + } + codes[i] = new int[]{load, store}; + store += v; + iconst++; + intconst++; + store++; + } + { //调用service + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "service", serviceDesc); + for (int[] j : codes) { + mv.visitVarInsn(j[0], j[1]); + } + mv.visitMethodInsn(INVOKEVIRTUAL, serviceName, method.getName(), Type.getMethodDescriptor(method), false); + } + + int maxStack = codes.length > 0 ? codes[codes.length - 1][1] : 1; + Class returnClass = method.getReturnType(); + if (method.getReturnType() == void.class) { //返回 + mv.visitInsn(ACONST_NULL); + mv.visitInsn(ARETURN); + maxStack = 8; + } else { + if (returnClass.isPrimitive()) { + Class bigClass = Array.get(Array.newInstance(returnClass, 1), 0).getClass(); + try { + Method vo = bigClass.getMethod("valueOf", returnClass); + mv.visitMethodInsn(INVOKESTATIC, bigClass.getName().replace('.', '/'), vo.getName(), Type.getMethodDescriptor(vo), false); + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + } + mv.visitVarInsn(ASTORE, store); //11 + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "convert", Type.getDescriptor(BsonConvert.class)); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, newDynName, "paramTypes", "[Ljava/lang/reflect/Type;"); + mv.visitInsn(ICONST_0); + mv.visitInsn(AALOAD); + mv.visitVarInsn(ALOAD, store); + mv.visitMethodInsn(INVOKEVIRTUAL, convertName, "convertTo", "(Ljava/lang/reflect/Type;Ljava/lang/Object;)[B", false); + mv.visitInsn(ARETURN); + store++; + if (maxStack < 10) maxStack = 10; + } + mv.visitMaxs(maxStack, store); + mv.visitEnd(); + } + cw.visitEnd(); + + byte[] bytes = cw.toByteArray(); + Class newClazz = new ClassLoader(serviceClass.getClassLoader()) { + public final Class loadClass(String name, byte[] b) { + return defineClass(name, b, 0, b.length); + } + }.loadClass(newDynName.replace('/', '.'), bytes); + try { + SncpServletAction instance = (SncpServletAction) newClazz.newInstance(); + instance.method = method; + java.lang.reflect.Type[] ptypes = method.getGenericParameterTypes(); + java.lang.reflect.Type[] types = new java.lang.reflect.Type[ptypes.length + 1]; + java.lang.reflect.Type rt = method.getGenericReturnType(); + if (rt instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) rt; + if (tv.getBounds().length == 1) rt = tv.getBounds()[0]; + } + types[0] = rt; + System.arraycopy(ptypes, 0, types, 1, ptypes.length); + instance.paramTypes = types; + newClazz.getField("service").set(instance, service); + return instance; + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + } + } + +} diff --git a/src/com/wentch/redkale/net/sncp/SncpPrepareServlet.java b/src/com/wentch/redkale/net/sncp/SncpPrepareServlet.java new file mode 100644 index 000000000..57a7de69d --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpPrepareServlet.java @@ -0,0 +1,80 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.net.PrepareServlet; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.AnyValue; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; + +/** + * + * @author zhangjx + */ +public class SncpPrepareServlet extends PrepareServlet { + + private static final ByteBuffer pongBuffer = ByteBuffer.wrap("PONG".getBytes()).asReadOnlyBuffer(); + + private final Map> maps = new HashMap<>(); + + private final Map singlemaps = new HashMap<>(); + + public void addSncpServlet(SncpServlet servlet) { + if (servlet.getNameid() == 0) { + singlemaps.put(servlet.getServiceid(), servlet); + } else { + Map m = maps.get(servlet.getServiceid()); + if (m == null) { + m = new HashMap<>(); + maps.put(servlet.getServiceid(), m); + } + m.put(servlet.getNameid(), servlet); + } + } + + @Override + public void init(Context context, AnyValue config) { + Collection> values = this.maps.values(); + values.stream().forEach((en) -> { + en.values().stream().forEach(s -> s.init(context, s.conf)); + }); + } + + @Override + public void destroy(Context context, AnyValue config) { + Collection> values = this.maps.values(); + values.stream().forEach((en) -> { + en.values().stream().forEach(s -> s.destroy(context, s.conf)); + }); + } + + @Override + public void execute(SncpRequest request, SncpResponse response) throws IOException { + if (request.isPing()) { + response.finish(pongBuffer.duplicate()); + return; + } + SncpServlet servlet; + if (request.getNameid() == 0) { + servlet = singlemaps.get(request.getServiceid()); + } else { + Map m = maps.get(request.getServiceid()); + if (m == null) { + response.finish(SncpResponse.RETCODE_ILLSERVICEID, null); //无效serviceid + return; + } + servlet = m.get(request.getNameid()); + } + if (servlet == null) { + response.finish(SncpResponse.RETCODE_ILLNAMEID, null); //无效nameid + } else { + servlet.execute(request, response); + } + } + +} diff --git a/src/com/wentch/redkale/net/sncp/SncpRequest.java b/src/com/wentch/redkale/net/sncp/SncpRequest.java new file mode 100644 index 000000000..ef2230627 --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpRequest.java @@ -0,0 +1,134 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.convert.bson.BsonConvert; +import com.wentch.redkale.convert.bson.BsonFactory; +import com.wentch.redkale.net.Request; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.TwoLong; +import java.nio.ByteBuffer; + +/** + * + * @author zhangjx + */ +public final class SncpRequest extends Request { + + public static final int HEADER_SIZE = 49; + + protected final BsonConvert convert; + + private long seqid; + + private int frame; + + private long nameid; + + private long serviceid; + + private TwoLong actionid; + + private int bodylength; + + private byte[][] paramBytes; + + private boolean ping; + + protected SncpRequest(Context context, BsonFactory factory) { + super(context); + this.convert = factory.getConvert(); + } + + @Override + protected int readHeader(ByteBuffer buffer) { + if (buffer.remaining() < 8) { + this.ping = true; + return 0; + } + //---------------------head---------------------------------- + this.seqid = buffer.getLong(); + if (buffer.getChar() != HEADER_SIZE) { + context.getLogger().finest("sncp buffer header.length not " + HEADER_SIZE); + return -1; + } + this.serviceid = buffer.getLong(); + this.nameid = buffer.getLong(); + this.actionid = new TwoLong(buffer.getLong(), buffer.getLong()); + this.frame = buffer.get(); + if (buffer.getInt() != 0) { + context.getLogger().finest("sncp buffer header.retcode not 0"); + return -1; + } + this.bodylength = buffer.getChar(); + //---------------------body---------------------------------- + int paramlen = buffer.getChar(); + byte[][] bbytes = new byte[paramlen + 1][]; //占位第0个byte[] + for (int i = 1; i <= paramlen; i++) { + byte[] bytes = new byte[(int) buffer.getChar()]; + buffer.get(bytes); + bbytes[i] = bytes; + } + this.paramBytes = bbytes; + return 0; + } + + @Override + public String toString() { + return SncpRequest.class.getSimpleName() + "{seqid=" + this.seqid + + ",serviceid=" + this.serviceid + ",actionid=" + this.actionid + + ",frame=" + this.frame + ",bodylength=" + this.bodylength + "}"; + } + + protected void setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + } + + protected boolean isKeepAlive() { + return this.keepAlive; + } + + @Override + protected void readBody(ByteBuffer buffer) { + } + + @Override + protected void recycle() { + this.seqid = 0; + this.frame = 0; + this.serviceid = 0; + this.actionid = null; + this.bodylength = 0; + this.paramBytes = null; + this.ping = false; + super.recycle(); + } + + protected boolean isPing() { + return ping; + } + + public byte[][] getParamBytes() { + return paramBytes; + } + + public long getSeqid() { + return seqid; + } + + public long getServiceid() { + return serviceid; + } + + public long getNameid() { + return nameid; + } + + public TwoLong getActionid() { + return actionid; + } + +} diff --git a/src/com/wentch/redkale/net/sncp/SncpResponse.java b/src/com/wentch/redkale/net/sncp/SncpResponse.java new file mode 100644 index 000000000..8af354691 --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpResponse.java @@ -0,0 +1,49 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.net.Response; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.util.TwoLong; +import java.nio.ByteBuffer; + +/** + * + * @author zhangjx + */ +public final class SncpResponse extends Response { + + public static final int RETCODE_ILLSERVICEID = 10001; //无效serviceid + + public static final int RETCODE_ILLNAMEID = 10002; //无效nameid + + public static final int RETCODE_ILLACTIONID = 10003; //无效actionid + + public static final int RETCODE_THROWEXCEPTION = 10011; //内部异常 + + protected SncpResponse(Context context, SncpRequest request) { + super(context, request); + } + + public void finish(final int retcode, final byte[] bytes) { + ByteBuffer buffer = context.pollBuffer(); + //---------------------head---------------------------------- + buffer.putLong(request.getSeqid()); + buffer.putChar((char) SncpRequest.HEADER_SIZE); + buffer.putLong(request.getServiceid()); + buffer.putLong(request.getNameid()); + TwoLong actionid = request.getActionid(); + buffer.putLong(actionid.getFirst()); + buffer.putLong(actionid.getSecond()); + buffer.put((byte) 0); + buffer.putInt(retcode); + buffer.putChar((char) (bytes == null ? 0 : bytes.length)); + //---------------------body---------------------------------- + if (bytes != null) buffer.put(bytes); + buffer.flip(); + finish(buffer); + } +} diff --git a/src/com/wentch/redkale/net/sncp/SncpServer.java b/src/com/wentch/redkale/net/sncp/SncpServer.java new file mode 100644 index 000000000..d2c158a26 --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpServer.java @@ -0,0 +1,59 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.convert.bson.BsonConvert; +import com.wentch.redkale.convert.bson.BsonFactory; +import com.wentch.redkale.net.ResponsePool; +import com.wentch.redkale.net.BufferPool; +import com.wentch.redkale.net.Server; +import com.wentch.redkale.net.Context; +import com.wentch.redkale.watch.WatchFactory; +import java.util.*; +import java.util.concurrent.atomic.*; + +/** + * Service Node Communicate Protocol + * + * @author zhangjx + */ +public final class SncpServer extends Server { + + private final List services = new ArrayList<>(); + + public SncpServer() { + this(System.currentTimeMillis(), null); + } + + public SncpServer(long serverStartTime, final WatchFactory watch) { + super(serverStartTime, "UDP", watch); + } + + public void addService(ServiceEntry entry) { + this.services.add(entry); + } + + @Override + @SuppressWarnings("unchecked") + protected Context createContext() { + final int port = this.address.getPort(); + AtomicLong createBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SNCP_" + port + ".Buffer.creatCounter"); + AtomicLong cycleBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SNCP_" + port + ".Buffer.cycleCounter"); + BufferPool bufferPool = new BufferPool(createBufferCounter, cycleBufferCounter, Math.max(this.capacity, 8 * 1024), this.bufferPoolSize); + SncpPrepareServlet prepare = new SncpPrepareServlet(); + final BsonConvert convert = BsonFactory.root().getConvert(); + this.services.stream().forEach(x -> x.getNames().forEach(y -> prepare.addSncpServlet(new SncpDynServlet(convert, y, x.getService(), x.getServiceConf())))); + this.services.clear(); + AtomicLong createResponseCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SNCP_" + port + ".Response.creatCounter"); + AtomicLong cycleResponseCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SNCP_" + port + ".Response.cycleCounter"); + SncpContext sncpcontext = new SncpContext(this.serverStartTime, this.logger, executor, bufferPool, + new ResponsePool(createResponseCounter, cycleResponseCounter, this.responsePoolSize), + this.maxbody, this.charset, this.address, prepare, this.watch, this.readTimeoutSecond, this.writeTimeoutSecond); + sncpcontext.getResponsePool().setResponseFactory(() -> new SncpResponse(sncpcontext, new SncpRequest(sncpcontext, sncpcontext.bsonFactory))); + return sncpcontext; + } + +} diff --git a/src/com/wentch/redkale/net/sncp/SncpServlet.java b/src/com/wentch/redkale/net/sncp/SncpServlet.java new file mode 100644 index 000000000..c079a06ad --- /dev/null +++ b/src/com/wentch/redkale/net/sncp/SncpServlet.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.net.sncp; + +import com.wentch.redkale.net.Servlet; +import com.wentch.redkale.util.AnyValue; + +/** + * + * @author zhangjx + */ +public abstract class SncpServlet implements Servlet { + + AnyValue conf; + + public abstract long getNameid(); + + public abstract long getServiceid(); + + @Override + public final boolean equals(Object obj) { + return obj != null && obj.getClass() == this.getClass(); + } + + @Override + public final int hashCode() { + return this.getClass().hashCode(); + } +} diff --git a/src/com/wentch/redkale/service/DataCacheListenerService.java b/src/com/wentch/redkale/service/DataCacheListenerService.java new file mode 100644 index 000000000..4840b61ab --- /dev/null +++ b/src/com/wentch/redkale/service/DataCacheListenerService.java @@ -0,0 +1,217 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.service; + +import com.wentch.redkale.source.*; +import com.wentch.redkale.util.*; +import java.io.*; +import java.util.*; +import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.*; +import java.util.logging.*; +import javax.annotation.*; + +/** + * + * @author zhangjx + */ +@AutoLoad(false) +public class DataCacheListenerService implements DataCacheListener, Service { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + private static final String format = "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%tL"; + + private final ConcurrentHashMap>> insertQueues = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap>> updateQueues = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap>> deleteQueues = new ConcurrentHashMap<>(); + + private boolean finer; + + @Resource(name = "APP_NODE") + private String localNodeName = ""; + + @Resource(name = ".*") + HashMap sourcemap; + + @Resource(name = ".*") + HashMap nodemap; + + @Override + public void init(AnyValue config) { + finer = logger.isLoggable(Level.FINER); + } + + @Override + public void insert(String sourceName, Class clazz, T... entitys) { + BlockingQueue> queue = this.insertQueues.get(sourceName); + if (queue == null) { + synchronized (this.insertQueues) { + queue = this.insertQueues.get(sourceName); + if (queue == null) { + queue = new ArrayBlockingQueue<>(10240); + this.insertQueues.put(sourceName, queue); + final BlockingQueue> tq = queue; + new Thread() { + { + setName(DataCacheListener.class.getSimpleName() + "-" + (sourceName.isEmpty() ? "<>" : sourceName) + "-Insert-Thread"); + setDaemon(true); + } + + @Override + public void run() { + while (true) { + try { + Map.Entry entry = tq.take(); + sendInsert(sourceName, entry.getKey(), entry.getValue()); + } catch (Exception e) { + logger.log(Level.SEVERE, this.getName() + " sendInsert occur error", e); + } + } + + } + }.start(); + } + } + } + try { + queue.put(new SimpleEntry<>(clazz, entitys)); + } catch (Exception e) { + logger.log(Level.WARNING, this.getClass().getSimpleName() + " put insert queue error " + Arrays.toString(entitys), e); + } + } + + @RemoteOn + public void sendInsert(String sourceName, Class clazz, T... entitys) { + if (nodemap == null) return; + nodemap.forEach((x, y) -> { + try { + y.sendInsert(sourceName, clazz, entitys); + } catch (Exception e) { + logger.log(Level.FINE, this.getClass().getSimpleName() + " send insert error (" + x + ", " + sourceName + ", " + clazz + ", " + Arrays.toString(entitys) + ")", e); + } + }); + } + + public final void onSendInsert(String sourceName, Class clazz, T... entitys) { + ((DataJDBCSource) sourcemap.get(sourceName)).insertCache(entitys); + if (finer) logger.finer(DataSource.class.getSimpleName() + "(" + this.localNodeName + "," + sourceName + ") onSendInsert " + Arrays.toString(entitys)); + } + + @Override + public void update(String sourceName, Class clazz, T... values) { + BlockingQueue> queue = this.updateQueues.get(sourceName); + if (queue == null) { + synchronized (this.updateQueues) { + queue = this.updateQueues.get(sourceName); + if (queue == null) { + queue = new ArrayBlockingQueue<>(10240); + this.updateQueues.put(sourceName, queue); + final BlockingQueue> tq = queue; + new Thread() { + { + setName(DataCacheListener.class.getSimpleName() + "-" + (sourceName.isEmpty() ? "<>" : sourceName) + "-Update-Thread"); + setDaemon(true); + } + + @Override + public void run() { + while (true) { + try { + Map.Entry entry = tq.take(); + sendUpdate(sourceName, entry.getKey(), entry.getValue()); + } catch (Exception e) { + logger.log(Level.SEVERE, this.getName() + " sendUpdate occur error", e); + } + } + + } + }.start(); + } + } + } + try { + queue.put(new SimpleEntry<>(clazz, values)); + } catch (Exception e) { + logger.log(Level.WARNING, this.getClass().getSimpleName() + " put update queue error " + clazz + "," + Arrays.toString(values), e); + } + } + + @RemoteOn + public void sendUpdate(String sourceName, Class clazz, Object... values) { + if (nodemap == null) return; + nodemap.forEach((x, y) -> { + try { + y.sendUpdate(sourceName, clazz, values); + } catch (Exception e) { + logger.log(Level.FINE, this.getClass().getSimpleName() + " send update error (" + x + ", " + sourceName + ", " + clazz + ", " + Arrays.toString(values) + ")", e); + } + }); + } + + public final void onSendUpdate(String sourceName, Class clazz, T... entitys) { + ((DataJDBCSource) sourcemap.get(sourceName)).updateCache(clazz, entitys); + if (finer) logger.finer(DataSource.class.getSimpleName() + "(" + this.localNodeName + "," + sourceName + ") onSendUpdate " + Arrays.toString(entitys)); + } + + @Override + public void delete(String sourceName, Class clazz, Serializable... ids) { + BlockingQueue> queue = this.deleteQueues.get(sourceName); + if (queue == null) { + synchronized (this.deleteQueues) { + queue = this.deleteQueues.get(sourceName); + if (queue == null) { + queue = new ArrayBlockingQueue<>(10240); + this.deleteQueues.put(sourceName, queue); + final BlockingQueue> tq = queue; + new Thread() { + { + setName(DataCacheListener.class.getSimpleName() + "-" + (sourceName.isEmpty() ? "<>" : sourceName) + "-Delete-Thread"); + setDaemon(true); + } + + @Override + public void run() { + while (true) { + try { + Map.Entry entry = tq.take(); + sendDelete(sourceName, entry.getKey(), entry.getValue()); + } catch (Exception e) { + logger.log(Level.SEVERE, this.getName() + " sendDelete occur error", e); + } + } + + } + }.start(); + } + } + } + try { + queue.put(new SimpleEntry<>(clazz, ids)); + } catch (Exception e) { + logger.log(Level.WARNING, this.getClass().getSimpleName() + " put delete queue error " + clazz + "," + Arrays.toString(ids), e); + } + } + + @RemoteOn + public void sendDelete(String sourceName, Class clazz, Serializable... ids) { + if (nodemap == null) return; + nodemap.forEach((x, y) -> { + try { + y.sendDelete(sourceName, clazz, ids); + } catch (Exception e) { + logger.log(Level.FINE, this.getClass().getSimpleName() + " send delete error (" + x + ", " + sourceName + ", " + clazz + ", " + Arrays.toString(ids) + ")", e); + } + }); + } + + public final void onSendDelete(String sourceName, Class clazz, Serializable... ids) { + ((DataJDBCSource) sourcemap.get(sourceName)).deleteCache(clazz, ids); + if (finer) logger.finer(DataSource.class.getSimpleName() + "(" + this.localNodeName + "," + sourceName + ") onSendDelete " + clazz.getName() + " " + Arrays.toString(ids)); + } +} diff --git a/src/com/wentch/redkale/service/DataSQLListenerService.java b/src/com/wentch/redkale/service/DataSQLListenerService.java new file mode 100644 index 000000000..9a71a05c8 --- /dev/null +++ b/src/com/wentch/redkale/service/DataSQLListenerService.java @@ -0,0 +1,178 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.service; + +import com.wentch.redkale.source.DataSQLListener; +import com.wentch.redkale.source.DataSource; +import com.wentch.redkale.source.DataJDBCSource; +import com.wentch.redkale.util.AnyValue; +import com.wentch.redkale.util.AutoLoad; +import java.io.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.*; +import javax.annotation.Resource; + +/** + * + * @author zhangjx + */ +@AutoLoad(false) +public class DataSQLListenerService implements DataSQLListener, Service { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + private static final String format = "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%tL"; + + private boolean finest; + + @Resource(name = "APP_NODE") + private String localNodeName = ""; + + private String localIDCName = ""; + + @Resource(name = "APP_HOME") + private File home; + + private File root; + + @Resource(name = ".*") + HashMap sourcemaps; + + private ConcurrentHashMap> queues = new ConcurrentHashMap<>(); + + @Resource + private HashMap nodemaps; + + private final HashSet allidcs = new HashSet<>(); + + private ConcurrentHashMap syncfiles = new ConcurrentHashMap<>(); + + @Override + public void init(AnyValue config) { + finest = logger.isLoggable(Level.FINEST); + //nodename的前两位字符表示机房ID + if (localNodeName.length() > 2) localIDCName = getIDC(localNodeName); + if (finest) logger.fine("LocalNodeName: " + localNodeName + ", " + localIDCName + " " + this.nodemaps); + if (this.nodemaps == null) return; + this.nodemaps.forEach((x, y) -> allidcs.add(x.substring(0, 2))); + } + + @Override + public void destroy(AnyValue config) { + this.syncfiles.forEach((x, y) -> { + y.close(); + }); + } + + private void write(String node, String sourceName, String... sqls) { + if (sourceName == null || sourceName.isEmpty()) sourceName = "<>"; + String key = node + "-" + sourceName; + PrintStream channel = syncfiles.get(key); + try { + if (channel == null) { + if (this.root == null) { + this.root = new File(home, "dbsync"); + this.root.mkdirs(); + } + channel = new PrintStream(new FileOutputStream(new File(this.root, key + ".sql"), true), false, "UTF-8"); + syncfiles.put(key, channel); + } + for (String sql : sqls) { + channel.print(sql + ";\r\n"); + } + channel.flush(); + } catch (Exception e) { + logger.log(Level.WARNING, "write sql file error. (" + node + ", " + sourceName + ", " + Arrays.toString(sqls) + ")", e); + } + } + + @Override + public void insert(String sourceName, String... sqls) { + put(sourceName, sqls); + } + + @Override + public void update(String sourceName, String... sqls) { + put(sourceName, sqls); + } + + @Override + public void delete(String sourceName, String... sqls) { + put(sourceName, sqls); + } + + private void put(final String sourceName, String... sqls) { + String date = String.format(format, System.currentTimeMillis()); + BlockingQueue queue = this.queues.get(sourceName); + if (queue == null) { + synchronized (this) { + queue = this.queues.get(sourceName); + if (queue == null) { + queue = new ArrayBlockingQueue<>(1024 * 1024); + this.queues.put(sourceName, queue); + final BlockingQueue tq = queue; + new Thread() { + { + setName(DataSQLListener.class.getSimpleName() + "-" + (sourceName.isEmpty() ? "<>" : sourceName) + "-Thread"); + setDaemon(true); + } + + @Override + public void run() { + while (true) { + try { + String sql = tq.take(); + send(sourceName, sql); + } catch (Exception e) { + logger.log(Level.SEVERE, this.getName() + " occur error"); + } + } + + } + }.start(); + } + } + } + try { + for (String sql : sqls) { + queue.put("/* " + date + " */ " + sql); + } + } catch (Exception e) { + logger.log(Level.WARNING, this.getClass().getSimpleName() + " put queue error" + Arrays.toString(sqls), e); + } + } + + private String getIDC(String nodeName) { + return nodeName.substring(0, 2); + } + + @RemoteOn + public void send(String sourceName, String... sqls) { + if (this.nodemaps == null) return; + final Set idcs = new HashSet<>(); + idcs.add(localIDCName); + nodemaps.forEach((x, y) -> { + try { + String idc = getIDC(x); + if (!idcs.contains(idc)) { + y.send(sourceName, sqls); + idcs.add(idc); + } + } catch (Exception e) { + logger.log(Level.FINE, this.getClass().getSimpleName() + " send error (" + x + ", " + sourceName + ", " + Arrays.toString(sqls) + ")", e); + } + }); + allidcs.forEach(x -> { + if (!idcs.contains(x)) write(x, sourceName, sqls); + }); + } + + public final void onSend(String sourceName, String... sqls) { + ((DataJDBCSource) sourcemaps.get(sourceName)).execute(sqls); + } + +} diff --git a/src/com/wentch/redkale/service/MultiService.java b/src/com/wentch/redkale/service/MultiService.java new file mode 100644 index 000000000..4a39fd6d8 --- /dev/null +++ b/src/com/wentch/redkale/service/MultiService.java @@ -0,0 +1,14 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.service; + +/** + * + * @author zhangjx + */ +public interface MultiService extends Service { + +} diff --git a/src/com/wentch/redkale/service/RemoteOn.java b/src/com/wentch/redkale/service/RemoteOn.java new file mode 100644 index 000000000..e17491332 --- /dev/null +++ b/src/com/wentch/redkale/service/RemoteOn.java @@ -0,0 +1,42 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.service; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * * @author zhangjx + */ +/* + * 只能标识在Service类的方法上, 且Service类被实例成RemoteService时才有效。 + * 被@RemoteOn 标记的xxx方法必须存在onXxx方法, 且参数喝返回值必须一致, onXxx方法必须声明为public final。 且onXxx方法不会被RemoteService重载。 + * 例如: + * public class XXXService implements Service { + * + * @Resource + * private HashMap nodemaps; + * + * @RemoteOn + * public void send(XXXBean bean){ + * nodemaps.forEach((x, y) -> {if(y != this) y.send(bean);}); + * } + * + * public final void onSend(XXXBean bean){ + * ... + * } + * } + * + * 如果没有public final void onSend(XXXBean bean)方法,生成RemoteService会抛出异常。 + */ +@Inherited +@Documented +@Target({METHOD, TYPE}) +@Retention(RUNTIME) +public @interface RemoteOn { + +} diff --git a/src/com/wentch/redkale/service/Service.java b/src/com/wentch/redkale/service/Service.java new file mode 100644 index 000000000..b2ac63afa --- /dev/null +++ b/src/com/wentch/redkale/service/Service.java @@ -0,0 +1,29 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.service; + +import com.wentch.redkale.util.AnyValue; + +/** + * 所有Service的实现类不得声明为final, 允许远程模式的public方法不能声明为final。 + * + * @author zhangjx + */ +public interface Service { + + /** + * 该方法必须是可以重复调用, 当reload时需要重复调用init方法 + * + * @param config + */ + default void init(AnyValue config) { + + } + + default void destroy(AnyValue config) { + + } +} diff --git a/src/com/wentch/redkale/source/DataCacheListener.java b/src/com/wentch/redkale/source/DataCacheListener.java new file mode 100644 index 000000000..0fef119f2 --- /dev/null +++ b/src/com/wentch/redkale/source/DataCacheListener.java @@ -0,0 +1,21 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import java.io.Serializable; + +/** + * + * @author zhangjx + */ +public interface DataCacheListener { + + public void insert(String sourceName, Class clazz, T... entitys); + + public void update(String sourceName, Class clazz, T... entitys); + + public void delete(String sourceName, Class clazz, Serializable... ids); +} diff --git a/src/com/wentch/redkale/source/DataConnection.java b/src/com/wentch/redkale/source/DataConnection.java new file mode 100644 index 000000000..6ff5bae12 --- /dev/null +++ b/src/com/wentch/redkale/source/DataConnection.java @@ -0,0 +1,29 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +/** + * + * @author zhangjx + */ +public abstract class DataConnection { + + private final Object connection; + + protected DataConnection(Object connection) { + this.connection = connection; + } + + protected T getConnection() { + return (T) this.connection; + } + + public abstract boolean commit(); + + public abstract void rollback(); + + public abstract void close(); +} diff --git a/src/com/wentch/redkale/source/DataJDBCSource.java b/src/com/wentch/redkale/source/DataJDBCSource.java new file mode 100644 index 000000000..bd41405f0 --- /dev/null +++ b/src/com/wentch/redkale/source/DataJDBCSource.java @@ -0,0 +1,2075 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import com.wentch.redkale.source.DataSource; +import com.wentch.redkale.source.DistributeGenerator.DistributeTables; +import static com.wentch.redkale.source.FilterInfo.formatToString; +import com.wentch.redkale.util.*; +import java.io.*; +import java.lang.ref.*; +import java.lang.reflect.*; +import java.lang.reflect.Array; +import java.net.*; +import java.nio.file.*; +import static java.nio.file.StandardWatchEventKinds.*; +import java.sql.*; +import java.util.*; +import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import java.util.function.*; +import java.util.logging.*; +import javax.annotation.*; +import javax.persistence.*; +import javax.sql.*; +import javax.xml.stream.*; + +/** + * + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public final class DataJDBCSource implements DataSource { + + public static final String DATASOURCE_CONFPATH = "DATASOURCE_CONFPATH"; + + private static final String JDBC_CONNECTIONMAX = "javax.persistence.connection.limit"; + + private static final String JDBC_URL = "javax.persistence.jdbc.url"; + + private static final String JDBC_USER = "javax.persistence.jdbc.user"; + + private static final String JDBC_PWD = "javax.persistence.jdbc.password"; + + private static final String JDBC_DRIVER = "javax.persistence.jdbc.driver"; + + private static final String JDBC_SOURCE = "javax.persistence.jdbc.source"; + + private final Logger logger = Logger.getLogger(DataJDBCSource.class.getSimpleName()); + + private final AtomicBoolean debug = new AtomicBoolean(logger.isLoggable(Level.FINEST)); + + private final String name; + + private final URL conf; + + private final JDBCPoolSource readPool; + + private final JDBCPoolSource writePool; + + final List cacheClasses = new ArrayList<>(); + + @Resource(name = "property.datasource.nodeid") + private int nodeid; + + @Resource + DataSQLListener writeListener; + + @Resource + DataCacheListener cacheListener; + + private static class DataJDBCConnection extends DataConnection { + + private final Connection sqlconn; + + private DataJDBCConnection(Connection c) { + super(c); + this.sqlconn = c; + try { + this.sqlconn.setAutoCommit(true); + } catch (Exception e) { + //do nothing + } + } + + @Override + public void close() { + try { + sqlconn.close(); + } catch (Exception e) { + //do nothing + } + } + + @Override + public boolean commit() { + try { + sqlconn.commit(); + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public void rollback() { + try { + sqlconn.rollback(); + } catch (Exception e) { + //do nothing + } + } + } + + public DataJDBCSource() throws IOException { + this(""); + } + + public DataJDBCSource(final String unitName) throws IOException { + this(unitName, System.getProperty(DATASOURCE_CONFPATH) == null + ? DataJDBCSource.class.getResource("/META-INF/persistence.xml") + : new File(System.getProperty(DATASOURCE_CONFPATH)).toURI().toURL()); + } + + public DataJDBCSource(final String unitName, URL url) throws IOException { + if (url == null) url = this.getClass().getResource("/persistence.xml"); + InputStream in = url.openStream(); + Map map = loadProperties(in); + Properties readprop = null; + Properties writeprop = null; + if (unitName != null) { + readprop = map.get(unitName); + writeprop = readprop; + if (readprop == null) { + readprop = map.get(unitName + ".read"); + writeprop = map.get(unitName + ".write"); + } + } + if ((unitName == null || unitName.isEmpty()) || readprop == null) { + String key = null; + for (Map.Entry en : map.entrySet()) { + key = en.getKey(); + readprop = en.getValue(); + writeprop = readprop; + break; + } + if (key != null && (key.endsWith(".read") || key.endsWith(".write"))) { + if (key.endsWith(".read")) { + writeprop = map.get(key.substring(0, key.lastIndexOf('.')) + ".write"); + } else { + readprop = map.get(key.substring(0, key.lastIndexOf('.')) + ".read"); + } + } + } + if (readprop == null) throw new RuntimeException("not found persistence properties (unit:" + unitName + ")"); + this.name = unitName; + this.conf = url; + this.readPool = new JDBCPoolSource(this, "read", readprop); + this.writePool = new JDBCPoolSource(this, "write", writeprop); + for (Map.Entry en : readprop.entrySet()) { + if ("cache".equalsIgnoreCase(en.getValue().toString())) { + try { + cacheClasses.add(Class.forName(en.getKey().toString())); + } catch (Exception e) { + } + } + } + } + + public DataJDBCSource(String unitName, Properties readprop, Properties writeprop) { + this.name = unitName; + this.conf = null; + this.readPool = new JDBCPoolSource(this, "read", readprop); + this.writePool = new JDBCPoolSource(this, "write", writeprop); + for (Map.Entry en : readprop.entrySet()) { + if ("cache".equalsIgnoreCase(en.getValue().toString())) { + try { + cacheClasses.add(Class.forName(en.getKey().toString())); + } catch (Exception e) { + } + } + } + } + + public static Map create(final InputStream in) { + Map map = loadProperties(in); + Map maps = new HashMap<>(); + map.entrySet().stream().forEach((en) -> { + if (en.getKey().endsWith(".read") || en.getKey().endsWith(".write")) { + String key = en.getKey().substring(0, en.getKey().lastIndexOf('.')); + if (maps.containsKey(key)) return; + boolean read = en.getKey().endsWith(".read"); + Properties rp = read ? en.getValue() : map.get(key + ".read"); + Properties wp = read ? map.get(key + ".write") : en.getValue(); + maps.put(key, new Properties[]{rp, wp}); + } else { + maps.put(en.getKey(), new Properties[]{en.getValue(), en.getValue()}); + } + }); + Map result = new HashMap<>(); + maps.entrySet().stream().forEach((en) -> { + result.put(en.getKey(), new DataJDBCSource(en.getKey(), en.getValue()[0], en.getValue()[1])); + }); + return result; + } + + private static Map loadProperties(final InputStream in0) { + final Map map = new LinkedHashMap(); + Properties result = new Properties(); + boolean flag = false; + try (final InputStream in = in0) { + XMLStreamReader reader = XMLInputFactory.newFactory().createXMLStreamReader(in); + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + if ("persistence-unit".equalsIgnoreCase(reader.getLocalName())) { + if (!result.isEmpty()) result = new Properties(); + map.put(reader.getAttributeValue(null, "name"), result); + flag = true; + } else if (flag && "property".equalsIgnoreCase(reader.getLocalName())) { + String name = reader.getAttributeValue(null, "name"); + String value = reader.getAttributeValue(null, "value"); + if (name == null) continue; + result.put(name, value); + } else if (result.getProperty(JDBC_URL) != null) { + } + } + } + in.close(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + return map; + } + + private static ConnectionPoolDataSource createDataSource(Properties property) { + try { + return createDataSource(property.getProperty(JDBC_SOURCE, property.getProperty(JDBC_DRIVER)), + property.getProperty(JDBC_URL), property.getProperty(JDBC_USER), property.getProperty(JDBC_PWD)); + } catch (Exception ex) { + throw new RuntimeException("(" + property + ") have no jdbc parameters", ex); + } + } + + private static ConnectionPoolDataSource createDataSource(final String source0, String url, String user, String password) throws Exception { + String source = source0; + if (source0.contains("Driver")) { //为了兼容JPA的配置文件 + switch (source0) { + case "org.mariadb.jdbc.Driver": + source = "org.mariadb.jdbc.MySQLDataSource"; + break; + case "com.mysql.jdbc.Driver": + source = "com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource"; + break; + case "oracle.jdbc.driver.OracleDriver": + source = "oracle.jdbc.pool.OracleConnectionPoolDataSource"; + break; + case "com.microsoft.sqlserver.jdbc.SQLServerDriver": + source = "com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource"; + break; + } + } + final Class clazz = Class.forName(source); + Object pdsource = clazz.newInstance(); + Method seturlm; + try { + seturlm = clazz.getMethod("setUrl", String.class); + } catch (Exception e) { + seturlm = clazz.getMethod("setURL", String.class); + } + seturlm.invoke(pdsource, url); + clazz.getMethod("setUser", String.class).invoke(pdsource, user); + clazz.getMethod("setPassword", String.class).invoke(pdsource, password); + return (ConnectionPoolDataSource) pdsource; + } + + @Override + public DataConnection createReadConnection() { + return new DataJDBCConnection(createReadSQLConnection()); + } + + @Override + public DataConnection createWriteConnection() { + return new DataJDBCConnection(createWriteSQLConnection()); + } + + public void close() { + readPool.close(); + writePool.close(); + } + + public String getName() { + return name; + } + + private Connection createReadSQLConnection() { + return readPool.poll(); + } + + private Connection createWriteSQLConnection() { + return writePool.poll(); + } + + private void closeSQLConnection(final Connection sqlconn) { + try { + sqlconn.close(); + } catch (Exception e) { + logger.log(Level.WARNING, "closeSQLConnection abort", e); + } + } + + public void execute(String... sqls) { + Connection conn = createWriteSQLConnection(); + try { + execute(conn, sqls); + } finally { + closeSQLConnection(conn); + } + } + + public void execute(final DataConnection conn, String... sqls) { + execute((Connection) conn.getConnection(), sqls); + } + + private void execute(final Connection conn, String... sqls) { + if (sqls.length == 0) return; + try { + final Statement stmt = conn.createStatement(); + for (String sql : sqls) { + stmt.execute(sql); + } + stmt.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * 将entity的对象全部加载到Cache中去,如果clazz没有被@javax.persistence.Cacheable注解则不做任何事 + *

+ * @param + * @param clazz + */ + @Override + public void refreshCache(Class clazz) { + EntityInfo info = EntityInfo.load(clazz, this); + EntityCache cache = info.getCache(); + if (cache == null) return; + cache.clear(); + List all = queryList(clazz, null); + cache.fullLoad(all); + } + + //----------------------insert----------------------------- + /** + * 新增对象, 必须是Entity对象 + * + * @param + * @param values + */ + @Override + public void insert(T... values) { + Connection conn = createWriteSQLConnection(); + try { + insert(conn, values); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 新增对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + @Override + public void insert(final DataConnection conn, T... values) { + insert((Connection) conn.getConnection(), values); + } + + private void insert(final Connection conn, T... values) { + if (values.length == 0) return; + try { + final Class clazz = (Class) values[0].getClass(); + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + final String sql = info.insert.sql; + if (debug.get()) logger.finest(clazz.getSimpleName() + " insert sql=" + sql); + final PreparedStatement prestmt = info.autoGenerated + ? conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) : conn.prepareStatement(sql); + final Class primaryType = info.getPrimaryType(); + final Attribute primary = info.getPrimary(); + final boolean distributed = info.distributed; + Attribute[] attrs = info.insert.attributes; + String[] sqls = null; + if (distributed && !info.initedPrimaryValue && primaryType.isPrimitive()) { + synchronized (info) { + if (!info.initedPrimaryValue) { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT MAX(" + info.getPrimarySQLColumn() + ") FROM " + info.getTable()); + if (rs.next()) { + if (primaryType == int.class) { + int v = rs.getInt(1) / info.allocationSize; + if (v > info.primaryValue.get()) info.primaryValue.set(v); + } else { + long v = rs.getLong(1) / info.allocationSize; + if (v > info.primaryValue.get()) info.primaryValue.set(v); + } + } + rs.close(); + stmt.close(); + if (info.distributeTables != null) { + for (final Class t : info.distributeTables) { + EntityXInfo infox = EntityXInfo.load(this, t); + stmt = conn.createStatement(); + rs = stmt.executeQuery("SELECT MAX(" + infox.getPrimarySQLColumn() + ") FROM " + infox.getTable()); + if (rs.next()) { + if (primaryType == int.class) { + int v = rs.getInt(1) / info.allocationSize; + if (v > info.primaryValue.get()) info.primaryValue.set(v); + } else { + long v = rs.getLong(1) / info.allocationSize; + if (v > info.primaryValue.get()) info.primaryValue.set(v); + } + } + rs.close(); + stmt.close(); + } + } + info.initedPrimaryValue = true; + } + } + } + if (writeListener == null) { + for (final T value : values) { + int i = 0; + if (distributed) info.createPrimaryValue(value); + for (Attribute attr : attrs) { + prestmt.setObject(++i, attr.get(value)); + } + prestmt.addBatch(); + } + } else { + char[] sqlchars = sql.toCharArray(); + sqls = new String[values.length]; + String[] ps = new String[attrs.length]; + int index = 0; + for (final T value : values) { + int i = 0; + if (distributed) info.createPrimaryValue(value); + for (Attribute attr : attrs) { + Object a = attr.get(value); + ps[i] = formatToString(a); + prestmt.setObject(++i, a); + } + prestmt.addBatch(); + //----------------------------- + StringBuilder sb = new StringBuilder(128); + i = 0; + for (char ch : sqlchars) { + if (ch == '?') { + sb.append(ps[i++]); + } else { + sb.append(ch); + } + } + sqls[index++] = sb.toString(); + } + } + prestmt.executeBatch(); + if (writeListener != null) writeListener.insert(name, sqls); + if (info.autoGenerated) { + ResultSet set = prestmt.getGeneratedKeys(); + int i = -1; + while (set.next()) { + if (primaryType == int.class) { + primary.set(values[++i], set.getInt(1)); + } else if (primaryType == long.class) { + primary.set(values[++i], set.getLong(1)); + } else { + primary.set(values[++i], set.getObject(1)); + } + } + set.close(); + } + if (cache != null) { + for (final T value : values) { + cache.insert(value); + } + if (cacheListener != null) cacheListener.insert(name, clazz, values); + } + prestmt.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void insertCache(T... values) { + if (values.length == 0) return; + final EntityXInfo info = EntityXInfo.load(this, (Class) values[0].getClass()); + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + for (T value : values) { + cache.insert(value); + } + } + + //-------------------------delete-------------------------- + /** + * 删除对象, 必须是Entity对象 + * + * @param + * @param values + */ + @Override + public void delete(T... values) { + Connection conn = createWriteSQLConnection(); + try { + delete(conn, values); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 删除对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + @Override + public void delete(final DataConnection conn, T... values) { + delete((Connection) conn.getConnection(), values); + } + + private void delete(final Connection conn, T... values) { + if (values.length == 0) return; + final Class clazz = values[0].getClass(); + final EntityXInfo info = EntityXInfo.load(this, clazz); + final Attribute primary = info.inner.getPrimary(); + Serializable[] ids = new Serializable[values.length]; + int i = 0; + for (final T value : values) { + ids[i++] = (Serializable) primary.get(value); + } + delete(conn, clazz, ids); + } + + /** + * 根据主键值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param ids 主键值 + */ + @Override + public void delete(Class clazz, Serializable... ids) { + Connection conn = createWriteSQLConnection(); + try { + delete(conn, clazz, ids); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 根据主键值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param ids + */ + @Override + public void delete(final DataConnection conn, Class clazz, Serializable... ids) { + delete((Connection) conn.getConnection(), clazz, ids); + } + + private void delete(final Connection conn, Class clazz, Serializable... ids) { + deleteByColumn(conn, clazz, EntityXInfo.load(this, clazz).getPrimaryField(), ids); + } + + /** + * 根据column字段的值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param column + * @param keys + */ + @Override + public void deleteByColumn(Class clazz, String column, Serializable... keys) { + Connection conn = createWriteSQLConnection(); + try { + deleteByColumn(conn, clazz, column, keys); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 根据column字段的值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param column + * @param keys + */ + @Override + public void deleteByColumn(final DataConnection conn, Class clazz, String column, Serializable... keys) { + deleteByColumn((Connection) conn.getConnection(), clazz, column, keys); + } + + private void deleteByColumn(final Connection conn, Class clazz, String column, Serializable... keys) { + if (keys.length == 0) return; + try { + final EntityXInfo info = EntityXInfo.load(this, clazz); + String sql = "DELETE FROM " + info.getTable() + " WHERE " + info.getSQLColumn(column); + if (keys.length == 1) { + sql += " = " + formatToString(keys[0]); + } else { + sql += " IN ("; + boolean flag = false; + for (final Serializable value : keys) { + if (flag) sql += ","; + sql += formatToString(value); + flag = true; + } + sql += ")"; + } + if (debug.get()) logger.finest(clazz.getSimpleName() + " delete sql=" + sql); + final Statement stmt = conn.createStatement(); + stmt.execute(sql); + stmt.close(); + if (writeListener != null) writeListener.delete(name, sql); + //------------------------------------ + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + final Attribute attr = info.getAttribute(column); + Serializable[] ids = cache.delete((T t) -> Arrays.binarySearch(keys, attr.get(t)) >= 0); + if (cacheListener != null) cacheListener.delete(name, clazz, ids); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * 根据两个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + */ + @Override + public void deleteByTwoColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + Connection conn = createWriteSQLConnection(); + try { + deleteByTwoColumn(conn, clazz, column1, key1, column2, key2); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 根据两个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + */ + @Override + public void deleteByTwoColumn(final DataConnection conn, Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + deleteByTwoColumn((Connection) conn.getConnection(), clazz, column1, key1, column2, key2); + } + + private void deleteByTwoColumn(final Connection conn, Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + try { + final EntityXInfo info = EntityXInfo.load(this, clazz); + String sql = "DELETE FROM " + info.getTable() + " WHERE " + info.getSQLColumn(column1) + " = " + formatToString(key1) + + " AND " + info.getSQLColumn(column2) + " = " + formatToString(key2); + if (debug.get()) logger.finest(clazz.getSimpleName() + " delete sql=" + sql); + final Statement stmt = conn.createStatement(); + stmt.execute(sql); + stmt.close(); + if (writeListener != null) writeListener.delete(name, sql); + //------------------------------------ + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + final Attribute attr1 = info.getAttribute(column1); + final Attribute attr2 = info.getAttribute(column2); + Serializable[] ids = cache.delete((T t) -> key1.equals(attr1.get(t)) && key2.equals(attr2.get(t))); + if (cacheListener != null) cacheListener.delete(name, clazz, ids); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void deleteCache(Class clazz, Serializable... ids) { + if (ids.length == 0) return; + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + for (Serializable id : ids) { + cache.delete(id); + } + } + + //------------------------update--------------------------- + /** + * 更新对象, 必须是Entity对象 + * + * @param + * @param values + */ + @Override + public void update(T... values) { + Connection conn = createWriteSQLConnection(); + try { + update(conn, values); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 更新对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + @Override + public void update(final DataConnection conn, T... values) { + update((Connection) conn.getConnection(), values); + } + + private void update(final Connection conn, T... values) { + try { + Class clazz = values[0].getClass(); + final EntityXInfo info = EntityXInfo.load(this, clazz); + if (debug.get()) logger.finest(clazz.getSimpleName() + " update sql=" + info.update.sql); + final PreparedStatement prestmt = conn.prepareStatement(info.update.sql); + Attribute[] attrs = info.update.attributes; + String[] sqls = null; + if (writeListener == null) { + for (final T value : values) { + int i = 0; + for (Attribute attr : attrs) { + prestmt.setObject(++i, attr.get(value)); + } + prestmt.addBatch(); + } + } else { + char[] sqlchars = info.update.sql.toCharArray(); + sqls = new String[values.length]; + String[] ps = new String[attrs.length]; + int index = 0; + for (final T value : values) { + int i = 0; + for (Attribute attr : attrs) { + Object a = attr.get(value); + ps[i] = formatToString(a); + prestmt.setObject(++i, a); + } + prestmt.addBatch(); + //----------------------------- + StringBuilder sb = new StringBuilder(128); + i = 0; + for (char ch : sqlchars) { + if (ch == '?') { + sb.append(ps[i++]); + } else { + sb.append(ch); + } + } + sqls[index++] = sb.toString(); + } + } + prestmt.executeBatch(); + prestmt.close(); + if (writeListener != null) writeListener.update(name, sqls); + //--------------------------------------------------- + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + for (final T value : values) { + cache.update(value); + } + if (cacheListener != null) cacheListener.update(name, clazz, values); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * 根据主键值更新对象的column对应的值, 必须是Entity Class + * + * @param + * @param clazz + * @param id + * @param column + * @param value + */ + @Override + public void updateColumn(Class clazz, Serializable id, String column, Serializable value) { + Connection conn = createWriteSQLConnection(); + try { + updateColumn(conn, clazz, id, column, value); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 根据主键值更新对象的column对应的值, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param id + * @param column + * @param value + */ + @Override + public void updateColumn(DataConnection conn, Class clazz, Serializable id, String column, Serializable value) { + updateColumn((Connection) conn.getConnection(), clazz, id, column, value); + } + + private void updateColumn(Connection conn, Class clazz, Serializable id, String column, Serializable value) { + try { + final EntityXInfo info = EntityXInfo.load(this, clazz); + String sql = "UPDATE " + info.getTable() + " SET " + info.getSQLColumn(column) + " = " + formatToString(value) + " WHERE " + info.getPrimarySQLColumn() + " = " + formatToString(id); + if (debug.get()) logger.finest(clazz.getSimpleName() + " update sql=" + sql); + final Statement stmt = conn.createStatement(); + stmt.execute(sql); + stmt.close(); + if (writeListener != null) writeListener.update(name, sql); + //--------------------------------------------------- + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + T rs = cache.update(id, (Attribute) info.getAttribute(column), value); + if (cacheListener != null) cacheListener.update(name, clazz, rs); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + if (conn != null) closeSQLConnection(conn); + } + } + + /** + * 根据主键值给对象的column对应的值+incvalue, 必须是Entity Class + * + * @param + * @param clazz + * @param id + * @param column + * @param incvalue + */ + @Override + public void updateColumnIncrement(Class clazz, Serializable id, String column, long incvalue) { + Connection conn = createWriteSQLConnection(); + try { + updateColumnIncrement(conn, clazz, id, column, incvalue); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 根据主键值给对象的column对应的值+incvalue, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param id + * @param column + * @param incvalue + */ + @Override + public void updateColumnIncrement(DataConnection conn, Class clazz, Serializable id, String column, long incvalue) { + updateColumnIncrement((Connection) conn.getConnection(), clazz, id, column, incvalue); + } + + private void updateColumnIncrement(Connection conn, Class clazz, Serializable id, String column, long incvalue) { + try { + final EntityXInfo info = EntityXInfo.load(this, clazz); + String col = info.getSQLColumn(column); + String sql = "UPDATE " + info.getTable() + " SET " + col + " = " + col + " + (" + incvalue + ") WHERE " + info.getPrimarySQLColumn() + " = " + formatToString(id); + if (debug.get()) logger.finest(clazz.getSimpleName() + " update sql=" + sql); + final Statement stmt = conn.createStatement(); + stmt.execute(sql); + stmt.close(); + if (writeListener != null) writeListener.update(name, sql); + //--------------------------------------------------- + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + Attribute attr = (Attribute) info.getAttribute(column); + T value = find(clazz, id); + if (value == null) return; + cache.update(id, attr, attr.get(value)); + if (cacheListener != null) cacheListener.update(name, clazz, value); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + if (conn != null) closeSQLConnection(conn); + } + } + + /** + * 更新对象指定的一些字段, 必须是Entity对象 + * + * @param + * @param value + * @param columns + */ + @Override + public void updateColumns(final T value, final String... columns) { + Connection conn = createWriteSQLConnection(); + try { + updateColumns(conn, value, columns); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 更新对象指定的一些字段, 必须是Entity对象 + * + * @param + * @param conn + * @param value + * @param columns + */ + @Override + public void updateColumns(final DataConnection conn, final T value, final String... columns) { + updateColumns((Connection) conn.getConnection(), value, columns); + } + + private void updateColumns(final Connection conn, final T value, final String... columns) { + if (columns.length < 1) return; + try { + final Class clazz = (Class) value.getClass(); + final EntityXInfo info = EntityXInfo.load(this, clazz); + StringBuilder setsql = new StringBuilder(); + Attribute[] attrs = new Attribute[columns.length]; + int i = - 1; + final Serializable id = (Serializable) info.getPrimary().get(value); + for (String col : columns) { + if (setsql.length() > 0) setsql.append(','); + attrs[++i] = info.getAttribute(col); + setsql.append(info.getSQLColumn(col)).append(" = ") + .append(formatToString(attrs[i].get(value))); + } + String sql = "UPDATE " + info.getTable() + " SET " + setsql + " WHERE " + info.getPrimarySQLColumn() + " = " + + formatToString(id); + if (debug.get()) logger.finest(value.getClass().getSimpleName() + ": " + sql); + final Statement stmt = conn.createStatement(); + stmt.execute(sql); + stmt.close(); + if (writeListener != null) writeListener.update(name, sql); + //--------------------------------------------------- + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + cache.update(value, attrs); + if (cacheListener != null) cacheListener.update(name, clazz, value); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void updateCache(Class clazz, T... values) { + if (values.length == 0) return; + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + for (T value : values) { + cache.update(value); + } + } + + public void reloadCache(Class clazz, Serializable... ids) { + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache == null) return; + for (Serializable id : ids) { + T value = find(clazz, false, id); + if (value != null) cache.update(value); + } + } + + //-----------------------getSingleResult----------------------------- + //-----------------------------MAX----------------------------- + @Override + public Number getMaxSingleResult(final Class entityClass, final String column) { + return getMaxSingleResult(entityClass, column, null); + } + + @Override + public Number getMaxSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.MAX, entityClass, column, bean); + } + + //-----------------------------MIN----------------------------- + @Override + public Number getMinSingleResult(final Class entityClass, final String column) { + return getMinSingleResult(entityClass, column, null); + } + + @Override + public Number getMinSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.MIN, entityClass, column, bean); + } + + //-----------------------------SUM----------------------------- + @Override + public Number getSumSingleResult(final Class entityClass, final String column) { + return getSumSingleResult(entityClass, column, null); + } + + @Override + public Number getSumSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.SUM, entityClass, column, bean); + } + + //----------------------------COUNT---------------------------- + @Override + public Number getCountSingleResult(final Class entityClass) { + return getCountSingleResult(entityClass, null); + } + + @Override + public Number getCountSingleResult(final Class entityClass, FilterBean bean) { + return getSingleResult(ReckonType.COUNT, entityClass, null, bean); + } + + //-----------------------------AVG----------------------------- + @Override + public Number getAvgSingleResult(final Class entityClass, final String column) { + return getAvgSingleResult(entityClass, column, null); + } + + @Override + public Number getAvgSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.AVG, entityClass, column, bean); + } + + private Number getSingleResult(final ReckonType type, final Class entityClass, final String column, FilterBean bean) { + final Connection conn = createReadSQLConnection(); + try { + final EntityXInfo info = EntityXInfo.load(this, entityClass); + final String sql = "SELECT " + type.getReckonColumn("a." + column) + " FROM " + info.getTable() + " a" + createWhereExpression(info, null, bean); + if (debug.get()) logger.finest(entityClass.getSimpleName() + " single sql=" + sql); + final PreparedStatement prestmt = conn.prepareStatement(sql); + Number rs = null; + ResultSet set = prestmt.executeQuery(); + if (set.next()) { + rs = (Number) set.getObject(1); + } + set.close(); + prestmt.close(); + return rs; + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + if (conn != null) closeSQLConnection(conn); + } + } + + //-----------------------find---------------------------- + /** + * 根据主键获取对象 + * + * @param + * @param clazz + * @param pk + * @return + */ + @Override + public T find(Class clazz, Serializable pk) { + return find(clazz, true, pk); + } + + private T find(Class clazz, boolean readcache, Serializable pk) { + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (readcache && cache != null) { + T r = cache.find(pk); + if (r != null || cache.isFullLoaded()) return r; + } + final Connection conn = createReadSQLConnection(); + try { + if (debug.get()) logger.finest(clazz.getSimpleName() + " find sql=" + info.query.sql.replace("?", String.valueOf(pk))); + final PreparedStatement prestmt = conn.prepareStatement(info.query.sql); + prestmt.setObject(1, pk); + T rs = null; + ResultSet set = prestmt.executeQuery(); + if (set.next()) { + rs = info.createInstance(); + for (Attribute attr : info.query.attributes) { + attr.set(rs, set.getObject(attr.field())); + } + } + set.close(); + prestmt.close(); + return rs; + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + if (conn != null) closeSQLConnection(conn); + } + } + + /** + * 根据主键值集合获取对象集合 + * + * @param + * @param clazz + * @param ids + * @return + */ + @Override + public T[] find(Class clazz, Serializable... ids) { + EntityXInfo info = EntityXInfo.load(this, clazz); + return findByColumn(clazz, info, null, info.getPrimarySQLColumn(), ids); + } + + /** + * 根据唯一索引获取单个对象 + * + * @param + * @param clazz + * @param column + * @param key + * @return + */ + @Override + public T findByColumn(Class clazz, String column, Serializable key) { + EntityXInfo info = EntityXInfo.load(this, clazz); + return findByColumn(clazz, info, null, info.getSQLColumn(column), key)[0]; + } + + /** + * 根据两个字段的值获取单个对象 + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + * @return + */ + @Override + public T findByTwoColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache != null) { + final Attribute attr1 = info.getAttribute(column1); + final Attribute attr2 = info.getAttribute(column2); + T r = cache.find((T t) -> key1.equals(attr1.get(t)) && key2.equals(attr2.get(t))); + if (r != null || cache.isFullLoaded()) return r; + } + final Connection conn = createReadSQLConnection(); + try { + final String sql = "SELECT * FROM " + info.getTable() + " WHERE " + info.getSQLColumn(column1) + " = ? AND " + info.getSQLColumn(column2) + " = ?"; + if (debug.get()) logger.finest(clazz.getSimpleName() + " find sql=" + sql.replaceFirst("\\?", String.valueOf(key1)).replaceFirst("\\?", String.valueOf(key2))); + final PreparedStatement prestmt = conn.prepareStatement(sql); + prestmt.setObject(1, key1); + prestmt.setObject(2, key2); + T rs = null; + ResultSet set = prestmt.executeQuery(); + if (set.next()) { + rs = info.createInstance(); + for (AttributeX attr : info.query.attributes) { + attr.setValue(null, rs, set); + } + } + set.close(); + prestmt.close(); + return rs; + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + if (conn != null) closeSQLConnection(conn); + } + } + + /** + * 根据唯一索引获取对象 + * + * @param + * @param clazz + * @param column + * @param keys + * @return + */ + @Override + public T[] findByColumn(Class clazz, String column, Serializable... keys) { + return findByColumn(clazz, (SelectColumn) null, column, keys); + } + + /** + * 根据字段值拉去对象, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects 只拉起指定字段名或者排除指定字段名的值 + * @param column + * @param keys + * @return + */ + @Override + + public T[] findByColumn(Class clazz, final SelectColumn selects, String column, Serializable... keys) { + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache != null) { + Attribute idattr = info.getAttribute(column); + List list = cache.queryList(selects, (x) -> Arrays.binarySearch(keys, idattr.get(x)) >= 0, null); + final T[] rs = (T[]) Array.newInstance(clazz, keys.length); + if (!list.isEmpty()) { + for (int i = 0; i < rs.length; i++) { + T item = null; + for (T s : list) { + if (keys[i].equals(idattr.get(s))) { + item = s; + break; + } + } + rs[i] = item; + } + } + if (!list.isEmpty() || cache.isFullLoaded()) return rs; + } + return findByColumn(clazz, info, selects, info.getSQLColumn(column), keys); + } + + private T[] findByColumn(Class clazz, final EntityXInfo info, final SelectColumn selects, String sqlcolumn, Serializable... keys) { + if (keys.length < 1) return (T[]) Array.newInstance(clazz, 0); + final Connection conn = createReadSQLConnection(); + try { + final String sql = "SELECT * FROM " + info.getTable() + " WHERE " + sqlcolumn + " = ?"; + if (debug.get()) logger.finest(clazz.getSimpleName() + " query sql=" + sql); + final PreparedStatement prestmt = conn.prepareStatement(sql); + T[] rs = (T[]) Array.newInstance(clazz, keys.length); + for (int i = 0; i < keys.length; i++) { + prestmt.clearParameters(); + prestmt.setObject(1, keys[i]); + T one = null; + ResultSet set = prestmt.executeQuery(); + if (set.next()) { + one = info.createInstance(); + for (AttributeX attr : info.query.attributes) { + attr.setValue(selects, one, set); + } + } + rs[i] = one; + set.close(); + } + prestmt.close(); + return rs; + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + if (conn != null) closeSQLConnection(conn); + } + } + + //-----------------------list---------------------------- + /** + * 根据指定字段值查询对象某个字段的集合 + * + * @param + * @param + * @param selectedColumn + * @param clazz + * @param column + * @param key + * @return + */ + @Override + public List queryColumnList(String selectedColumn, Class clazz, String column, Serializable key) { + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache != null) { + final Attribute attr = info.getAttribute(column); + List list = cache.queryList(null, (T t) -> key.equals(attr.get(t)), null); + final List rs = new ArrayList<>(); + if (!list.isEmpty()) { + final Attribute selected = (Attribute) info.getAttribute(selectedColumn); + for (T t : list) { + rs.add(selected.get(t)); + } + } + if (!rs.isEmpty() || cache.isFullLoaded()) return rs; + } + final Connection conn = createReadSQLConnection(); + try { + final List list = new ArrayList(); + final String sql = "SELECT " + info.getSQLColumn(selectedColumn) + " FROM " + info.getTable() + " WHERE " + info.getSQLColumn(column) + " = ?"; + if (debug.get()) logger.finest(clazz.getSimpleName() + " query sql=" + sql.replaceFirst("\\?", String.valueOf(key))); + final PreparedStatement ps = conn.prepareStatement(sql); + ps.setObject(1, key); + final ResultSet set = ps.executeQuery(); + while (set.next()) { + list.add((V) set.getObject(1)); + } + set.close(); + ps.close(); + return list; + } catch (Exception ex) { + throw new RuntimeException(ex); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 根据指定字段值查询对象集合 + * + * @param + * @param clazz + * @param column + * @param key + * @return + */ + @Override + public List queryList(Class clazz, String column, Serializable key) { + return queryList(clazz, (SelectColumn) null, column, key); + } + + @Override + public List queryList(Class clazz, String column, FilterExpress express, Serializable key) { + return queryList(clazz, (SelectColumn) null, column, express, key); + } + + /** + * 根据指定字段值查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param column + * @param key + * @return + */ + @Override + public List queryList(Class clazz, final SelectColumn selects, String column, Serializable key) { + return queryList(clazz, selects, column, FilterExpress.EQUAL, key); + } + + /** + * 根据指定字段值查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param column + * @param express + * @param key + * @return + */ + @Override + public List queryList(Class clazz, final SelectColumn selects, String column, FilterExpress express, Serializable key) { + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache != null) { + final Attribute attr = info.getAttribute(column); + Predicate filter = null; + switch (express) { + case EQUAL: + filter = (T t) -> key.equals(attr.get(t)); + break; + case NOTEQUAL: + filter = (T t) -> !key.equals(attr.get(t)); + break; + case GREATERTHAN: + filter = (T t) -> ((Number) attr.get(t)).longValue() > ((Number) key).longValue(); + break; + case LESSTHAN: + filter = (T t) -> ((Number) attr.get(t)).longValue() < ((Number) key).longValue(); + break; + case GREATERTHANOREQUALTO: + filter = (T t) -> ((Number) attr.get(t)).longValue() >= ((Number) key).longValue(); + break; + case LESSTHANOREQUALTO: + filter = (T t) -> ((Number) attr.get(t)).longValue() <= ((Number) key).longValue(); + break; + case LIKE: + filter = (T t) -> { + Object rs = attr.get(t); + return rs != null && rs.toString().contains(key.toString()); + }; + break; + case NOTLIKE: + filter = (T t) -> { + Object rs = attr.get(t); + return rs == null || !rs.toString().contains(key.toString()); + }; + break; + case ISNULL: + filter = (T t) -> attr.get(t) == null; + break; + case ISNOTNULL: + filter = (T t) -> attr.get(t) != null; + break; + case OPAND: + filter = (T t) -> (((Number) attr.get(t)).longValue() & ((Number) key).longValue()) > 0; + break; + case OPOR: + filter = (T t) -> (((Number) attr.get(t)).longValue() | ((Number) key).longValue()) > 0; + break; + case OPANDNO: + filter = (T t) -> (((Number) attr.get(t)).longValue() & ((Number) key).longValue()) == 0; + break; + } + List rs = cache.queryList(selects, filter, null); + if (!rs.isEmpty() || cache.isFullLoaded()) return rs; + } + final Connection conn = createReadSQLConnection(); + try { + final SelectColumn sels = selects; + final List list = new ArrayList(); + final String sql = "SELECT * FROM " + info.getTable() + " WHERE " + info.getSQLColumn(column) + " " + express.value() + " ?"; + if (debug.get()) logger.finest(clazz.getSimpleName() + " query sql=" + sql); + final PreparedStatement ps = conn.prepareStatement(sql); + ps.setObject(1, key); + final ResultSet set = ps.executeQuery(); + while (set.next()) { + final T result = info.createInstance(); + for (AttributeX attr : info.query.attributes) { + attr.setValue(sels, result, set); + } + list.add(result); + } + set.close(); + ps.close(); + return list; + } catch (Exception ex) { + throw new RuntimeException(ex); + } finally { + closeSQLConnection(conn); + } + } + + /** + * 根据过滤对象FilterBean查询对象集合 + * + * @param + * @param clazz + * @param bean + * @return + */ + @Override + public List queryList(final Class clazz, final FilterBean bean) { + return queryList(clazz, null, bean); + } + + /** + * 根据过滤对象FilterBean查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param bean + * @return + */ + @Override + public List queryList(final Class clazz, final SelectColumn selects, final FilterBean bean) { + return querySheet(clazz, selects, null, bean).list(true); + } + + //-----------------------sheet---------------------------- + /** + * 根据过滤对象FilterBean和翻页对象Flipper查询一页的数据 + * + * @param + * @param clazz + * @param flipper + * @param bean + * @return + */ + @Override + public Sheet querySheet(Class clazz, final Flipper flipper, final FilterBean bean) { + return querySheet(clazz, null, flipper, bean); + } + + /** + * 根据过滤对象FilterBean和翻页对象Flipper查询一页的数据, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param flipper + * @param bean + * @return + */ + @Override + public Sheet querySheet(Class clazz, final SelectColumn selects, final Flipper flipper, final FilterBean bean) { + final EntityXInfo info = EntityXInfo.load(this, clazz); + final EntityCache cache = info.inner.getCache(); + if (cache != null) { + Predicate filter = null; + Comparator sort = null; + boolean valid = true; + if (bean != null) { + FilterInfo finfo = FilterInfo.load(bean.getClass(), this); + valid = finfo.isValidCacheJoin(); + if (valid) { + filter = finfo.getFilterPredicate(info.inner, bean); + sort = finfo.getSortComparator(info.inner, flipper); + } + } + if (valid) { + Sheet sheet = cache.querySheet(selects, filter, flipper, sort); + if (!sheet.isEmpty() || cache.isFullLoaded()) return sheet; + } + } + final Connection conn = createReadSQLConnection(); + try { + final SelectColumn sels = selects; + final List list = new ArrayList(); + final String sql = "SELECT a.* FROM " + info.getTable() + " a" + createWhereExpression(info, flipper, bean); + if (debug.get()) logger.finest(clazz.getSimpleName() + " query sql=" + sql + (flipper == null ? "" : (" LIMIT " + flipper.index() + "," + flipper.getSize()))); + final PreparedStatement ps = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + final ResultSet set = ps.executeQuery(); + if (flipper != null && flipper.index() > 0) set.absolute(flipper.index()); + final int limit = flipper == null ? Integer.MAX_VALUE : flipper.getSize(); + int i = 0; + long total; + while (set.next()) { + i++; + final T result = info.createInstance(); + for (AttributeX attr : info.query.attributes) { + attr.setValue(sels, result, set); + } + list.add(result); + if (limit <= i) break; + } + if (flipper != null) { + set.last(); + total = set.getRow(); + } else { + total = list.size(); + } + set.close(); + ps.close(); + return new Sheet<>(total, list); + } catch (Exception ex) { + throw new RuntimeException(ex); + } finally { + closeSQLConnection(conn); + } + } + + private String createWhereExpression(final EntityXInfo info, final Flipper flipper, final FilterBean bean) { + if (bean == null && flipper == null) return ""; + boolean emptySort = flipper == null || flipper.getSort() == null || flipper.getSort().isEmpty(); + StringBuilder where = null; + boolean join = false; + if (bean != null) { + final FilterInfo filter = FilterInfo.load(bean.getClass(), this); + join = filter.isJoin(); + where = filter.createWhereSql(info.getPrimarySQLColumn(), bean); + } + if (emptySort) return where == null ? "" : where.toString(); + if (where == null) where = new StringBuilder(); + where.append(" ORDER BY "); + if (info.same && !join) { + where.append(flipper.getSort()); + } else { + boolean flag = false; + for (String item : flipper.getSort().split(",")) { + if (item.isEmpty()) continue; + String[] sub = item.split("\\s+"); + if (flag) where.append(','); + if (sub.length < 2 || sub[1].equalsIgnoreCase("ASC")) { + where.append("a.").append(info.getSQLColumn(sub[0])).append(" ASC"); + } else { + where.append("a.").append(info.getSQLColumn(sub[0])).append(" DESC"); + } + flag = true; + } + } + return where.toString(); + } + + //---------------------------------------------------------------------- + public static final class JDBCPoolSource { + + private static final Map>>> maps = new HashMap<>(); + + private final AtomicLong usingCounter = new AtomicLong(); + + private final AtomicLong creatCounter = new AtomicLong(); + + private final AtomicLong cycleCounter = new AtomicLong(); + + private final AtomicLong saveCounter = new AtomicLong(); + + private final ConnectionPoolDataSource source; + + private final ArrayBlockingQueue queue; + + private final ConnectionEventListener listener; + + private final DataJDBCSource dataSource; + + private final String stype; // "" 或 "read" 或 "write" + + private final int max; + + private String url; + + private String user; + + private String password; + + public JDBCPoolSource(DataJDBCSource source, String stype, Properties prop) { + this.dataSource = source; + this.stype = stype; + this.source = createDataSource(prop); + this.url = prop.getProperty(JDBC_URL); + this.user = prop.getProperty(JDBC_USER); + this.password = prop.getProperty(JDBC_PWD); + this.max = Integer.decode(prop.getProperty(JDBC_CONNECTIONMAX, "" + Runtime.getRuntime().availableProcessors() * 16)); + this.queue = new ArrayBlockingQueue<>(this.max); + this.listener = new ConnectionEventListener() { + + @Override + public void connectionClosed(ConnectionEvent event) { + PooledConnection pc = (PooledConnection) event.getSource(); + if (queue.offer(pc)) saveCounter.incrementAndGet(); + } + + @Override + public void connectionErrorOccurred(ConnectionEvent event) { + usingCounter.decrementAndGet(); + if ("08S01".equals(event.getSQLException().getSQLState())) return; //MySQL特性, 长时间连接没使用会抛出com.mysql.jdbc.exceptions.jdbc4.CommunicationsException + dataSource.logger.log(Level.WARNING, "connectionErronOccurred", event.getSQLException()); + } + }; + try { + this.watch(); + } catch (Exception e) { + dataSource.logger.log(Level.WARNING, DataSource.class.getSimpleName() + " watch " + dataSource.conf + " error", e); + } + } + + private void watch() throws IOException { + if (dataSource.conf == null || dataSource.name == null) return; + final String file = dataSource.conf.getFile(); + final File f = new File(file); + if (!f.isFile() || !f.canRead()) return; + synchronized (maps) { + SimpleEntry>> entry = maps.get(file); + if (entry != null) { + entry.getValue().add(new WeakReference<>(this)); + return; + } + final WatchService watcher = f.toPath().getFileSystem().newWatchService(); + final List> list = new CopyOnWriteArrayList<>(); + Thread watchThread = new Thread() { + + @Override + public void run() { + try { + while (!this.isInterrupted()) { + final WatchKey key = watcher.take(); + Thread.sleep(3000); //防止文件正在更新过程中去读取 + final Map m = loadProperties(new FileInputStream(file)); + key.pollEvents().stream().forEach((event) -> { + if (event.kind() != ENTRY_MODIFY) return; + if (!((Path) event.context()).toFile().getName().equals(f.getName())) return; + for (WeakReference ref : list) { + JDBCPoolSource pool = ref.get(); + if (pool == null) continue; + try { + Properties property = m.get(pool.dataSource.name); + if (property == null) property = m.get(pool.dataSource.name + "." + pool.stype); + if (property != null) pool.change(property); + } catch (Exception ex) { + dataSource.logger.log(Level.INFO, event.context() + " occur error", ex); + } + } + }); + key.reset(); + } + } catch (Exception e) { + dataSource.logger.log(Level.WARNING, "DataSource watch " + file + " occur error", e); + } + } + }; + f.getParentFile().toPath().register(watcher, ENTRY_MODIFY); + watchThread.setName("DataSource-Watch-" + maps.size() + "-Thread"); + watchThread.setDaemon(true); + watchThread.start(); + dataSource.logger.log(Level.FINER, watchThread.getName() + " start watching " + file); + //----------------------------------------------------------- + list.add(new WeakReference<>(this)); + maps.put(file, new SimpleEntry<>(watcher, list)); + } + } + + public void change(Properties property) { + Method seturlm; + Class clazz = source.getClass(); + String newurl = property.getProperty(JDBC_URL); + String newuser = property.getProperty(JDBC_USER); + String newpassword = property.getProperty(JDBC_PWD); + if (this.url.equals(newurl) && this.user.equals(newuser) && this.password.equals(newpassword)) return; + try { + try { + seturlm = clazz.getMethod("setUrl", String.class); + } catch (Exception e) { + seturlm = clazz.getMethod("setURL", String.class); + } + seturlm.invoke(source, newurl); + clazz.getMethod("setUser", String.class).invoke(source, newuser); + clazz.getMethod("setPassword", String.class).invoke(source, newpassword); + this.url = newurl; + this.user = newuser; + this.password = newpassword; + dataSource.logger.log(Level.INFO, DataSource.class.getSimpleName() + "(" + dataSource.name + "." + stype + ") change (" + property + ")"); + } catch (Exception e) { + dataSource.logger.log(Level.SEVERE, DataSource.class.getSimpleName() + " dynamic change JDBC (url userName password) error", e); + } + } + + public Connection poll() { + return poll(0, null); + } + + private Connection poll(final int count, SQLException e) { + if (count >= 3) { + dataSource.logger.log(Level.WARNING, "create pooled connection error", e); + throw new RuntimeException(e); + } + PooledConnection result = queue.poll(); + if (result == null) { + if (usingCounter.get() >= max) { + try { + result = queue.poll(6, TimeUnit.SECONDS); + } catch (Exception t) { + dataSource.logger.log(Level.WARNING, "take pooled connection error", t); + } + } + if (result == null) { + try { + result = source.getPooledConnection(); + result.addConnectionEventListener(listener); + usingCounter.incrementAndGet(); + } catch (SQLException ex) { + return poll(count + 1, ex); + } + creatCounter.incrementAndGet(); + } + } else { + cycleCounter.incrementAndGet(); + } + Connection conn; + try { + conn = result.getConnection(); + if (!conn.isValid(1)) { + dataSource.logger.info("sql connection is not vaild"); + usingCounter.decrementAndGet(); + return poll(0, null); + } + } catch (SQLException ex) { + dataSource.logger.log(Level.FINER, "result.getConnection from pooled connection abort", ex); + return poll(0, null); + } + return conn; + } + + public long getCreatCount() { + return creatCounter.longValue(); + } + + public long getCycleCount() { + return cycleCounter.longValue(); + } + + public long getSaveCount() { + return saveCounter.longValue(); + } + + public void close() { + queue.stream().forEach(x -> { + try { + x.close(); + } catch (Exception e) { + } + }); + } + } + + //---------------------------------------------------------------------- + private static class AttributeX implements Attribute { + + private final Class type; + + private final Attribute attribute; + + private final String fieldName; + + public AttributeX(Class type, Attribute attribute, String fieldname) { + this.type = type; + this.attribute = attribute; + this.fieldName = fieldname; + } + + @Override + public String field() { + return attribute.field(); + } + + @Override + public F get(T obj) { + return attribute.get(obj); + } + + @Override + public void set(T obj, F value) { + Object o = value; + if (o != null) { + if (type == long.class) { + o = ((Number) o).longValue(); + } else if (type == int.class) { + o = ((Number) o).intValue(); + } else if (type == short.class) { + o = ((Number) o).shortValue(); + } + } + attribute.set(obj, (F) o); + } + + public void setValue(SelectColumn sels, T obj, ResultSet set) throws SQLException { + if (sels == null || sels.validate(this.fieldName)) { + Object o = set.getObject(this.attribute.field()); + if (o != null) { + if (type == long.class) { + o = ((Number) o).longValue(); + } else if (type == int.class) { + o = ((Number) o).intValue(); + } else if (type == short.class) { + o = ((Number) o).shortValue(); + } + } + attribute.set(obj, (F) o); + } + } + } + + private static class EntityXInfo { + + private static final ConcurrentHashMap entityxInfos = new ConcurrentHashMap<>(); + + private final int nodeid; + + private final EntityInfo inner; + + final Class[] distributeTables; + + final boolean autoGenerated; + + final boolean distributed; + + boolean initedPrimaryValue = false; + + final AtomicLong primaryValue = new AtomicLong(0); + + final int allocationSize; + + private final ActionInfo query; + + private final ActionInfo insert; + + private final ActionInfo update; + + private final ActionInfo delete; + + //表字段与字段名是否全部一致 + private final boolean same; + + private class ActionInfo { + + final String sql; + + final AttributeX[] attributes; + + public ActionInfo(String sql, List> list) { + this.sql = sql; + this.attributes = list.toArray(new AttributeX[list.size()]); + } + + public ActionInfo(String sql, AttributeX... attributes) { + this.sql = sql; + this.attributes = attributes; + } + } + + public EntityXInfo(DataJDBCSource source, Class type) { + this.inner = EntityInfo.load(type, source); + this.nodeid = source.nodeid; + DistributeTables dt = type.getAnnotation(DistributeTables.class); + this.distributeTables = dt == null ? null : dt.value(); + + Class cltmp = type; + Set fields = new HashSet<>(); + boolean auto = false; + boolean sqldistribute = false; + int allocationSize0 = 0; + String wheresql = ""; + List> queryattrs = new ArrayList<>(); + List insertcols = new ArrayList<>(); + List> insertattrs = new ArrayList<>(); + List updatecols = new ArrayList<>(); + List> updateattrs = new ArrayList<>(); + boolean same0 = true; + Class idfieldtype = int.class; + do { + for (Field field : cltmp.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + if (Modifier.isFinal(field.getModifiers())) continue; + if (field.getAnnotation(Transient.class) != null) continue; + final String fieldname = field.getName(); + if (fields.contains(fieldname)) continue; + fields.add(fieldname); + final Column col = field.getAnnotation(Column.class); + final String sqlfield = col == null || col.name().isEmpty() ? fieldname : col.name(); + if (same0) same0 = fieldname.equals(sqlfield); + final Class fieldtype = field.getType(); + Attribute attribute = inner.getAttribute(fieldname); + if (attribute == null) continue; + AttributeX attr = new AttributeX(fieldtype, attribute, fieldname); + if (field.getAnnotation(Id.class) != null) { + idfieldtype = fieldtype; + GeneratedValue gv = field.getAnnotation(GeneratedValue.class); + auto = gv != null; + if (gv != null && gv.strategy() != GenerationType.IDENTITY) { + throw new RuntimeException(cltmp.getName() + "'s @ID primary not a GenerationType.IDENTITY"); + } + DistributeGenerator dg = field.getAnnotation(DistributeGenerator.class); + if (dg != null) { + if (!fieldtype.isPrimitive()) throw new RuntimeException(cltmp.getName() + "'s @DistributeGenerator primary must be primitive class type field"); + sqldistribute = true; + auto = false; + allocationSize0 = dg.allocationSize(); + primaryValue.set(dg.initialValue()); + } + wheresql = " WHERE " + sqlfield + " = ?"; + if (!auto) { + insertcols.add(sqlfield); + insertattrs.add(attr); + } + } else { + if (col == null || col.insertable()) { + insertcols.add(sqlfield); + insertattrs.add(attr); + } + if (col == null || col.updatable()) { + updatecols.add(sqlfield); + updateattrs.add(attr); + } + } + queryattrs.add(attr); + } + } while ((cltmp = cltmp.getSuperclass()) != Object.class); + AttributeX idxattr = new AttributeX(idfieldtype, inner.getPrimary(), inner.getPrimaryField()); + updateattrs.add(idxattr); + this.autoGenerated = auto; + this.delete = new ActionInfo("DELETE FROM " + inner.getTable() + wheresql, idxattr); + StringBuilder updatesb = new StringBuilder(); + for (String col : updatecols) { + if (updatesb.length() > 0) updatesb.append(','); + updatesb.append(col).append(" = ?"); + } + this.update = new ActionInfo("UPDATE " + inner.getTable() + " SET " + updatesb + wheresql, updateattrs); + StringBuilder insertsb = new StringBuilder(); + StringBuilder insertsb2 = new StringBuilder(); + for (String col : insertcols) { + if (insertsb.length() > 0) insertsb.append(','); + insertsb.append(col); + if (insertsb2.length() > 0) insertsb2.append(','); + insertsb2.append('?'); + } + String insertsql = "INSERT INTO " + inner.getTable() + "(" + insertsb + ") VALUES(" + insertsb2 + ")"; + this.same = same0; + this.distributed = sqldistribute; + this.allocationSize = allocationSize0; + this.insert = new ActionInfo(insertsql, insertattrs); + this.query = new ActionInfo("SELECT * FROM " + inner.getTable() + wheresql, queryattrs); + } + + public static EntityXInfo load(DataJDBCSource source, Class clazz) { + EntityXInfo rs = entityxInfos.get(clazz); + if (rs != null) return rs; + synchronized (entityxInfos) { + rs = entityxInfos.get(clazz); + if (rs == null) { + rs = new EntityXInfo(source, clazz); + entityxInfos.put(clazz, rs); + } + return rs; + } + } + + public T createInstance() { + return inner.getCreator().create(); + } + + public void createPrimaryValue(T src) { + long v = primaryValue.incrementAndGet() * allocationSize + nodeid; + Class p = inner.getPrimaryType(); + if (p == int.class || p == Integer.class) { + getPrimary().set(src, (Integer) ((Long) v).intValue()); + } else { + getPrimary().set(src, v); + } + } + + public Class getPrimaryType() { + return inner.getPrimaryType(); + } + + public Attribute getPrimary() { + return inner.getPrimary(); + } + + public String getPrimaryField() { + return inner.getPrimaryField(); + } + + public String getTable() { + return inner.getTable(); + } + + public String getPrimarySQLColumn() { + return inner.getPrimary().field(); + } + + public String getSQLColumn(String fieldname) { + if (same) return fieldname; + return inner.getAttribute(fieldname).field(); + } + + public Attribute getAttribute(String fieldname) { + return inner.getAttribute(fieldname); + } + } + + private static enum ReckonType { + + MAX, MIN, SUM, COUNT, AVG; + + public String getReckonColumn(String col) { + if (this == COUNT) return this.name() + "(*)"; + return this.name() + "(" + col + ")"; + } + } +} diff --git a/src/com/wentch/redkale/source/DataJPASource.java b/src/com/wentch/redkale/source/DataJPASource.java new file mode 100644 index 000000000..e9c566cca --- /dev/null +++ b/src/com/wentch/redkale/source/DataJPASource.java @@ -0,0 +1,1092 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import com.wentch.redkale.util.Sheet; +import com.wentch.redkale.util.Attribute; +import static com.wentch.redkale.source.FilterExpress.*; +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.*; +import javax.persistence.*; +import javax.persistence.criteria.*; + +/** + * 不再完全实现JPA版的DataSource + *

+ * @author zhangjx + */ +@Deprecated +final class DataJPASource implements DataSource { + + protected final EntityManagerFactory factory; + + private final AtomicBoolean debug = new AtomicBoolean(false); + + private final Logger logger = Logger.getLogger(DataJPASource.class.getSimpleName()); + + @Override + public void updateColumnIncrement(Class clazz, Serializable id, String column, long incvalue) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void updateColumnIncrement(DataConnection conn, Class clazz, Serializable id, String column, long incvalue) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void refreshCache(Class clazz) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + private static class DataJPAConnection extends DataConnection { + + private final EntityManager manager; + + private DataJPAConnection(EntityManager m) { + super(m); + this.manager = m; + } + + @Override + public void close() { + manager.close(); + } + + @Override + public boolean commit() { + try { + manager.getTransaction().commit(); + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public void rollback() { + manager.getTransaction().rollback(); + } + } + + public DataJPASource() { + this(""); + } + + public DataJPASource(final String unitName) { + factory = Persistence.createEntityManagerFactory(unitName); + setDebug(System.getProperty("javax.persistence.debug") != null); + } + + public static DataJPASource create() { + return new DataJPASource(""); + } + + public static DataJPASource create(final String unitName) { + return new DataJPASource(unitName); + } + + private void setDebug(boolean flag) { + this.debug.set(flag); + if (flag) logger.setLevel(Level.FINEST); + } + + @Override + public DataConnection createReadConnection() { + return new DataJPAConnection(factory.createEntityManager()); + } + + @Override + public DataConnection createWriteConnection() { + return new DataJPAConnection(factory.createEntityManager()); + } + + public void close() { + this.factory.close(); + } + //----------------------insert----------------------------- + + /** + * 新增对象, 必须是Entity对象 + * + * @param + * @param values + */ + @Override + public void insert(T... values) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + for (T value : values) { + manager.persist(value); + } + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 新增对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + @Override + public void insert(final DataConnection conn, T... values) { + final EntityManager manager = conn.getConnection(); + for (T value : values) { + manager.persist(value); + } + } + + //-------------------------delete-------------------------- + /** + * 删除对象, 必须是Entity对象 + * + * @param + * @param values + */ + @Override + public void delete(T... values) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + delete(manager, values); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 删除对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + @Override + public void delete(final DataConnection conn, T... values) { + final EntityManager manager = conn.getConnection(); + delete(manager, values); + } + + private void delete(final EntityManager manager, T... values) { + for (T value : values) { + manager.remove(value); + } + } + + /** + * 根据主键值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param ids 主键值 + */ + @Override + public void delete(Class clazz, Serializable... ids) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + delete(manager, clazz, ids); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 根据主键值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param ids + */ + @Override + public void delete(final DataConnection conn, Class clazz, Serializable... ids) { + final EntityManager manager = conn.getConnection(); + delete(manager, clazz, ids); + } + + private void delete(final EntityManager manager, Class clazz, Serializable... ids) { + for (Serializable id : ids) { + Object value = manager.find(clazz, id); + if (value != null) manager.remove(value); + } + } + + /** + * 根据column字段的值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param column + * @param keys + */ + @Override + public void deleteByColumn(Class clazz, String column, Serializable... keys) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + deleteByColumn(manager, clazz, column, keys); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 根据column字段的值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param column + * @param keys + */ + @Override + public void deleteByColumn(final DataConnection conn, Class clazz, String column, Serializable... keys) { + final EntityManager manager = conn.getConnection(); + deleteByColumn(manager, clazz, column, keys); + } + + private void deleteByColumn(final EntityManager manager, Class clazz, String column, Serializable... keys) { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaDelete cd = builder.createCriteriaDelete(clazz); + cd.where(cd.getRoot().get(column).in((Object[]) keys)); + manager.createQuery(cd).executeUpdate(); + } + + /** + * 根据两个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + */ + @Override + public void deleteByTwoColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + deleteByTwoColumn(manager, clazz, column1, key1, column2, key2); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 根据两个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + */ + @Override + public void deleteByTwoColumn(final DataConnection conn, Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + deleteByTwoColumn((EntityManager) conn.getConnection(), clazz, column1, key1, column2, key2); + } + + private void deleteByTwoColumn(final EntityManager manager, Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaDelete cd = builder.createCriteriaDelete(clazz); + final Root root = cd.getRoot(); + cd.where(builder.and(builder.equal(root.get(column1), key1), builder.equal(root.get(column2), key2))); + manager.createQuery(cd).executeUpdate(); + } + + /** + * 根据三个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + * @param column3 + * @param key3 + */ + public void deleteByThreeColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2, String column3, Serializable key3) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + deleteByThreeColumn(manager, clazz, column1, key1, column2, key2, column3, key3); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 根据三个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + * @param column3 + * @param key3 + */ + public void deleteByThreeColumn(final DataConnection conn, Class clazz, String column1, Serializable key1, String column2, Serializable key2, String column3, Serializable key3) { + deleteByThreeColumn((EntityManager) conn.getConnection(), clazz, column1, key1, column2, key2, column3, key3); + } + + private void deleteByThreeColumn(final EntityManager manager, Class clazz, String column1, Serializable key1, String column2, Serializable key2, String column3, Serializable key3) { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaDelete cd = builder.createCriteriaDelete(clazz); + final Root root = cd.getRoot(); + cd.where(builder.and(builder.equal(root.get(column1), key1), builder.equal(root.get(column2), key2), builder.equal(root.get(column3), key3))); + manager.createQuery(cd).executeUpdate(); + } + //------------------------update--------------------------- + + /** + * 更新对象, 必须是Entity对象 + * + * @param + * @param values + */ + @Override + public void update(T... values) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + update(manager, values); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 更新对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + @Override + public void update(final DataConnection conn, T... values) { + final EntityManager manager = conn.getConnection(); + update(manager, values); + } + + private void update(final EntityManager manager, T... values) { + for (T value : values) { + manager.merge(value); + } + } + + /** + * 根据主键值更新对象的column对应的值, 必须是Entity Class + * + * @param + * @param clazz + * @param id + * @param column + * @param value + */ + @Override + public void updateColumn(Class clazz, Serializable id, String column, Serializable value) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + updateColumn(manager, clazz, id, column, value); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 根据主键值更新对象的column对应的值, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param id + * @param column + * @param value + */ + @Override + public void updateColumn(final DataConnection conn, Class clazz, Serializable id, String column, Serializable value) { + final EntityManager manager = conn.getConnection(); + updateColumn(manager, clazz, id, column, value); + } + + private void updateColumn(final EntityManager manager, Class clazz, Serializable id, String column, Serializable value) { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaUpdate cd = builder.createCriteriaUpdate(clazz); + cd.set(column, value); + cd.where(builder.equal(cd.from(clazz).get(EntityInfo.load(clazz, this).getPrimaryField()), id)); + manager.createQuery(cd).executeUpdate(); + } + + /** + * 更新对象指定的一些字段, 必须是Entity对象 + * + * @param + * @param value + * @param columns + */ + @Override + public void updateColumns(final T value, final String... columns) { + final EntityManager manager = factory.createEntityManager(); + try { + manager.getTransaction().begin(); + updateColumns(manager, value, columns); + manager.getTransaction().commit(); + } finally { + manager.close(); + } + } + + /** + * 更新对象指定的一些字段, 必须是Entity对象 + * + * @param + * @param conn + * @param value + * @param columns + */ + @Override + public void updateColumns(final DataConnection conn, final T value, final String... columns) { + final EntityManager manager = conn.getConnection(); + updateColumns(manager, value, columns); + } + + private void updateColumns(final EntityManager manager, final T value, final String... columns) { + final Class clazz = value.getClass(); + final EntityInfo info = EntityInfo.load(clazz, this); + final Attribute idattr = info.getPrimary(); + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + final CriteriaUpdate cd = builder.createCriteriaUpdate(clazz); + for (String column : columns) { + cd.set(column, info.getAttribute(column).get(value)); + } + cd.where(builder.equal(cd.from(clazz).get(info.getPrimaryField()), idattr.get(value))); + manager.createQuery(cd).executeUpdate(); + } + + //-----------------------getSingleResult----------------------------- + //-----------------------------MAX----------------------------- + @Override + public Number getMaxSingleResult(final Class entityClass, final String column) { + return getMaxSingleResult(entityClass, column, null); + } + + @Override + public Number getMaxSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.MAX, entityClass, column, bean); + } + + //-----------------------------MIN----------------------------- + @Override + public Number getMinSingleResult(final Class entityClass, final String column) { + return getMinSingleResult(entityClass, column, null); + } + + @Override + public Number getMinSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.MIN, entityClass, column, bean); + } + + //-----------------------------SUM----------------------------- + @Override + public Number getSumSingleResult(final Class entityClass, final String column) { + return getSumSingleResult(entityClass, column, null); + } + + @Override + public Number getSumSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.SUM, entityClass, column, bean); + } + + //----------------------------COUNT---------------------------- + @Override + public Number getCountSingleResult(final Class entityClass) { + return getCountSingleResult(entityClass, null); + } + + @Override + public Number getCountSingleResult(final Class entityClass, FilterBean bean) { + return getSingleResult(ReckonType.COUNT, entityClass, null, bean); + } + + //-----------------------------AVG----------------------------- + @Override + public Number getAvgSingleResult(final Class entityClass, final String column) { + return getAvgSingleResult(entityClass, column, null); + } + + @Override + public Number getAvgSingleResult(final Class entityClass, final String column, FilterBean bean) { + return getSingleResult(ReckonType.AVG, entityClass, column, bean); + } + + private Number getSingleResult(final ReckonType type, final Class entityClass, final String column, FilterBean bean) { + final EntityManager manager = factory.createEntityManager(); + try { + String sql = "SELECT " + type.name() + "(a." + column + ") FROM " + entityClass.getSimpleName() + " a"; + if (debug.get()) logger.finer(entityClass.getSimpleName() + " single sql=" + sql); + return manager.createQuery(sql, Number.class).getSingleResult(); + } finally { + manager.close(); + } + } + + private Number getSingleResultCriteria(final ReckonType type, final Class entityClass, final String column, FilterBean bean) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + final CriteriaQuery cry = builder.createQuery(Number.class); + final Root root = cry.from(entityClass); + Expression where = bean == null ? null : createWhereExpression(builder, root, bean); + if (where != null) cry.where(where); + switch (type) { + case MAX: cry.select(builder.max(root.get(column))); + break; + case MIN: cry.select(builder.min(root.get(column))); + break; + case SUM: cry.select(builder.sum(root.get(column))); + break; + case AVG: cry.select(builder.avg(root.get(column))); + break; + case COUNT: cry.select(builder.count(column == null ? root : root.get(column))); + break; + default: throw new RuntimeException("error ReckonType"); + } + //max count + return manager.createQuery(cry).getSingleResult(); + } finally { + manager.close(); + } + } + + //-----------------------find---------------------------- + /** + * 根据主键获取对象 + * + * @param + * @param clazz + * @param pk + * @return + */ + @Override + public T find(Class clazz, Serializable pk) { + final EntityManager manager = factory.createEntityManager(); + try { + return manager.find(clazz, pk); + } finally { + manager.close(); + } + } + + /** + * 根据主键值集合获取对象集合 + * + * @param + * @param clazz + * @param ids + * @return + */ + @Override + public T[] find(Class clazz, Serializable... ids) { + final EntityManager manager = factory.createEntityManager(); + try { + T[] result = (T[]) Array.newInstance(clazz, ids.length); + int i = 0; + for (Serializable id : ids) { + result[i++] = manager.find(clazz, id); + } + return result; + } finally { + manager.close(); + } + } + + /** + * 根据唯一索引获取单个对象 + * + * @param + * @param clazz + * @param column + * @param key + * @return + */ + @Override + public T findByColumn(Class clazz, String column, Serializable key) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaQuery cd = builder.createQuery(clazz); + cd.where(builder.equal(cd.from(clazz).get(column), key)); + List list = manager.createQuery(cd).getResultList(); + return list == null || list.isEmpty() ? null : list.get(0); + } finally { + manager.close(); + } + } + + /** + * 根据两个字段的值获取单个对象 + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + * @return + */ + @Override + public T findByTwoColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + final CriteriaQuery cd = builder.createQuery(clazz); + final Root root = cd.from(clazz); + cd.where(builder.and(builder.equal(root.get(column1), key1), builder.equal(root.get(column2), key2))); + return manager.createQuery(cd).getSingleResult(); + } finally { + manager.close(); + } + } + + /** + * 根据三个字段的值获取单个对象 + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + * @param column3 + * @param key3 + * @return + */ + public T findByThreeColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2, String column3, Serializable key3) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + final CriteriaQuery cd = builder.createQuery(clazz); + final Root root = cd.from(clazz); + cd.where(builder.and(builder.equal(root.get(column1), key1), builder.equal(root.get(column2), key2), builder.equal(root.get(column3), key3))); + return manager.createQuery(cd).getSingleResult(); + } finally { + manager.close(); + } + } + + /** + * 根据唯一索引获取对象 + * + * @param + * @param clazz + * @param column + * @param keys + * @return + */ + @Override + public T[] findByColumn(Class clazz, String column, Serializable... keys) { + return findByColumn(clazz, null, column, keys); + } + + /** + * 根据字段值拉去对象, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects 只拉起指定字段名或者排除指定字段名的值 + * @param column + * @param keys + * @return + */ + @Override + public T[] findByColumn(Class clazz, final SelectColumn selects, String column, Serializable... keys) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaQuery cd = builder.createQuery(clazz); + cd.where(cd.from(clazz).get(column).in((Object[]) keys)); + List list = manager.createQuery(cd).getResultList(); + list = selectList(clazz, selects, list); + return list.toArray((T[]) Array.newInstance(clazz, list.size())); + } finally { + manager.close(); + } + } + + //-----------------------list---------------------------- + /** + * 根据指定字段值查询对象某个字段的集合 + * + * @param + * @param + * @param selectedColumn + * @param clazz + * @param column + * @param key + * @return + */ + @Override + public List queryColumnList(String selectedColumn, Class clazz, String column, Serializable key) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + final CriteriaQuery query = builder.createQuery(); + final Root root = query.from(clazz); + query.select(root.get(selectedColumn)); + query.where(builder.equal(root.get(column), key)); + return manager.createQuery(query).getResultList(); + } finally { + manager.close(); + } + } + + /** + * 根据指定字段值查询对象集合 + * + * @param + * @param clazz + * @param column + * @param key + * @return + */ + @Override + public List queryList(Class clazz, String column, Serializable key) { + return queryList(clazz, (SelectColumn) null, column, key); + } + + /** + * 根据指定字段值查询对象集合 + * + * @param + * @param clazz + * @param column + * @param express + * @param key + * @return + */ + @Override + public List queryList(Class clazz, String column, FilterExpress express, Serializable key) { + return queryList(clazz, (SelectColumn) null, column, express, key); + } + + /** + * 根据指定字段值查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param column + * @param key + * @return + */ + @Override + public List queryList(Class clazz, final SelectColumn selects, String column, Serializable key) { + return queryList(clazz, selects, column, FilterExpress.EQUAL, key); + } + + /** + * 注意: 尚未实现识别express功能 + * 根据指定字段值查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param column + * @param express + * @param key + * @return + */ + @Override + public List queryList(Class clazz, final SelectColumn selects, String column, FilterExpress express, Serializable key) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaQuery cd = builder.createQuery(clazz); + cd.where(builder.equal(cd.from(clazz).get(column), key)); + List list = manager.createQuery(cd).getResultList(); + return selectList(clazz, selects, list); + } finally { + manager.close(); + } + } + + /** + * 根据过滤对象FilterBean查询对象集合 + * + * @param + * @param clazz + * @param bean + * @return + */ + @Override + public List queryList(final Class clazz, final FilterBean bean) { + return queryList(clazz, null, bean); + } + + /** + * 根据过滤对象FilterBean查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param bean + * @return + */ + @Override + public List queryList(final Class clazz, final SelectColumn selects, final FilterBean bean) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder builder = manager.getCriteriaBuilder(); + CriteriaQuery cd = builder.createQuery(clazz); + final Expression where = createWhereExpression(builder, cd.from(clazz), bean); + if (where != null) cd.where(where); + List list = manager.createQuery(cd).getResultList(); + return selectList(clazz, selects, list); + } finally { + manager.close(); + } + } + + //-----------------------sheet---------------------------- + /** + * 根据过滤对象FilterBean和翻页对象Flipper查询一页的数据 + * + * @param + * @param clazz + * @param flipper + * @param bean + * @return + */ + @Override + public Sheet querySheet(Class clazz, final Flipper flipper, final FilterBean bean) { + return querySheet(clazz, null, flipper, bean); + } + + /** + * 根据过滤对象FilterBean和翻页对象Flipper查询一页的数据, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param flipper + * @param bean + * @return + */ + @Override + public Sheet querySheet(Class clazz, final SelectColumn selects, final Flipper flipper, final FilterBean bean) { + final EntityManager manager = factory.createEntityManager(); + try { + final CriteriaBuilder totalbd = manager.getCriteriaBuilder(); + final CriteriaQuery totalcry = totalbd.createQuery(Long.class); + final Root totalroot = totalcry.from(clazz); + totalcry.select(totalbd.count(totalroot)); + Expression totalwhere = createWhereExpression(totalbd, totalroot, bean); + if (totalwhere != null) totalcry.where(totalwhere); + long total = manager.createQuery(totalcry).getSingleResult(); + if (total < 1) return new Sheet<>(); + //------------------------------------------------ + final CriteriaBuilder listbd = manager.getCriteriaBuilder(); + final CriteriaQuery listcry = listbd.createQuery(clazz); + final Root listroot = listcry.from(clazz); + Expression listwhere = createWhereExpression(listbd, listroot, bean); + if (listwhere != null) listcry.where(listwhere); + final String sort = flipper.getSort(); + if (flipper != null && sort != null && !sort.isEmpty()) { + if (sort.indexOf(',') > 0) { + List orders = new ArrayList<>(); + for (String item : sort.split(",")) { + if (item.isEmpty()) continue; + String[] sub = item.split("\\s+"); + if (sub.length < 2 || sub[1].equalsIgnoreCase("ASC")) { + orders.add(listbd.asc(listroot.get(sub[0]))); + } else { + orders.add(listbd.desc(listroot.get(sub[0]))); + } + } + listcry.orderBy(orders); + } else { + for (String item : sort.split(",")) { + if (item.isEmpty()) continue; + String[] sub = item.split("\\s+"); + if (sub.length < 2 || sub[1].equalsIgnoreCase("ASC")) { + listcry.orderBy(listbd.asc(listroot.get(sub[0]))); + } else { + listcry.orderBy(listbd.desc(listroot.get(sub[0]))); + } + } + } + } + final TypedQuery listqry = manager.createQuery(listcry); + if (flipper != null) { + listqry.setFirstResult(flipper.index()); + listqry.setMaxResults(flipper.getSize()); + } + List list = selectList(clazz, selects, listqry.getResultList()); + return new Sheet<>(total, list); + } finally { + manager.close(); + } + } + + private List selectList(final Class clazz, final SelectColumn selects, final List list) { + if (selects == null || selects.isEmpty() || list.isEmpty()) return list; + final EntityInfo info = EntityInfo.load(clazz, this); + final Object dftValue = info.getDefaultTypeInstance(); + final Map map = info.getAttributes(); + final List attrs = new ArrayList<>(); + if (selects.isExcludable()) { + for (String col : selects.getColumns()) { + Attribute attr = map.get(col); + if (attr != null) attrs.add(attr); + } + } else { + map.entrySet().forEach(x -> { + if (!selects.validate(x.getKey())) attrs.add(x.getValue()); + }); + } + for (Object obj : list) { + for (Attribute attr : attrs) { + attr.set(obj, attr.get(dftValue)); + } + } + return list; + } + + private Expression createWhereExpression(final CriteriaBuilder builder, final Root root, final FilterBean bean) { + if (bean == null) return null; + final FilterInfo filter = FilterInfo.load(bean.getClass(), this); + final List list = new ArrayList<>(); + for (final FilterInfo.FilterItem item : filter.getFilters()) { + final FilterExpress express = item.express; + Object attrval = item.attribute.get(bean); + if (express == FilterExpress.ISNULL) { + list.add(builder.isNull(root.get(item.attribute.field()))); + continue; + } else if (express == FilterExpress.ISNOTNULL) { + list.add(builder.isNotNull(root.get(item.attribute.field()))); + continue; + } + if (attrval == null) continue; + if (item.number && ((Number) attrval).longValue() < item.least) continue; + switch (express) { + case EQUAL: + list.add(builder.equal(root.get(item.attribute.field()), attrval)); + break; + case NOTEQUAL: + list.add(builder.notEqual(root.get(item.attribute.field()), attrval)); + break; + case GREATERTHAN: + list.add(builder.greaterThan(root.get(item.attribute.field()), (Comparable) attrval)); + break; + case LESSTHAN: + list.add(builder.lessThan(root.get(item.attribute.field()), (Comparable) attrval)); + break; + case GREATERTHANOREQUALTO: + list.add(builder.greaterThanOrEqualTo(root.get(item.attribute.field()), (Comparable) attrval)); + break; + case LESSTHANOREQUALTO: + list.add(builder.lessThanOrEqualTo(root.get(item.attribute.field()), (Comparable) attrval)); + break; + case LIKE: + list.add(builder.like(root.get(item.attribute.field()), (String) (item.likefit ? ("%" + attrval + "%") : attrval))); + break; + case NOTLIKE: + list.add(builder.notLike(root.get(item.attribute.field()), (String) (item.likefit ? ("%" + attrval + "%") : attrval))); + break; + case BETWEEN: + case NOTBETWEEN: + Range range = (Range) attrval; + Predicate p = builder.between(root.get(item.attribute.field()), (Comparable) range.getMin(), (Comparable) range.getMax()); + if (NOTBETWEEN == express) { + p = builder.not(p); + } + list.add(p); + break; + case AND: + case OR: + Range[] ranges = (Range[]) attrval; + Predicate[] ps = new Predicate[ranges.length]; + int oi = 0; + for (Range r : ranges) { + ps[oi++] = builder.between(root.get(item.attribute.field()), (Comparable) r.getMin(), (Comparable) r.getMax()); + } + if (OR == express) { + list.add(builder.or(ps)); + } else { + list.add(builder.and(ps)); + } + break; + case IN: + case NOTIN: + Predicate pd = null; + if (attrval instanceof Collection) { + Collection c = (Collection) attrval; + if (!c.isEmpty()) { + pd = builder.in(root.get(item.attribute.field()).in(c)); + } + } else { + final int len = Array.getLength(attrval); + if (len > 0) { + Class comp = attrval.getClass().getComponentType(); + if (comp.isPrimitive()) { + Object[] os = new Object[len]; + for (int i = 0; i < len; i++) { + os[i] = Array.get(attrval, i); + } + pd = builder.in(root.get(item.attribute.field()).in(os)); + } else { + pd = builder.in(root.get(item.attribute.field()).in((Object[]) attrval)); + } + if (NOTIN == express) { + pd = builder.not(pd); + } + list.add(pd); + } + } + if (pd != null) { + if (NOTIN == express) { + pd = builder.not(pd); + } + list.add(pd); + } + break; + default: + throw new RuntimeException(bean.getClass() + "'s field (" + item.aliasfield + ") have a illegal express (" + express + ")"); + } + } + if (list.isEmpty()) return null; + return builder.and(list.toArray(new Predicate[list.size()])); + } + + private static enum ReckonType { + + MAX, MIN, SUM, COUNT, AVG; + } +} diff --git a/src/com/wentch/redkale/source/DataSQLListener.java b/src/com/wentch/redkale/source/DataSQLListener.java new file mode 100644 index 000000000..db89d35a6 --- /dev/null +++ b/src/com/wentch/redkale/source/DataSQLListener.java @@ -0,0 +1,20 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +/** + * @Resource(name = "property.datasource.nodeid") + * + * @author zhangjx + */ +public interface DataSQLListener { + + public void insert(String sourceName, String... sqls); + + public void update(String sourceName, String... sqls); + + public void delete(String sourceName, String... sqls); +} diff --git a/src/com/wentch/redkale/source/DataSource.java b/src/com/wentch/redkale/source/DataSource.java new file mode 100644 index 000000000..d09922f35 --- /dev/null +++ b/src/com/wentch/redkale/source/DataSource.java @@ -0,0 +1,427 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import com.wentch.redkale.util.Sheet; +import java.io.*; +import java.util.*; + +/** + * + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public interface DataSource { + + /** + * 创建读连接 + * + * @return + */ + public DataConnection createReadConnection(); + + /** + * 创建写连接 + * + * @return + */ + public DataConnection createWriteConnection(); + + //----------------------insert----------------------------- + /** + * 新增对象, 必须是Entity对象 + * + * @param + * @param values + */ + public void insert(T... values); + + /** + * 新增对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + public void insert(final DataConnection conn, T... values); + + /** + * 将entity的对象全部加载到Cache中去,如果clazz没有被@javax.persistence.Cacheable注解则不做任何事 + *

+ * @param + * @param clazz + */ + public void refreshCache(Class clazz); + + //-------------------------delete-------------------------- + /** + * 删除对象, 必须是Entity对象 + * + * @param + * @param values + */ + public void delete(T... values); + + /** + * 删除对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + public void delete(final DataConnection conn, T... values); + + /** + * 根据主键值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param ids 主键值 + */ + public void delete(Class clazz, Serializable... ids); + + /** + * 根据主键值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param ids + */ + public void delete(final DataConnection conn, Class clazz, Serializable... ids); + + /** + * 根据column字段的值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param column + * @param keys + */ + public void deleteByColumn(Class clazz, String column, Serializable... keys); + + /** + * 根据column字段的值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param column + * @param keys + */ + public void deleteByColumn(final DataConnection conn, Class clazz, String column, Serializable... keys); + + /** + * 根据两个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + */ + public void deleteByTwoColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2); + + /** + * 根据两个column字段的值删除对象, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + */ + public void deleteByTwoColumn(final DataConnection conn, Class clazz, String column1, Serializable key1, String column2, Serializable key2); + + //------------------------update--------------------------- + /** + * 更新对象, 必须是Entity对象 + * + * @param + * @param values + */ + public void update(T... values); + + /** + * 更新对象, 必须是Entity对象 + * + * @param + * @param conn + * @param values + */ + public void update(final DataConnection conn, T... values); + + /** + * 根据主键值更新对象的column对应的值, 必须是Entity Class + * + * @param + * @param clazz + * @param id + * @param column + * @param value + */ + public void updateColumn(Class clazz, Serializable id, String column, Serializable value); + + /** + * 根据主键值更新对象的column对应的值, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param id + * @param column + * @param value + */ + public void updateColumn(final DataConnection conn, Class clazz, Serializable id, String column, Serializable value); + + /** + * 根据主键值给对象的column对应的值+incvalue, 必须是Entity Class + * + * @param + * @param clazz + * @param id + * @param column + * @param incvalue + */ + public void updateColumnIncrement(Class clazz, Serializable id, String column, long incvalue); + + /** + * 根据主键值给对象的column对应的值+incvalue, 必须是Entity Class + * + * @param + * @param conn + * @param clazz + * @param id + * @param column + * @param incvalue + */ + public void updateColumnIncrement(final DataConnection conn, Class clazz, Serializable id, String column, long incvalue); + + /** + * 更新对象指定的一些字段, 必须是Entity对象 + * + * @param + * @param value + * @param columns + */ + public void updateColumns(final T value, final String... columns); + + /** + * 更新对象指定的一些字段, 必须是Entity对象 + * + * @param + * @param conn + * @param value + * @param columns + */ + public void updateColumns(final DataConnection conn, final T value, final String... columns); + + //-----------------------getSingleResult----------------------------- + //-----------------------------MAX----------------------------- + public Number getMaxSingleResult(final Class entityClass, final String column); + + public Number getMaxSingleResult(final Class entityClass, final String column, FilterBean bean); + + //-----------------------------MIN----------------------------- + public Number getMinSingleResult(final Class entityClass, final String column); + + public Number getMinSingleResult(final Class entityClass, final String column, FilterBean bean); + + //-----------------------------SUM----------------------------- + public Number getSumSingleResult(final Class entityClass, final String column); + + public Number getSumSingleResult(final Class entityClass, final String column, FilterBean bean); + + //----------------------------COUNT---------------------------- + public Number getCountSingleResult(final Class entityClass); + + public Number getCountSingleResult(final Class entityClass, FilterBean bean); + + //-----------------------------AVG----------------------------- + public Number getAvgSingleResult(final Class entityClass, final String column); + + public Number getAvgSingleResult(final Class entityClass, final String column, FilterBean bean); + + //-----------------------find---------------------------- + /** + * 根据主键获取对象 + * + * @param + * @param clazz + * @param pk + * @return + */ + public T find(Class clazz, Serializable pk); + + /** + * 根据主键值集合获取对象集合 + * + * @param + * @param clazz + * @param ids + * @return + */ + public T[] find(Class clazz, Serializable... ids); + + /** + * 根据唯一索引获取单个对象 + * + * @param + * @param clazz + * @param column + * @param key + * @return + */ + public T findByColumn(Class clazz, String column, Serializable key); + + /** + * 根据两个字段的值获取单个对象 + * + * @param + * @param clazz + * @param column1 + * @param key1 + * @param column2 + * @param key2 + * @return + */ + public T findByTwoColumn(Class clazz, String column1, Serializable key1, String column2, Serializable key2); + + /** + * 根据唯一索引获取对象 + * + * @param + * @param clazz + * @param column + * @param keys + * @return + */ + public T[] findByColumn(Class clazz, String column, Serializable... keys); + + /** + * 根据字段值拉去对象, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects 只拉起指定字段名或者排除指定字段名的值 + * @param column + * @param keys + * @return + */ + public T[] findByColumn(Class clazz, final SelectColumn selects, String column, Serializable... keys); + + //-----------------------list---------------------------- + /** + * 根据指定字段值查询对象某个字段的集合 + * + * @param + * @param + * @param selectedColumn + * @param clazz + * @param column + * @param key + * @return + */ + public List queryColumnList(String selectedColumn, Class clazz, String column, Serializable key); + + /** + * 根据指定字段值查询对象集合 + * + * @param + * @param clazz + * @param column + * @param key + * @return + */ + public List queryList(Class clazz, String column, Serializable key); + + /** + * 根据指定字段值查询对象集合 + * + * @param + * @param clazz + * @param column + * @param express + * @param key + * @return + */ + public List queryList(Class clazz, String column, FilterExpress express, Serializable key); + + //-----------------------list---------------------------- + /** + * 根据指定字段值查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param column + * @param express + * @param key + * @return + */ + public List queryList(Class clazz, final SelectColumn selects, String column, FilterExpress express, Serializable key); + + /** + * 根据指定字段值查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param column + * @param key + * @return + */ + public List queryList(Class clazz, final SelectColumn selects, String column, Serializable key); + + /** + * 根据过滤对象FilterBean查询对象集合 + * + * @param + * @param clazz + * @param bean + * @return + */ + public List queryList(final Class clazz, final FilterBean bean); + + /** + * 根据过滤对象FilterBean查询对象集合, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param bean + * @return + */ + public List queryList(final Class clazz, final SelectColumn selects, final FilterBean bean); + + //-----------------------sheet---------------------------- + /** + * 根据过滤对象FilterBean和翻页对象Flipper查询一页的数据 + * + * @param + * @param clazz + * @param flipper + * @param bean + * @return + */ + public Sheet querySheet(Class clazz, final Flipper flipper, final FilterBean bean); + + /** + * 根据过滤对象FilterBean和翻页对象Flipper查询一页的数据, 对象只填充或排除SelectColumn指定的字段 + * + * @param + * @param clazz + * @param selects + * @param flipper + * @param bean + * @return + */ + public Sheet querySheet(Class clazz, final SelectColumn selects, final Flipper flipper, final FilterBean bean); + +} diff --git a/src/com/wentch/redkale/source/DataSourceFactory.java b/src/com/wentch/redkale/source/DataSourceFactory.java new file mode 100644 index 000000000..cc2d72bd0 --- /dev/null +++ b/src/com/wentch/redkale/source/DataSourceFactory.java @@ -0,0 +1,40 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import java.io.*; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public abstract class DataSourceFactory { + + private static final Logger logger = Logger.getLogger(DataSourceFactory.class.getSimpleName()); + + public static DataSource create() { + return create(""); + } + + public static DataSource create(final String unitName) { +// boolean jpa = false; +// if (!"jdbc".equalsIgnoreCase(System.getProperty("source.type", "jpa"))) { +// try { +// jpa = ServiceLoader.load(Class.forName("javax.persistence.spi.PersistenceProvider")).iterator().hasNext(); +// } catch (Exception e) { +// jpa = false; +// } +// } +// if (jpa) return new DataJPASource(unitName); + try { + return new DataJDBCSource(unitName); + } catch (IOException ex) { + logger.log(Level.WARNING, "cannot create DataSource (" + unitName + ")", ex); + return null; + } + } +} diff --git a/src/com/wentch/redkale/source/DistributeGenerator.java b/src/com/wentch/redkale/source/DistributeGenerator.java new file mode 100644 index 000000000..1b5338e42 --- /dev/null +++ b/src/com/wentch/redkale/source/DistributeGenerator.java @@ -0,0 +1,39 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * + * @author zhangjx + */ +@Target({FIELD}) +@Retention(RUNTIME) +public @interface DistributeGenerator { + + /** + * 当使用DistributeGenerator控制主键值时, 如果表A删除的数据迁移到表B时, 就需要将表A的class标记: + * DistributeTables({B.class}) + * public class A { + * } + * 这样DistributeGenerator将从A、B表中取最大值来初始化主键值。 + * + * @author zhangjx + */ + @Target({TYPE}) + @Retention(RUNTIME) + public @interface DistributeTables { + + Class[] value(); + } + + int initialValue() default 1; + + int allocationSize() default 1000; +} diff --git a/src/com/wentch/redkale/source/EntityCache.java b/src/com/wentch/redkale/source/EntityCache.java new file mode 100644 index 000000000..7aa6e6bd9 --- /dev/null +++ b/src/com/wentch/redkale/source/EntityCache.java @@ -0,0 +1,208 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import com.wentch.redkale.util.Sheet; +import com.wentch.redkale.util.Reproduce; +import com.wentch.redkale.util.Creator; +import com.wentch.redkale.util.Attribute; +import java.io.*; +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.*; +import java.util.logging.*; +import java.util.stream.Stream; +import javax.persistence.Transient; + +/** + * + * @author zhangjx + */ +final class EntityCache { + + private static final Logger logger = Logger.getLogger(EntityCache.class.getName()); + + private final ConcurrentHashMap map = new ConcurrentHashMap(); + + private final CopyOnWriteArrayList list = new CopyOnWriteArrayList(); + + private final Class type; + + private final boolean needcopy; + + private final Creator creator; + + private final Attribute primary; + + private final Map> attributes; + + private final Reproduce reproduce; + + private boolean fullloaded; + + public EntityCache(final Class type, Creator creator, + Attribute primary, Map> attributes) { + this.type = type; + this.creator = creator; + this.primary = primary; + this.attributes = attributes; + this.needcopy = true; + this.reproduce = Reproduce.create(type, type, (m) -> { + char[] mn = m.getName().substring(3).toCharArray(); + if (mn.length < 2 || Character.isLowerCase(mn[1])) mn[0] = Character.toLowerCase(mn[0]); + try { + Field f = type.getDeclaredField(new String(mn)); + return f.getAnnotation(Transient.class) != null; + } catch (Exception e) { + return false; + } + }); + } + + public void fullLoad(List all) { + clear(); + all.stream().filter(x -> x != null).forEach(x -> this.map.put((Serializable) this.primary.get(x), x)); + this.list.addAll(all); + this.fullloaded = true; + } + + public Class getType() { + return type; + } + + public void clear() { + this.fullloaded = false; + this.list.clear(); + this.map.clear(); + } + + public boolean isFullLoaded() { + return fullloaded; + } + + public T find(Serializable id) { + if (id == null) return null; + T rs = map.get(id); + return rs == null ? null : (needcopy ? reproduce.copy(this.creator.create(), rs) : rs); + } + + public T find(final Predicate filter) { + if (filter == null) return null; + Optional rs = listStream().filter(filter).findFirst(); + return rs.isPresent() ? (needcopy ? reproduce.copy(this.creator.create(), rs.get()) : rs.get()) : null; + } + + public List queryList(final SelectColumn selects, final Predicate filter, final Comparator sort) { + boolean parallel = isParallel(); + final List rs = parallel ? new CopyOnWriteArrayList<>() : new ArrayList<>(); + Stream stream = listStream(); + if (filter != null) stream = stream.filter(filter); + if (sort != null) stream = stream.sorted(sort); + if (selects == null) { + stream.forEach(x -> rs.add(needcopy ? reproduce.copy(creator.create(), x) : x)); + } else { + final Map> attrs = this.attributes; + stream.forEach(x -> { + final T item = creator.create(); + for (Map.Entry> en : attrs.entrySet()) { + Attribute attr = en.getValue(); + if (selects.validate(en.getKey())) attr.set(item, attr.get(x)); + } + rs.add(item); + }); + } + return parallel ? new ArrayList<>(rs) : rs; + } + + public Sheet querySheet(final SelectColumn selects, final Predicate filter, final Flipper flipper, final Comparator sort) { + Stream stream = listStream(); + if (filter != null) stream = stream.filter(filter); + long total = stream.count(); + if (total == 0) return new Sheet<>(); + stream = listStream(); + if (filter != null) stream = stream.filter(filter); + if (sort != null) stream = stream.sorted(sort); + if (flipper != null) { + stream = stream.skip(flipper.index()).limit(flipper.getSize()); + } + boolean parallel = isParallel(); + final List rs = parallel ? new CopyOnWriteArrayList<>() : new ArrayList<>(); + if (selects == null) { + stream.forEach(x -> rs.add(needcopy ? reproduce.copy(creator.create(), x) : x)); + } else { + final Map> attrs = this.attributes; + stream.forEach(x -> { + final T item = creator.create(); + for (Map.Entry> en : attrs.entrySet()) { + Attribute attr = en.getValue(); + if (selects.validate(en.getKey())) attr.set(item, attr.get(x)); + } + rs.add(item); + }); + } + return new Sheet<>(total, parallel ? new ArrayList<>(rs) : rs); + } + + public void insert(T value) { + if (value == null) return; + T rs = needcopy ? reproduce.copy(this.creator.create(), value) : value; //确保同一主键值的map与list中的对象必须共用。 + T old = this.map.put((Serializable) this.primary.get(rs), rs); + if (old != null) logger.log(Level.WARNING, "cache repeat insert data: " + value); + this.list.add(rs); + } + + public void delete(final Serializable id) { + if (id == null) return; + T rs = this.map.remove(id); + if (rs != null) this.list.remove(rs); + } + + public Serializable[] delete(final Predicate filter) { + if (filter == null || this.list.isEmpty()) return new Serializable[0]; + Object[] rms = listStream().filter(filter).toArray(); + Serializable[] ids = new Serializable[rms.length]; + int i = -1; + for (Object o : rms) { + T t = (T) o; + ids[++i] = (Serializable) this.primary.get(t); + this.map.remove(ids[i]); + this.list.remove(t); + } + return ids; + } + + public void update(final T value) { + if (value == null) return; + T rs = this.map.get((Serializable) this.primary.get(value)); + if (rs == null) return; + this.reproduce.copy(rs, value); + } + + public void update(final T value, Attribute[] attrs) { + if (value == null) return; + T rs = this.map.get((Serializable) this.primary.get(value)); + if (rs == null) return; + for (Attribute attr : attrs) { + attr.set(rs, attr.get(value)); + } + } + + public T update(final Serializable id, Attribute attr, final V fieldValue) { + if (id == null) return null; + T rs = this.map.get(id); + if (rs != null) attr.set(rs, fieldValue); + return rs; + } + + public boolean isParallel() { + return this.list.size() > 1024 * 16; + } + + private Stream listStream() { + return isParallel() ? this.list.parallelStream() : this.list.stream(); + } +} diff --git a/src/com/wentch/redkale/source/EntityInfo.java b/src/com/wentch/redkale/source/EntityInfo.java new file mode 100644 index 000000000..15b9c7e67 --- /dev/null +++ b/src/com/wentch/redkale/source/EntityInfo.java @@ -0,0 +1,155 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import com.sun.istack.internal.logging.Logger; +import com.wentch.redkale.util.*; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.*; +import javax.persistence.*; + +/** + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +public final class EntityInfo { + + private static final ConcurrentHashMap entityInfos = new ConcurrentHashMap<>(); + + private static final Logger logger = Logger.getLogger(EntityInfo.class); + + private final Class type; + + private final String table; + + private final Creator creator; + + private final Class primaryType; + + private final Attribute primary; + + private final String primaryFieldName; + + private final T defaultTypeInstance; + + private final EntityCache cache; + + private final Map> attributes = new HashMap<>(); + + public static EntityInfo load(Class clazz, final DataSource source) { + EntityInfo rs = entityInfos.get(clazz); + if (rs != null) return rs; + synchronized (entityInfos) { + rs = entityInfos.get(clazz); + if (rs == null) { + final List cacheClasses = source instanceof DataJDBCSource ? ((DataJDBCSource) source).cacheClasses : null; + rs = new EntityInfo(clazz, cacheClasses); + entityInfos.put(clazz, rs); + if (rs.cache != null && source != null) { + AutoLoad auto = clazz.getAnnotation(AutoLoad.class); + if (auto != null && auto.value()) { + long s = System.currentTimeMillis(); + rs.cache.fullLoad(source.queryList(clazz, null)); + long e = System.currentTimeMillis() - s; + if (logger.isLoggable(Level.FINEST)) logger.finest(clazz.getName() + " full auto loaded for cache in " + e + " ms"); + } + } + } + return rs; + } + } + + private EntityInfo(Class type, final List cacheClasses) { + this.type = type; + Table t = type.getAnnotation(Table.class); + this.table = (t == null) ? type.getSimpleName().toLowerCase() : (t.catalog().isEmpty()) ? t.name() : (t.catalog() + '.' + t.name()); + this.creator = Creator.create(type); + this.defaultTypeInstance = this.creator.create(); + Attribute idAttr0 = null; + Class primaryType0 = null; + String primaryName = null; + Class cltmp = type; + Set fields = new HashSet<>(); + do { + for (Field field : cltmp.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + if (Modifier.isFinal(field.getModifiers())) continue; + if (field.getAnnotation(Transient.class) != null) continue; + if (fields.contains(field.getName())) continue; + final Column col = field.getAnnotation(Column.class); + final String sqlfield = col == null || col.name().isEmpty() ? field.getName() : col.name(); + Attribute attr; + try { + attr = Attribute.create(cltmp, sqlfield, field); + } catch (RuntimeException e) { + continue; + } + if (field.getAnnotation(javax.persistence.Id.class) != null) { + idAttr0 = attr; + primaryType0 = field.getType(); + primaryName = field.getName(); + } + fields.add(field.getName()); + attributes.put(field.getName(), attr); + } + } while ((cltmp = cltmp.getSuperclass()) != Object.class); + this.primary = idAttr0; + this.primaryType = primaryType0; + this.primaryFieldName = primaryName; + //----------------cache-------------- + Cacheable c = type.getAnnotation(Cacheable.class); + boolean cf = (c == null) ? (cacheClasses != null && cacheClasses.contains(type)) : false; + if ((c != null && c.value()) || cf) { + this.cache = new EntityCache<>(type, creator, primary, attributes); + } else { + this.cache = null; + } + } + + EntityCache getCache() { + return cache; + } + + public Creator getCreator() { + return creator; + } + + public Class getType() { + return type; + } + + public String getTable() { + return table; + } + + public Class getPrimaryType() { + return this.primaryType; + } + + public Attribute getPrimary() { + return this.primary; + } + + public String getPrimaryField() { + return this.primaryFieldName; + } + + public Attribute getAttribute(String fieldname) { + return this.attributes.get(fieldname); + } + + public T getDefaultTypeInstance() { + return defaultTypeInstance; + } + + public Map> getAttributes() { + return attributes; + } +} diff --git a/src/com/wentch/redkale/source/FilterBean.java b/src/com/wentch/redkale/source/FilterBean.java new file mode 100644 index 000000000..cb525fb4d --- /dev/null +++ b/src/com/wentch/redkale/source/FilterBean.java @@ -0,0 +1,15 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.wentch.redkale.source; + +/** + * + * @author zhangjx + */ +public interface FilterBean { + +} diff --git a/src/com/wentch/redkale/source/FilterColumn.java b/src/com/wentch/redkale/source/FilterColumn.java new file mode 100644 index 000000000..bf9b8b73f --- /dev/null +++ b/src/com/wentch/redkale/source/FilterColumn.java @@ -0,0 +1,60 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({FIELD}) +@Retention(RUNTIME) +public @interface FilterColumn { + + /** + * 对应Entity Class中字段的名称, 而不是SQL字段名称 + * + * @return + */ + String name() default ""; + + /** + * 当字段类型是Number时, 如果值>=least() 则需要过滤, 否则跳过该字段 + * + * @return + */ + long least() default 1; + + /** + * 当express="like" 是否把非空值首尾加上% + * + * @return + */ + boolean likefit() default true; + + /** + * LIKE、NOT LIKE时是否区分大小写 + *

+ * @return + */ + boolean ignoreCase() default false; + + /** + * express的默认值根据字段类型的不同而不同: + * 数组 --> IN + * Range --> Between + * 其他 --> = + * + * @return + */ + FilterExpress express() default FilterExpress.EQUAL; + +} diff --git a/src/com/wentch/redkale/source/FilterExpress.java b/src/com/wentch/redkale/source/FilterExpress.java new file mode 100644 index 000000000..33a372a51 --- /dev/null +++ b/src/com/wentch/redkale/source/FilterExpress.java @@ -0,0 +1,44 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +/** + * + * @author zhangjx + */ +public enum FilterExpress { + + EQUAL("="), + NOTEQUAL("<>"), + GREATERTHAN(">"), + LESSTHAN("<"), + GREATERTHANOREQUALTO(">="), + LESSTHANOREQUALTO("<="), + LIKE("LIKE"), + NOTLIKE("NOT LIKE"), + BETWEEN("BETWEEN"), + NOTBETWEEN("NOT BETWEEN"), + IN("IN"), + NOTIN("NOT IN"), + ISNULL("IS NULL"), + ISNOTNULL("IS NOT NULL"), + OPAND("&"), //与运算 > 0 + OPOR("|"), //或运算 > 0 + OPANDNO("&"), //与运算 == 0 + AND("AND"), + OR("OR"); + + private final String value; + + private FilterExpress(String v) { + this.value = v; + } + + public String value() { + return value; + } + +} diff --git a/src/com/wentch/redkale/source/FilterGroup.java b/src/com/wentch/redkale/source/FilterGroup.java new file mode 100644 index 000000000..1c0929c1e --- /dev/null +++ b/src/com/wentch/redkale/source/FilterGroup.java @@ -0,0 +1,62 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.lang.annotation.ElementType.FIELD; +import java.lang.annotation.*; + +/* + * 默认情况下FilterBean下的过滤字段之间是AND关系。 + * 当需要使用OR或AND OR组合过滤查询时需要使用 FilterGroup。 + * FilterGroup 的value 必须是[OR]或者[AND]开头, 多级需要用点.分隔。 (注: 暂时不支持多级) + * 示例一: + * public class TestFilterBean implements FilterBean { + * + * private int id; + * + * @FilterGroup("[OR]g1") + * private String desc; + * + * @FilterGroup("[OR]g1") + * private String name; + * } + * 转换的SQL语句为: WHERE id = ? AND (desc = ? OR name = ?) + * + * 示例二: + * public class TestFilterBean implements FilterBean { + * + * private int id; + * + * @FilterGroup("[OR]g1.[AND]subg1") + * @FilterColumn(express = LIKE) + * private String desc; + * + * @FilterGroup("[OR]g1.[AND]subg1") + * @FilterColumn(express = LIKE) + * private String name; + * + * @FilterGroup("[OR]g1.[OR]subg2") + * private int age; + * + * @FilterGroup("[OR]g1.[OR]subg2") + * private int birthday; + * } + * 转换的SQL语句为: WHERE id = ? AND ((desc LIKE ? AND name LIKE ?) OR (age = ? OR birthday = ?)) + * 因为默认是AND关系, @FilterGroup("") 等价于 @FilterGroup("[AND]") + * 所以示例二的@FilterGroup("[OR]g1.[AND]subg1") 可以简化为 @FilterGroup("[OR]g1") + */ +/** + * @author zhangjx + */ +@Inherited +@Documented +@Target({FIELD}) +@Retention(RUNTIME) +public @interface FilterGroup { + + String value() default "[AND]"; +} diff --git a/src/com/wentch/redkale/source/FilterGroups.java b/src/com/wentch/redkale/source/FilterGroups.java new file mode 100644 index 000000000..722710b75 --- /dev/null +++ b/src/com/wentch/redkale/source/FilterGroups.java @@ -0,0 +1,23 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.*; + +/** + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({FIELD}) +@Retention(RUNTIME) +public @interface FilterGroups { + + FilterGroup[] value(); +} diff --git a/src/com/wentch/redkale/source/FilterInfo.java b/src/com/wentch/redkale/source/FilterInfo.java new file mode 100644 index 000000000..8a454e46f --- /dev/null +++ b/src/com/wentch/redkale/source/FilterInfo.java @@ -0,0 +1,512 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import static com.wentch.redkale.source.FilterExpress.*; +import com.wentch.redkale.util.Attribute; +import com.wentch.redkale.util.Ignore; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.logging.Logger; + +/** + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +final class FilterInfo { + + private final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + private static final ConcurrentHashMap infos = new ConcurrentHashMap<>(); + + private final String joinsql; + + private final boolean validCacheJoin; + + private final Class type; + + private final FilterExpressNode rootNode; + + public static FilterInfo load(Class clazz, DataSource source) { + FilterInfo rs = infos.get(clazz); + if (rs != null) return rs; + synchronized (infos) { + rs = infos.get(clazz); + if (rs == null) { + rs = new FilterInfo(clazz, source); + infos.put(clazz, rs); + } + return rs; + } + } + + private FilterInfo(Class type, DataSource source) { + this.type = type; + Class cltmp = type; + Set fields = new HashSet<>(); + StringBuilder joinsb = new StringBuilder(); + final Map joinTables = new HashMap<>(); + final Map getters = new HashMap<>(); + final Map> nodes = new HashMap<>(); + boolean cachejoin = true; + int index = 0; + do { + for (Field field : cltmp.getDeclaredFields()) { + if (field.getAnnotation(Ignore.class) != null) continue; + if (Modifier.isStatic(field.getModifiers())) continue; + if (fields.contains(field.getName())) continue; + char[] chars = field.getName().toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + final Class t = field.getType(); + try { + type.getMethod(((t == boolean.class || t == Boolean.class) ? "is" : "get") + new String(chars)); + } catch (Exception ex) { + continue; + } + fields.add(field.getName()); + FilterItem item = new FilterItem(field, "a", null); + FilterJoinColumn joinCol = field.getAnnotation(FilterJoinColumn.class); + if (joinCol != null) { + if (!joinTables.containsKey(joinCol.table())) { + joinTables.put(joinCol.table(), String.valueOf((char) ('a' + (++index)))); + } + String alias = joinTables.get(joinCol.table()); + EntityInfo info = EntityInfo.load(joinCol.table(), source); + EntityCache cache = null; + if (info.getCache() != null && info.getCache().isFullLoaded()) { + cache = info.getCache(); + } else { + cachejoin = false; + } + item = new FilterItem(field, alias, cache); + EntityInfo secinfo = EntityInfo.load(joinCol.table(), null); + joinsb.append(" ").append(joinCol.type().name()).append(" JOIN ").append(secinfo.getTable()).append(" ").append(alias) + .append(" ON a.# = ").append(alias).append(".") + .append(joinCol.column().isEmpty() ? secinfo.getPrimary().field() : joinCol.column()); + } + getters.put(field.getName(), item); + FilterGroup[] refs = field.getAnnotationsByType(FilterGroup.class); + String[] groups = new String[refs.length]; + for (int i = 0; i < refs.length; i++) { + groups[i] = refs[i].value(); + } + if (groups.length == 0) groups = new String[]{"[AND]"}; + for (String key : groups) { + if (!key.startsWith("[AND]") && !key.startsWith("[OR]")) { + throw new RuntimeException(field + "'s FilterGroup.value(" + key + ") illegal, must be [AND] or [OR] startsWith"); + } + List nd = nodes.get(key); + if (nd == null) { + nd = new ArrayList(); + nodes.put(key, nd); + } + nd.add(item); + } + } + } while ((cltmp = cltmp.getSuperclass()) != Object.class); + //--------------------------------------------------------------- + List expnodes = new ArrayList<>(); + for (Map.Entry> en : nodes.entrySet()) { + if (en.getValue().size() == 1) { + expnodes.add(en.getValue().get(0)); + } else { + List sitems = en.getValue(); + expnodes.add(new FilterGroupNode(en.getKey(), sitems.toArray(new FilterExpressNode[sitems.size()]))); + } + } + rootNode = new FilterGroupNode("AND", expnodes.toArray(new FilterExpressNode[expnodes.size()])); + //--------------------------------------------------------------- + this.validCacheJoin = cachejoin; + if (!joinTables.isEmpty()) { + this.joinsql = joinsb.toString(); + } else { + this.joinsql = null; + } + } + + public FilterItem[] getFilters() { + return new FilterItem[0]; + } + + public Class getType() { + return type; + } + + public boolean isJoin() { + return joinsql != null; + } + + public boolean isValidCacheJoin() { + return validCacheJoin; + } + + public Predicate getFilterPredicate(EntityInfo info, T bean) { + return rootNode.getFilterPredicate(info, bean); + } + + public Comparator getSortComparator(EntityInfo info, Flipper flipper) { + if (flipper == null || flipper.getSort() == null || flipper.getSort().isEmpty()) return null; + Comparator comparator = null; + for (String item : flipper.getSort().split(",")) { + if (item.isEmpty()) continue; + String[] sub = item.split("\\s+"); + final Attribute attr = info.getAttribute(sub[0]); + Comparator c = (E o1, E o2) -> { + Comparable c1 = (Comparable) attr.get(o1); + Comparable c2 = (Comparable) attr.get(o2); + return c1 == null ? -1 : c1.compareTo(c2); + }; + if (sub.length > 1 && sub[1].equalsIgnoreCase("DESC")) { + c = c.reversed(); + } + if (comparator == null) { + comparator = c; + } else { + comparator = comparator.thenComparing(c); + } + } + return comparator; + } + + public StringBuilder createWhereSql(String primaryColumn, T obj) { + StringBuilder sb = rootNode.getFilterExpress(obj); + if (sb == null) return null; + final StringBuilder all = new StringBuilder(128); + if (this.isJoin()) { + all.append(this.joinsql.replace("#", primaryColumn)); + } + all.append(" WHERE ").append(sb); + return all; + } + + public static String formatToString(Object rs) { + if (rs == null) return null; + Class clazz = rs.getClass(); + if (CharSequence.class.isAssignableFrom(clazz)) { + return "'" + rs.toString().replace("'", "\\'") + "'"; + } else if (java.util.Date.class.isAssignableFrom(clazz)) { + return "'" + String.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS", (java.util.Date) rs) + "'"; + } + return String.valueOf(rs); + } + + static final class FilterGroupNode implements FilterExpressNode { + + private final String sign; + + private final boolean or; + + private final FilterExpressNode[] nodes; + + public FilterGroupNode(String sign, FilterExpressNode[] nodes) { + this.sign = sign.indexOf("[OR]") == 0 || sign.equalsIgnoreCase("OR") ? "OR" : "AND"; + this.or = "OR".equals(this.sign); + this.nodes = nodes; + } + + @Override + public StringBuilder getFilterExpress(T obj) { + StringBuilder sb = null; + int count = 0; + for (FilterExpressNode node : nodes) { + StringBuilder sub = node.getFilterExpress(obj); + if (sub == null) continue; + if (sb == null) { + sb = new StringBuilder(); + sb.append(sub); + count++; + } else { + sb.append(' ').append(sign).append(' ').append(sub); + count++; + } + } + if (sb == null) return null; + if (count < 2) return sb; + return new StringBuilder(sb.length() + 2).append('(').append(sb).append(')'); + } + + @Override + public Predicate getFilterPredicate(EntityInfo info, T bean) { + Predicate predicate = null; + for (FilterExpressNode node : nodes) { + Predicate p = node.getFilterPredicate(info, bean); + if (p == null) continue; + if (predicate == null) { + predicate = p; + } else { + predicate = or ? predicate.or(p) : predicate.and(p); + } + } + return predicate; + } + } + + static final class FilterItem implements FilterExpressNode { + + public final Attribute attribute; + + public final String aliasfield; + + private final String field; + + public final FilterExpress express; + + public final boolean string; + + public final boolean number; + + public final boolean likefit; + + public final boolean ignoreCase; + + public final long least; + + public final Class type; + + public final EntityCache joinCache; //待实现 + + public FilterItem(Field field, String alias, EntityCache joinCache) { + this.joinCache = joinCache; + FilterColumn column = field.getAnnotation(FilterColumn.class); + String sqlfield = (column != null && !column.name().isEmpty() ? column.name() : field.getName()); + this.field = sqlfield; + sqlfield = alias + "." + sqlfield; + this.attribute = Attribute.create(sqlfield, field); + this.aliasfield = this.attribute.field(); + this.type = (Class) field.getType(); + FilterExpress exp = column == null ? FilterExpress.EQUAL : column.express(); + if (type.isArray() || Collection.class.isAssignableFrom(type)) { + if (Range.class.isAssignableFrom(type.getComponentType())) { + if (AND != exp) exp = FilterExpress.OR; + } else { + if (NOTIN != exp) exp = FilterExpress.IN; + } + } else if (Range.class.isAssignableFrom(type)) { + if (NOTBETWEEN != exp) exp = FilterExpress.BETWEEN; + } + this.express = exp; + this.least = column == null ? 1L : column.least(); + this.likefit = column == null ? true : column.likefit(); + this.ignoreCase = column == null ? true : column.ignoreCase(); + this.number = type.isPrimitive() || Number.class.isAssignableFrom(type); + this.string = CharSequence.class.isAssignableFrom(type); + } + + /** + * 返回null表示无需过滤该字段 + *

+ * @param bean + * @return + */ + @Override + public StringBuilder getFilterExpress(final T bean) { + final F rs = attribute.get(bean); + if (rs == null) return null; + if (string && ((CharSequence) rs).length() == 0) return null; + if (number && ((Number) rs).longValue() < this.least) return null; + if (Range.class.isAssignableFrom(type)) return getRangeExpress((Range) rs); + if (type.isArray() || Collection.class.isAssignableFrom(type)) { + Object[] os; + if (Collection.class.isAssignableFrom(type)) { + os = ((Collection) rs).toArray(); + } else { + final int len = Array.getLength(rs); + if (len < 1) return null; + if (type.getComponentType().isPrimitive()) { + os = new Object[len]; + for (int i = 0; i < len; i++) { + os[i] = Array.get(rs, i); + } + } else { + os = (Object[]) rs; + } + } + if (Range.class.isAssignableFrom(os[0].getClass())) { + StringBuilder sb = new StringBuilder(); + sb.append('('); + boolean flag = false; + for (Object o : os) { + if (flag) sb.append(' ').append(express.value()).append(' '); + sb.append(getRangeExpress((Range) o)); + flag = true; + } + return sb.append(')'); + } else { + StringBuilder sb = new StringBuilder(); + sb.append(aliasfield).append(' ').append(express.value()).append(" ("); + boolean flag = false; + for (Object o : os) { + if (flag) sb.append(','); + sb.append(formatValue(o)); + flag = true; + } + return sb.append(')'); + } + } else if (express == OPAND || express == OPOR) { + StringBuilder sb = new StringBuilder(); + sb.append('(').append(aliasfield).append(' ').append(express.value()).append(' ').append(formatValue(rs)).append(" > 0)"); + return sb; + } else if (express == OPANDNO) { + StringBuilder sb = new StringBuilder(); + sb.append('(').append(aliasfield).append(' ').append(express.value()).append(' ').append(formatValue(rs)).append(" = 0)"); + return sb; + } else { + StringBuilder sb = new StringBuilder(); + sb.append(aliasfield).append(' ').append(express.value()).append(' ').append(formatValue(rs)); + return sb; + } + } + + private String formatValue(Object rs) { + if ((LIKE == express || NOTLIKE == express) && this.likefit) return formatToString("%" + rs + "%"); + return formatToString(rs); + } + + private StringBuilder getRangeExpress(Range range) { + StringBuilder sb = new StringBuilder(); + return sb.append("(").append(aliasfield).append((NOTBETWEEN == express ? " NOT BETWEEN " : " BETWEEN ")) + .append(formatToString(range.getMin())).append(" AND ").append(formatToString(range.getMax())).append(")"); + } + + private Predicate getRangePredicate(final Attribute attr, Range range) { + final Comparable min = range.getMin(); + final Comparable max = range.getMax(); + Predicate p = (E t) -> { + Comparable rs = (Comparable) attr.get(t); + if (rs == null) return false; + if (min != null && min.compareTo(rs) >= 0) return false; + return !(max != null && max.compareTo(rs) <= 0); + }; + return (express == NOTBETWEEN) ? p.negate() : p; + } + + private Predicate getArrayPredicate(final Attribute attr, Object beanValue) { + if (beanValue == null) return null; + Predicate p; + if (type.isArray()) { + if (Array.getLength(beanValue) == 0) return null; + final Class comp = type.getComponentType(); + if (comp == int.class) { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((int[]) beanValue, (int) rs) >= 0; + }; + } else if (comp == long.class) { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((long[]) beanValue, (long) rs) >= 0; + }; + } else if (comp == short.class) { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((short[]) beanValue, (short) rs) >= 0; + }; + } else if (comp == float.class) { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((float[]) beanValue, (float) rs) >= 0; + }; + } else if (comp == double.class) { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((double[]) beanValue, (double) rs) >= 0; + }; + } else if (comp == byte.class) { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((byte[]) beanValue, (byte) rs) >= 0; + }; + } else if (comp == char.class) { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((char[]) beanValue, (char) rs) >= 0; + }; + } else { + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return Arrays.binarySearch((Object[]) beanValue, rs) >= 0; + }; + } + } else { // Collection + Collection collection = (Collection) beanValue; + if (collection.isEmpty()) return null; + p = (E t) -> { + Object rs = attr.get(t); + if (rs == null) return false; + return collection.contains(rs); + }; + } + return p == null ? null : (express == NOTIN) ? p.negate() : p; + } + + @Override + public Predicate getFilterPredicate(EntityInfo info, T bean) { + return getFilterPredicate(info.getAttribute(field), bean); + } + + private Predicate getFilterPredicate(final Attribute attr, T bean) { + final F beanValue = attribute.get(bean); + if (beanValue == null) return null; + if (string && ((CharSequence) beanValue).length() == 0) return null; + if (number && ((Number) beanValue).longValue() < this.least) return null; + if (Range.class.isAssignableFrom(type)) return getRangePredicate(attr, (Range) beanValue); + if (type.isArray() || Collection.class.isAssignableFrom(type)) return getArrayPredicate(attr, (Range) beanValue); + final long beanLongValue = number ? ((Number) beanValue).longValue() : 0L; + switch (express) { + case EQUAL: return (E t) -> beanValue.equals(attr.get(t)); + case NOTEQUAL: return (E t) -> !beanValue.equals(attr.get(t)); + case GREATERTHAN: return (E t) -> ((Number) attr.get(t)).longValue() > beanLongValue; + case LESSTHAN: return (E t) -> ((Number) attr.get(t)).longValue() < beanLongValue; + case GREATERTHANOREQUALTO: return (E t) -> ((Number) attr.get(t)).longValue() >= beanLongValue; + case LESSTHANOREQUALTO: return (E t) -> ((Number) attr.get(t)).longValue() <= beanLongValue; + case LIKE: if (!ignoreCase) return (E t) -> { + Object rs = attr.get(t); + return rs != null && rs.toString().contains(beanValue.toString()); + }; + String v1 = beanValue.toString().toLowerCase(); + return (E t) -> { + Object rs = attr.get(t); + return rs != null && rs.toString().toLowerCase().contains(v1); + }; + case NOTLIKE: if (!ignoreCase) return (E t) -> { + Object rs = attr.get(t); + return rs == null || !rs.toString().contains(beanValue.toString()); + }; + String v2 = beanValue.toString().toLowerCase(); + return (E t) -> { + Object rs = attr.get(t); + return rs == null || !rs.toString().toLowerCase().contains(v2); + }; + case ISNULL: return (E t) -> attr.get(t) == null; + case ISNOTNULL: return (E t) -> attr.get(t) != null; + case OPAND: return (E t) -> (((Number) attr.get(t)).longValue() & beanLongValue) > 0; + case OPOR: return (E t) -> (((Number) attr.get(t)).longValue() | beanLongValue) > 0; + case OPANDNO: return (E t) -> (((Number) attr.get(t)).longValue() & beanLongValue) == 0; + } + return null; + } + + } + + static interface FilterExpressNode { + + public StringBuilder getFilterExpress(final T bean); + + public Predicate getFilterPredicate(final EntityInfo info, final T bean); + } +} diff --git a/src/com/wentch/redkale/source/FilterJoinColumn.java b/src/com/wentch/redkale/source/FilterJoinColumn.java new file mode 100644 index 000000000..c8c083393 --- /dev/null +++ b/src/com/wentch/redkale/source/FilterJoinColumn.java @@ -0,0 +1,42 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({FIELD}) +@Retention(RUNTIME) +public @interface FilterJoinColumn { + + public enum JoinType { //不能支持RIGHT, 因为right获取的主对象都是null + + LEFT, INNER; + } + + /** + * 关联表 + * + * @return + */ + Class table(); + + /** + * 默认使用主键 + *

+ * @return + */ + String column() default ""; + + JoinType type() default JoinType.INNER; +} diff --git a/src/com/wentch/redkale/source/Flipper.java b/src/com/wentch/redkale/source/Flipper.java new file mode 100644 index 000000000..cb193cf95 --- /dev/null +++ b/src/com/wentch/redkale/source/Flipper.java @@ -0,0 +1,106 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import java.io.Serializable; + +/** + * + * @author zhangjx + */ +public final class Flipper implements Serializable { + + public static int DEFAULT_PAGESIZE = 20; + + private int size = DEFAULT_PAGESIZE; + + private int page = 1; + + private String sort = ""; + + public Flipper() { + } + + public Flipper(int pageSize) { + this.size = pageSize; + } + + public Flipper(int pageSize, int pageNo) { + this.size = pageSize; + this.page = pageNo; + } + + public Flipper(int pageSize, int pageNo, String sortColumn) { + this.size = pageSize; + this.page = pageNo; + this.sort = sortColumn; + } + + public void copyTo(Flipper copy) { + if (copy == null) return; + copy.page = this.page; + copy.size = this.size; + copy.sort = this.sort; + } + + public void copyFrom(Flipper copy) { + if (copy == null) return; + this.page = copy.page; + this.size = copy.size; + this.sort = copy.sort; + } + + public Flipper next() { + this.page++; + return this; + } + + public int index() { + return (getPage() - 1) * getSize(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{page:" + this.page + ", size=" + this.size + ", sort=" + this.sort + "}"; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + if (size > 0) { + this.size = size; + } + } + + public int getPage() { + return page; + } + + public void setPage(int page) { + if (page >= 0) { + this.page = page; + } + } + + public String getSort() { + return sort; + } + + public void putSortIfEmpty(String sort) { + if (this.sort == null || this.sort.isEmpty()) { + this.sort = sort; + } + } + + public void setSort(String sort) { + if (sort != null) { + this.sort = sort.trim(); + } + } + +} diff --git a/src/com/wentch/redkale/source/Range.java b/src/com/wentch/redkale/source/Range.java new file mode 100644 index 000000000..8d3c6a92d --- /dev/null +++ b/src/com/wentch/redkale/source/Range.java @@ -0,0 +1,284 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +/** + * + * @author zhangjx + * @param + */ +public interface Range extends java.io.Serializable { + + public E getMin(); + + public E getMax(); + + public static final class ByteRange implements Range { + + private Byte min = Byte.MIN_VALUE; + + private Byte max = Byte.MAX_VALUE; + + public ByteRange() { + } + + public ByteRange(Byte min, Byte max) { + if (min != null) this.min = min; + if (max != null) this.max = max; + } + + @Override + public Byte getMin() { + return min; + } + + @Override + public Byte getMax() { + return max; + } + + public void setMin(Byte min) { + if (min != null) this.min = min; + } + + public void setMax(Byte max) { + if (max != null) this.max = max; + } + + @Override + public String toString() { + return "{min:" + min + ", max:" + max + "}"; + } + } + + public static final class ShortRange implements Range { + + private Short min = Short.MIN_VALUE; + + private Short max = Short.MAX_VALUE; + + public ShortRange() { + } + + public ShortRange(Short min, Short max) { + if (min != null) this.min = min; + if (max != null) this.max = max; + } + + @Override + public Short getMin() { + return min; + } + + @Override + public Short getMax() { + return max; + } + + public void setMin(Short min) { + if (min != null) this.min = min; + } + + public void setMax(Short max) { + if (max != null) this.max = max; + } + + @Override + public String toString() { + return "{min:" + min + ", max:" + max + "}"; + } + } + + public static final class IntegerRange implements Range { + + private Integer min = Integer.MIN_VALUE; + + private Integer max = Integer.MAX_VALUE; + + public IntegerRange() { + } + + public IntegerRange(Integer min, Integer max) { + if (min != null) this.min = min; + if (max != null) this.max = max; + } + + @Override + public Integer getMin() { + return min; + } + + @Override + public Integer getMax() { + return max; + } + + public void setMin(Integer min) { + if (min != null) this.min = min; + } + + public void setMax(Integer max) { + if (max != null) this.max = max; + } + + @Override + public String toString() { + return "{min:" + min + ", max:" + max + "}"; + } + } + + public static final class LongRange implements Range { + + private Long min = Long.MIN_VALUE; + + private Long max = Long.MAX_VALUE; + + public LongRange() { + } + + public LongRange(Long min, Long max) { + if (min != null) this.min = min; + if (max != null) this.max = max; + } + + @Override + public Long getMin() { + return min; + } + + @Override + public Long getMax() { + return max; + } + + public void setMin(Long min) { + if (min != null) this.min = min; + } + + public void setMax(Long max) { + if (max != null) this.max = max; + } + + @Override + public String toString() { + return "{min:" + min + ", max:" + max + "}"; + } + } + + public static final class FloatRange implements Range { + + private Float min = Float.MIN_VALUE; + + private Float max = Float.MAX_VALUE; + + public FloatRange() { + } + + public FloatRange(Float min, Float max) { + if (min != null) this.min = min; + if (max != null) this.max = max; + } + + @Override + public Float getMin() { + return min; + } + + @Override + public Float getMax() { + return max; + } + + public void setMin(Float min) { + if (min != null) this.min = min; + } + + public void setMax(Float max) { + if (max != null) this.max = max; + } + + @Override + public String toString() { + return "{min:" + min + ", max:" + max + "}"; + } + } + + public static final class DoubleRange implements Range { + + private Double min = Double.MIN_VALUE; + + private Double max = Double.MAX_VALUE; + + public DoubleRange() { + } + + public DoubleRange(Double min, Double max) { + if (min != null) this.min = min; + if (max != null) this.max = max; + } + + @Override + public Double getMin() { + return min; + } + + @Override + public Double getMax() { + return max; + } + + public void setMin(Double min) { + if (min != null) this.min = min; + } + + public void setMax(Double max) { + if (max != null) this.max = max; + } + + @Override + public String toString() { + return "{min:" + min + ", max:" + max + "}"; + } + } + + public static final class StringRange implements Range { + + private String min = ""; + + private String max = ""; + + public StringRange() { + } + + public StringRange(String min, String max) { + this.min = min; + this.max = max; + } + + @Override + public String getMin() { + return min; + } + + @Override + public String getMax() { + return max; + } + + public void setMin(String min) { + this.min = min; + } + + public void setMax(String max) { + this.max = max; + } + + @Override + public String toString() { + return "{min:'" + min + "', max:'" + max + "'}"; + } + } +} diff --git a/src/com/wentch/redkale/source/SelectColumn.java b/src/com/wentch/redkale/source/SelectColumn.java new file mode 100644 index 000000000..7e28a584e --- /dev/null +++ b/src/com/wentch/redkale/source/SelectColumn.java @@ -0,0 +1,80 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.source; + +import java.util.Arrays; + +/** + * + * @author zhangjx + */ +public final class SelectColumn { + + private String[] columns; + + private boolean excludable; //是否排除 + + public SelectColumn() { + } + + private SelectColumn(String[] columns, boolean excludable) { + this.columns = columns; + this.excludable = excludable; + } + + /** + * class中的字段名 + * + * @param columns + * @return + */ + public static SelectColumn createIncludes(String... columns) { + return new SelectColumn(columns, false); + } + + /** + * class中的字段名 + * + * @param columns + * @return + */ + public static SelectColumn createExcludes(String... columns) { + return new SelectColumn(columns, true); + } + + public boolean validate(String column) { + for (String col : this.columns) { + if (col.equalsIgnoreCase(column)) return !excludable; + } + return excludable; + } + + public boolean isEmpty() { + return this.columns == null || this.columns.length == 0; + } + + public String[] getColumns() { + return columns; + } + + public void setColumns(String[] columns) { + this.columns = columns; + } + + public boolean isExcludable() { + return excludable; + } + + public void setExcludable(boolean excludable) { + this.excludable = excludable; + } + + @Override + public String toString() { + return "SelectColumn{" + "columns=" + Arrays.toString(columns) + ", excludable=" + excludable + '}'; + } + +} diff --git a/src/com/wentch/redkale/util/AnyValue.java b/src/com/wentch/redkale/util/AnyValue.java new file mode 100644 index 000000000..f225c1049 --- /dev/null +++ b/src/com/wentch/redkale/util/AnyValue.java @@ -0,0 +1,347 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.function.BiPredicate; + +/** + * 该类主要用于读取xml配置文件 + * + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public interface AnyValue { + + /** + * 可读写的AnyValue默认实现类 + * + * @author zhangjx + */ + @SuppressWarnings("unchecked") + public static final class DefaultAnyValue implements AnyValue { + + public static final BiPredicate EQUALS = (String name1, String name2) -> name1.equals(name2); + + public static final BiPredicate EQUALSIGNORE = (String name1, String name2) -> name1.equalsIgnoreCase(name2); + + private final BiPredicate predicate; + + private Entry[] stringValues = new Entry[0]; + + private Entry[] entityValues = new Entry[0]; + + public DefaultAnyValue() { + this(false); + } + + public DefaultAnyValue(boolean ignoreCase) { + this.predicate = ignoreCase ? EQUALSIGNORE : EQUALS; + } + + public DefaultAnyValue(BiPredicate predicate) { + this.predicate = predicate; + } + + public DefaultAnyValue duplicate() { + DefaultAnyValue rs = new DefaultAnyValue(this.predicate); + rs.stringValues = this.stringValues; + rs.entityValues = this.entityValues; + return rs; + } + + @Override + public Entry[] getStringEntrys() { + return stringValues; + } + + @Override + public Entry[] getAnyEntrys() { + return entityValues; + } + + @Override + public String[] getNames() { + Set set = new LinkedHashSet<>(); + for (Entry en : this.stringValues) { + set.add(en.name); + } + for (Entry en : this.entityValues) { + set.add(en.name); + } + return set.toArray(new String[set.size()]); + } + + @Override + public String[] getValues(String... names) { + return Entry.getValues(this.predicate, String.class, this.stringValues, names); + } + + @Override + public AnyValue[] getAnyValues(String... names) { + return Entry.getValues(this.predicate, AnyValue.class, this.entityValues, names); + } + + @Override + public String toString() { + return toString(0); + } + + public void clear() { + this.stringValues = new Entry[0]; + this.entityValues = new Entry[0]; + } + + public void setValue(String name, String value) { + if (name == null) return; + if (getValue(name) == null) { + this.addValue(name, value); + } else { + for (Entry en : this.stringValues) { + if (predicate.test(en.name, name)) { + en.value = value; + return; + } + } + } + } + + public void setValue(String name, AnyValue value) { + if (name == null) return; + if (getValue(name) == null) { + this.addValue(name, value); + } else { + for (Entry en : this.entityValues) { + if (predicate.test(en.name, name)) { + en.value = value; + return; + } + } + } + } + + public void addValue(String name, String value) { + if (name == null) return; + int len = this.stringValues.length; + Entry[] news = new Entry[len + 1]; + System.arraycopy(this.stringValues, 0, news, 0, len); + news[len] = new Entry(name, value); + this.stringValues = news; + } + + public void addValue(String name, AnyValue value) { + if (name == null || value == null) return; + int len = this.entityValues.length; + Entry[] news = new Entry[len + 1]; + System.arraycopy(this.entityValues, 0, news, 0, len); + news[len] = new Entry(name, value); + this.entityValues = news; + } + + @Override + public AnyValue getAnyValue(String name) { + for (Entry en : this.entityValues) { + if (predicate.test(en.name, name)) { + return en.value; + } + } + return null; + } + + @Override + public String getValue(String name) { + for (Entry en : this.stringValues) { + if (predicate.test(en.name, name)) { + return en.value; + } + } + return null; + } + + @Override + public String[] getValues(String name) { + return Entry.getValues(this.predicate, String.class, this.stringValues, name); + } + + @Override + public AnyValue[] getAnyValues(String name) { + return Entry.getValues(this.predicate, AnyValue.class, this.entityValues, name); + } + + } + + public final class Entry { + + public final String name; + + T value; + + public Entry(String name0, T value0) { + this.name = name0; + this.value = value0; + } + + public T getValue() { + return value; + } + + static T[] getValues(BiPredicate comparison, Class clazz, Entry[] entitys, String name) { + int len = 0; + for (Entry en : entitys) { + if (comparison.test(en.name, name)) { + ++len; + } + } + if (len == 0) return (T[]) Array.newInstance(clazz, len); + T[] rs = (T[]) Array.newInstance(clazz, len); + int i = 0; + for (Entry en : entitys) { + if (comparison.test(en.name, name)) { + rs[i++] = en.value; + } + } + return rs; + } + + static T[] getValues(BiPredicate comparison, Class clazz, Entry[] entitys, String... names) { + int len = 0; + for (Entry en : entitys) { + for (String name : names) { + if (comparison.test(en.name, name)) { + ++len; + break; + } + } + } + if (len == 0) return (T[]) Array.newInstance(clazz, len); + T[] rs = (T[]) Array.newInstance(clazz, len); + int i = 0; + for (Entry en : entitys) { + for (String name : names) { + if (comparison.test(en.name, name)) { + rs[i++] = en.value; + break; + } + } + } + return rs; + } + } + + default String toString(int len) { + if (len < 0) len = 0; + char[] chars = new char[len]; + Arrays.fill(chars, ' '); + final String space = new String(chars); + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + for (Entry en : getStringEntrys()) { + sb.append(space).append(" '").append(en.name).append("': '").append(en.value).append("',\n"); + } + for (Entry en : getAnyEntrys()) { + sb.append(space).append(" '").append(en.name).append("': '").append(en.value.toString(len + 4)).append("',\n"); + } + sb.append(space).append('}'); + return sb.toString(); + } + + public Entry[] getStringEntrys(); + + public Entry[] getAnyEntrys(); + + public String[] getNames(); + + public String[] getValues(String name); + + public String[] getValues(String... names); + + public AnyValue[] getAnyValues(String name); + + public AnyValue[] getAnyValues(String... names); + + public AnyValue getAnyValue(String name); + + public String getValue(String name); + + default boolean getBoolValue(String name) { + return Boolean.parseBoolean(getValue(name)); + } + + default boolean getBoolValue(String name, boolean defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : Boolean.parseBoolean(value); + } + + default byte getByteValue(String name) { + return Byte.parseByte(getValue(name)); + } + + default byte getByteValue(String name, byte defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : Byte.decode(value); + } + + default char getCharValue(String name) { + return getValue(name).charAt(0); + } + + default char getCharValue(String name, char defaultValue) { + String value = getValue(name); + return value == null || value.length() == 0 ? defaultValue : value.charAt(0); + } + + default short getShortValue(String name) { + return Short.decode(getValue(name)); + } + + default short getShortValue(String name, short defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : Short.decode(value); + } + + default int getIntValue(String name) { + return Integer.decode(getValue(name)); + } + + default int getIntValue(String name, int defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : Integer.decode(value); + } + + default long getLongValue(String name) { + return Long.decode(getValue(name)); + } + + default long getLongValue(String name, long defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : Long.decode(value); + } + + default float getFloatValue(String name) { + return Float.parseFloat(getValue(name)); + } + + default float getFloatValue(String name, float defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : Float.parseFloat(value); + } + + default double getDoubleValue(String name) { + return Double.parseDouble(getValue(name)); + } + + default double getDoubleValue(String name, double defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : Double.parseDouble(value); + } + + default String getValue(String name, String defaultValue) { + String value = getValue(name); + return value == null ? defaultValue : value; + } + +} diff --git a/src/com/wentch/redkale/util/Attribute.java b/src/com/wentch/redkale/util/Attribute.java new file mode 100644 index 000000000..c8d2898cd --- /dev/null +++ b/src/com/wentch/redkale/util/Attribute.java @@ -0,0 +1,259 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.lang.reflect.*; +import static jdk.internal.org.objectweb.asm.Opcodes.*; +import jdk.internal.org.objectweb.asm.Type; + +/** + * 该类功能是动态映射一个Data类中成员对应的getter、setter方法; 代替低效的反射实现方式。 + * 映射Field时,field要么是public非final,要么存在对应的getter、setter方法。 + * + * @author zhangjx + * @param + * @param + */ +public interface Attribute { + + public String field(); + + public F get(T obj); + + public void set(T obj, F value); + + public static Attribute create(final Field field) { + return create((Class) field.getDeclaringClass(), field.getName(), field, null, null); + } + + public static Attribute create(String fieldname, final Field field) { + return create((Class) field.getDeclaringClass(), fieldname, field, null, null); + } + + public static Attribute create(Class clazz, final String fieldname) { + try { + return create(clazz, fieldname, clazz.getDeclaredField(fieldname), null, null); + } catch (NoSuchFieldException | SecurityException ex) { + throw new RuntimeException(ex); + } + } + + public static Attribute create(Class clazz, final java.lang.reflect.Field field) { + return create(clazz, field.getName(), field); + } + + public static Attribute create(Class clazz, final String fieldname, final java.lang.reflect.Field field) { + return create(clazz, fieldname, field, null, null); + } + + public static Attribute create(final Method getter, final Method setter) { + return create((Class) (getter == null ? setter.getDeclaringClass() : getter.getDeclaringClass()), null, null, getter, setter); + } + + public static Attribute create(Class clazz, final Method getter, final Method setter) { + return create(clazz, null, null, getter, setter); + } + + public static Attribute create(Class clazz, final String fieldalias, final Method getter, final Method setter) { + return create(clazz, fieldalias, null, getter, setter); + } + + @SuppressWarnings("unchecked") + public static Attribute create(Class clazz, String fieldalias0, final Field field0, Method getter0, Method setter0) { + if (fieldalias0.isEmpty()) fieldalias0 = null; + int mod = field0 == null ? Modifier.STATIC : field0.getModifiers(); + if (field0 != null && !Modifier.isStatic(mod) && !Modifier.isPublic(mod)) { + Class t = field0.getType(); + char[] fs = field0.getName().toCharArray(); + fs[0] = Character.toUpperCase(fs[0]); + String mn = new String(fs); + if (getter0 == null) { + String prefix = t == boolean.class || t == Boolean.class ? "is" : "get"; + try { + getter0 = clazz.getMethod(prefix + mn); + } catch (Exception ex) { + } + } + if (setter0 == null) { + try { + setter0 = clazz.getMethod("set" + mn, field0.getType()); + } catch (Exception ex) { + } + } + } + final Field field = field0 == null ? null : (!Modifier.isPublic(mod) || Modifier.isStatic(mod) ? null : field0); + final java.lang.reflect.Method getter = getter0; + final java.lang.reflect.Method setter = setter0; + if (fieldalias0 == null) { + if (field0 != null) { + fieldalias0 = field0.getName(); + } else { + String s; + if (getter0 != null) { + s = getter0.getName().substring(getter0.getName().startsWith("is") ? 2 : 3); + } else { + s = setter0.getName().substring(3); + } + char[] d = s.toCharArray(); + if (d.length < 2 || Character.isLowerCase(d[1])) { + d[0] = Character.toLowerCase(d[0]); + } + fieldalias0 = new String(d); + } + } + if (getter == null && setter == null && field == null) { + throw new RuntimeException("[" + clazz + "]have no public field or setter or getter"); + } + final String fieldalias = fieldalias0; + Class column; + if (field != null) { // public field + column = field.getType(); + } else if (getter != null) { + column = getter.getReturnType(); + } else { // setter != null + column = setter.getParameterTypes()[0]; + } + final Class pcolumn = column; + if (column.isPrimitive()) column = Array.get(Array.newInstance(column, 1), 0).getClass(); + final String supDynName = Attribute.class.getName().replace('.', '/'); + final String interName = clazz.getName().replace('.', '/'); + final String columnName = column.getName().replace('.', '/'); + final String interDesc = Type.getDescriptor(clazz); + final String columnDesc = Type.getDescriptor(column); + + ClassLoader loader = Attribute.class.getClassLoader(); + String newDynName = supDynName + "_Dyn_" + clazz.getSimpleName() + "_" + + fieldalias.substring(fieldalias.indexOf('.') + 1) + "_" + pcolumn.getSimpleName().replace("[]", "Array"); + if (String.class.getClassLoader() != clazz.getClassLoader()) { + loader = clazz.getClassLoader(); + newDynName = interName + "_Dyn" + Attribute.class.getSimpleName() + "_" + + fieldalias.substring(fieldalias.indexOf('.') + 1) + "_" + pcolumn.getSimpleName().replace("[]", "Array"); + } + try { + return (Attribute) Class.forName(newDynName.replace('/', '.')).newInstance(); + } catch (Exception ex) { + } + //--------------------------------------------------- + final jdk.internal.org.objectweb.asm.ClassWriter cw = new jdk.internal.org.objectweb.asm.ClassWriter(0); + jdk.internal.org.objectweb.asm.MethodVisitor mv; + + cw.visit(V1_8, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, newDynName, "Ljava/lang/Object;L" + supDynName + "<" + interDesc + columnDesc + ">;", "java/lang/Object", new String[]{supDynName}); + + { //构造方法 + mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + + { //field 方法 + mv = cw.visitMethod(ACC_PUBLIC, "field", "()Ljava/lang/String;", null, null); + mv.visitLdcInsn(fieldalias); + mv.visitInsn(ARETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + { //get 方法 + mv = cw.visitMethod(ACC_PUBLIC, "get", "(" + interDesc + ")" + columnDesc, null, null); + int m = 1; + if (getter == null) { + if (field == null) { + mv.visitInsn(ACONST_NULL); + } else { //public field + mv.visitVarInsn(ALOAD, 1); + mv.visitFieldInsn(GETFIELD, interName, field.getName(), Type.getDescriptor(pcolumn)); + if (pcolumn != column) { + mv.visitMethodInsn(INVOKESTATIC, columnName, "valueOf", "(" + Type.getDescriptor(pcolumn) + ")" + columnDesc, false); + } + } + } else { + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEVIRTUAL, interName, getter.getName(), Type.getMethodDescriptor(getter), false); + if (pcolumn != column) { + mv.visitMethodInsn(INVOKESTATIC, columnName, "valueOf", "(" + Type.getDescriptor(pcolumn) + ")" + columnDesc, false); + m = 2; + } + } + mv.visitInsn(ARETURN); + mv.visitMaxs(m, 2); + mv.visitEnd(); + } + { //set 方法 + mv = cw.visitMethod(ACC_PUBLIC, "set", "(" + interDesc + columnDesc + ")V", null, null); + int m = 2; + if (setter == null) { + if (field == null || Modifier.isFinal(field.getModifiers())) { + m = 0; + } else { //public field + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + if (pcolumn != column) { + try { + java.lang.reflect.Method pm = column.getMethod(pcolumn.getSimpleName() + "Value"); + mv.visitMethodInsn(INVOKEVIRTUAL, columnName, pm.getName(), Type.getMethodDescriptor(pm), false); + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + } + mv.visitFieldInsn(PUTFIELD, interName, field.getName(), Type.getDescriptor(pcolumn)); + } + } else { + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + if (pcolumn != column) { + try { + java.lang.reflect.Method pm = column.getMethod(pcolumn.getSimpleName() + "Value"); + mv.visitMethodInsn(INVOKEVIRTUAL, columnName, pm.getName(), Type.getMethodDescriptor(pm), false); + m = 3; + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + } + mv.visitMethodInsn(INVOKEVIRTUAL, interName, setter.getName(), Type.getMethodDescriptor(setter), false); + } + mv.visitInsn(RETURN); + mv.visitMaxs(m, 3); + mv.visitEnd(); + } + { //虚拟get + mv = cw.visitMethod(ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", null, null); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitTypeInsn(CHECKCAST, interName); + mv.visitMethodInsn(INVOKEVIRTUAL, newDynName, "get", "(" + interDesc + ")" + columnDesc, false); + mv.visitInsn(ARETURN); + mv.visitMaxs(2, 2); + mv.visitEnd(); + } + {//虚拟set + mv = cw.visitMethod(ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC, "set", "(Ljava/lang/Object;Ljava/lang/Object;)V", null, null); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitTypeInsn(CHECKCAST, interName); + mv.visitVarInsn(ALOAD, 2); + mv.visitTypeInsn(CHECKCAST, columnName); + mv.visitMethodInsn(INVOKEVIRTUAL, newDynName, "set", "(" + interDesc + columnDesc + ")V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(3, 3); + mv.visitEnd(); + } + cw.visitEnd(); + + byte[] bytes = cw.toByteArray(); + Class creatorClazz = (Class) new ClassLoader(loader) { + public final Class loadClass(String name, byte[] b) { + return defineClass(name, b, 0, b.length); + } + }.loadClass(newDynName.replace('/', '.'), bytes); + try { + return creatorClazz.newInstance(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/com/wentch/redkale/util/AutoLoad.java b/src/com/wentch/redkale/util/AutoLoad.java new file mode 100644 index 000000000..68686adbe --- /dev/null +++ b/src/com/wentch/redkale/util/AutoLoad.java @@ -0,0 +1,24 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 自动全量缓存 + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({TYPE}) +@Retention(RUNTIME) +public @interface AutoLoad { + + boolean value() default true; +} diff --git a/src/com/wentch/redkale/util/Creator.java b/src/com/wentch/redkale/util/Creator.java new file mode 100644 index 000000000..c66ba2c97 --- /dev/null +++ b/src/com/wentch/redkale/util/Creator.java @@ -0,0 +1,207 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.beans.ConstructorProperties; +import java.lang.reflect.*; +import java.util.*; +import jdk.internal.org.objectweb.asm.*; +import static jdk.internal.org.objectweb.asm.Opcodes.*; +import jdk.internal.org.objectweb.asm.Type; + +/** + * 实现一个类的构造方法。 代替低效的反射实现方式。 + * + * @author zhangjx + * @param + */ +public interface Creator { +// +// static class PooledCreator implements Creator { +// +// private final T defValue; +// +// private final Reproduce reproduce; +// +// private final ReferenceQueue refQueue = new ReferenceQueue(); +// +// private final Queue queue; +// +// private final Creator creator; +// +// public PooledCreator(int max, Class clazz, Creator creator) { +// this.creator = creator; +// this.defValue = creator.create(); +// this.reproduce = Reproduce.create(clazz, clazz); +// this.queue = new ArrayBlockingQueue<>(Math.max(Runtime.getRuntime().availableProcessors() * 2, max)); +// new Thread() { +// { +// setDaemon(true); +// setName(PooledCreator.class.getSimpleName() + " " + clazz.getSimpleName() + " Reference Handler"); +// } +// +// @Override +// public void run() { +// try { +// for (;;) { +// T r = refQueue.remove().get(); +// if (r == null) continue; +// reproduce.copy(r, defValue); +// queue.offer(r); +// } +// } catch (Exception e) { +// //do nothind +// } +// } +// }.start(); +// } +// +// @Override +// public T create(Object... params) { +// T rs = queue.poll(); +// if (rs == null) { +// rs = creator.create(params); +// } +// return new WeakReference<>(rs, refQueue).get(); +// } +// +// } +// +// @SuppressWarnings("unchecked") +// public static Creator create(int max, Class clazz) { +// return new PooledCreator<>(max, clazz, create(clazz)); +// } +// +// @SuppressWarnings("unchecked") +// public static Creator create(int max, Class clazz, Creator creator) { +// return new PooledCreator<>(max, clazz, creator); +// } + + public T create(Object... params); + + @SuppressWarnings("unchecked") + public static Creator create(Class clazz) { + if (clazz.isAssignableFrom(ArrayList.class)) { + clazz = (Class) ArrayList.class; + } else if (clazz.isAssignableFrom(HashMap.class)) { + clazz = (Class) HashMap.class; + } else if (clazz.isAssignableFrom(HashSet.class)) { + clazz = (Class) HashSet.class; + } + if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { + throw new RuntimeException("[" + clazz + "] is a interface or abstract class, cannot create it's Creator."); + } + final String supDynName = Creator.class.getName().replace('.', '/'); + final String interName = clazz.getName().replace('.', '/'); + final String interDesc = Type.getDescriptor(clazz); + ClassLoader loader = Creator.class.getClassLoader(); + String newDynName = supDynName + "_" + clazz.getSimpleName() + "_" + (System.currentTimeMillis() % 10000); + if (String.class.getClassLoader() != clazz.getClassLoader()) { + loader = clazz.getClassLoader(); + newDynName = interName + "_Dyn" + Creator.class.getSimpleName(); + } + try { + return (Creator) Class.forName(newDynName.replace('/', '.')).newInstance(); + } catch (Exception ex) { + } + Constructor constructor = null; + for (Constructor c : clazz.getConstructors()) { + if (c.getParameterCount() == 0) { + constructor = c; + break; + } + } + if (constructor == null) { + for (Constructor c : clazz.getConstructors()) { + if (c.getAnnotation(ConstructorProperties.class) != null) { + constructor = c; + break; + } + } + } + if (constructor == null) throw new RuntimeException("[" + clazz + "] have no public or java.beans.ConstructorProperties-Annotation constructor."); + //------------------------------------------------------------- + ClassWriter cw = new ClassWriter(0); + FieldVisitor fv; + MethodVisitor mv; + AnnotationVisitor av0; + cw.visit(V1_8, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, newDynName, "Ljava/lang/Object;L" + supDynName + "<" + interDesc + ">;", "java/lang/Object", new String[]{supDynName}); + + {//构造方法 + mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + ConstructorProperties cps = constructor.getAnnotation(ConstructorProperties.class); + if (cps != null) { + av0 = mv.visitAnnotation("Ljava/beans/ConstructorProperties;", true); + AnnotationVisitor av1 = av0.visitArray("value"); + for (String n : cps.value()) { + av1.visit(null, n); + } + av1.visitEnd(); + av0.visitEnd(); + } + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + {//create 方法 + mv = cw.visitMethod(ACC_PUBLIC + ACC_VARARGS, "create", "([Ljava/lang/Object;)L" + interName + ";", null, null); + mv.visitTypeInsn(NEW, interName); + mv.visitInsn(DUP); + //--------------------------------------- + { + Parameter[] params = constructor.getParameters(); + final int[] iconsts = {ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5}; + for (int i = 0; i < params.length; i++) { + mv.visitVarInsn(ALOAD, 1); + if (i < 6) { + mv.visitInsn(iconsts[i]); + } else { + mv.visitIntInsn(BIPUSH, i); + } + mv.visitInsn(AALOAD); + Class ct = params[i].getType(); + mv.visitTypeInsn(CHECKCAST, Type.getInternalName(ct)); + if (ct.isPrimitive()) { + Class fct = Array.get(Array.newInstance(ct, 1), 0).getClass(); + try { + Method pm = ct.getMethod(ct.getSimpleName() + "Value"); + mv.visitMethodInsn(INVOKEVIRTUAL, fct.getName().replace('.', '/'), pm.getName(), Type.getMethodDescriptor(pm), false); + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + } + } + } + //--------------------------------------- + mv.visitMethodInsn(INVOKESPECIAL, interName, "", Type.getConstructorDescriptor(constructor), false); + mv.visitInsn(ARETURN); + mv.visitMaxs((constructor.getParameterCount() > 0 ? (constructor.getParameterCount() + 3) : 2), 2); + mv.visitEnd(); + } + { //虚拟 create 方法 + mv = cw.visitMethod(ACC_PUBLIC + ACC_BRIDGE + ACC_VARARGS + ACC_SYNTHETIC, "create", "([Ljava/lang/Object;)Ljava/lang/Object;", null, null); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEVIRTUAL, newDynName, "create", "([Ljava/lang/Object;)" + interDesc, false); + mv.visitInsn(ARETURN); + mv.visitMaxs(2, 2); + mv.visitEnd(); + } + cw.visitEnd(); + byte[] bytes = cw.toByteArray(); + Class creatorClazz = new ClassLoader(loader) { + public final Class loadClass(String name, byte[] b) { + return defineClass(name, b, 0, b.length); + } + }.loadClass(newDynName.replace('/', '.'), bytes); + try { + return (Creator) creatorClazz.newInstance(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/com/wentch/redkale/util/DebugMethodVisitor.java b/src/com/wentch/redkale/util/DebugMethodVisitor.java new file mode 100644 index 000000000..3add2e39c --- /dev/null +++ b/src/com/wentch/redkale/util/DebugMethodVisitor.java @@ -0,0 +1,98 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import jdk.internal.org.objectweb.asm.*; + +/** + * + * @author zhangjx + */ +public class DebugMethodVisitor { + + private final MethodVisitor visitor; + + private boolean debug = false; + + public void setDebug(boolean d) { + debug = d; + } + + private static final String[] opcodes = new String[200]; //0 -18 + + static { + try { + for (java.lang.reflect.Field field : Opcodes.class.getFields()) { + String name = field.getName(); + if (name.startsWith("ASM")) continue; + if (name.startsWith("V1_")) continue; + if (name.startsWith("ACC_")) continue; + if (name.startsWith("T_")) continue; + if (name.startsWith("H_")) continue; + if (name.startsWith("F_")) continue; + if (field.getType() != int.class) continue; + opcodes[(int) field.get(null)] = name; + } + } catch (Exception ex) { + throw new RuntimeException(ex); //不可能会发生 + } + } + + public DebugMethodVisitor(MethodVisitor visitor) { + //super(Opcodes.ASM5, visitor); + this.visitor = visitor; + } + + public void visitParameter(String name, int access) { + visitor.visitParameter(name, access); + if (debug) System.out.println("mv.visitParameter(" + name + ", " + access + ");"); + } + + public void visitVarInsn(int opcode, int var) { + visitor.visitVarInsn(opcode, var); + if (debug) System.out.println("mv.visitVarInsn(" + opcodes[opcode] + ", " + var + ");"); + } + + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + visitor.visitMethodInsn(opcode, owner, name, desc, itf); + if (debug) System.out.println("mv.visitMethodInsn(" + opcodes[opcode] + ", \"" + owner + "\", \"" + name + "\", \"" + desc + "\", " + itf + ");"); + } + + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + visitor.visitFieldInsn(opcode, owner, name, desc); + if (debug) System.out.println("mv.visitFieldInsn(" + opcodes[opcode] + ", \"" + owner + "\", \"" + name + "\", \"" + desc + "\");"); + } + + public void visitTypeInsn(int opcode, String type) { + visitor.visitTypeInsn(opcode, type); + if (debug) System.out.println("mv.visitTypeInsn(" + opcodes[opcode] + ", \"" + type + "\");"); + } + + public void visitInsn(int opcode) { + visitor.visitInsn(opcode); + if (debug) System.out.println("mv.visitInsn(" + opcodes[opcode] + ");"); + } + + public void visitIntInsn(int opcode, int value) { + visitor.visitIntInsn(opcode, value); + if (debug) System.out.println("mv.visitIntInsn(" + opcodes[opcode] + ", " + value + ");"); + } + + public void visitLdcInsn(Object o) { + visitor.visitLdcInsn(o); + if (debug) System.out.println("mv.visitLdcInsn(" + o + ");"); + } + + public void visitMaxs(int maxStack, int maxLocals) { + visitor.visitMaxs(maxStack, maxLocals); + if (debug) System.out.println("mv.visitMaxs(" + maxStack + ", " + maxLocals + ");"); + } + + public void visitEnd() { + visitor.visitEnd(); + if (debug) System.out.println("mv.visitEnd();\r\n\r\n\r\n"); + } +} diff --git a/src/com/wentch/redkale/util/Ignore.java b/src/com/wentch/redkale/util/Ignore.java new file mode 100644 index 000000000..3c83e1e67 --- /dev/null +++ b/src/com/wentch/redkale/util/Ignore.java @@ -0,0 +1,22 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({TYPE, FIELD, METHOD}) +@Retention(RUNTIME) +public @interface Ignore { + +} diff --git a/src/com/wentch/redkale/util/ObjectPool.java b/src/com/wentch/redkale/util/ObjectPool.java new file mode 100644 index 000000000..3302d03cb --- /dev/null +++ b/src/com/wentch/redkale/util/ObjectPool.java @@ -0,0 +1,61 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +/** + * + * @author zhangjx + * @param + */ +public final class ObjectPool { + + public static interface Poolable { + + public void prepare(); + + public void release(); + } + + private final Queue queue; + + private final Creator creator; + + public ObjectPool(Class clazz) { + this(2, clazz); + } + + public ObjectPool(int max, Class clazz) { + this(max, Creator.create(clazz)); + } + + public ObjectPool(Creator creator) { + this(2, creator); + } + + public ObjectPool(int max, Creator creator) { + this.creator = creator; + this.queue = new ArrayBlockingQueue<>(Math.max(Runtime.getRuntime().availableProcessors() * 2, max)); + } + + public T poll() { + T result = queue.poll(); + if (result == null) { + result = this.creator.create(); + } else { + result.prepare(); + } + return result; + } + + public void offer(final T e) { + if (e != null) { + e.release(); + queue.offer(e); + } + } +} diff --git a/src/com/wentch/redkale/util/Reproduce.java b/src/com/wentch/redkale/util/Reproduce.java new file mode 100644 index 000000000..3efd59a14 --- /dev/null +++ b/src/com/wentch/redkale/util/Reproduce.java @@ -0,0 +1,105 @@ +package com.wentch.redkale.util; + +import java.lang.reflect.Modifier; +import java.util.function.Predicate; +import static jdk.internal.org.objectweb.asm.Opcodes.*; +import jdk.internal.org.objectweb.asm.*; + +public interface Reproduce { + + public D copy(D dest, S src); + + public static Reproduce create(final Class destClass, final Class srcClass) { + return create(destClass, srcClass, null); + } + + @SuppressWarnings("unchecked") + public static Reproduce create(final Class destClass, final Class srcClass, final Predicate excludeSetterPredicate) { + // ------------------------------------------------------------------------------ + final String supDynName = Reproduce.class.getName().replace('.', '/'); + final String destName = destClass.getName().replace('.', '/'); + final String srcName = srcClass.getName().replace('.', '/'); + final String destDesc = Type.getDescriptor(destClass); + final String srcDesc = Type.getDescriptor(srcClass); + String newDynName = supDynName + "Dyn_" + destClass.getSimpleName() + "_" + srcClass.getSimpleName(); + ClassLoader loader = Reproduce.class.getClassLoader(); + if (String.class.getClassLoader() != destClass.getClassLoader()) { + loader = destClass.getClassLoader(); + newDynName = destName + "_Dyn" + Reproduce.class.getSimpleName() + "_" + srcClass.getSimpleName(); + } + try { + return (Reproduce) Class.forName(newDynName.replace('/', '.')).newInstance(); + } catch (Exception ex) { + } + // ------------------------------------------------------------------------------ + ClassWriter cw = new ClassWriter(0); + FieldVisitor fv; + MethodVisitor mv; + AnnotationVisitor av0; + + cw.visit(V1_8, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, newDynName, "Ljava/lang/Object;L" + supDynName + "<" + destDesc + srcDesc + ">;", "java/lang/Object", new String[]{supDynName}); + + { // 构造函数 + mv = (cw.visitMethod(ACC_PUBLIC, "", "()V", null, null)); + //mv.setDebug(true); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + { + mv = (cw.visitMethod(ACC_PUBLIC, "copy", "(" + destDesc + srcDesc + ")" + destDesc, null, null)); + //mv.setDebug(true); + for (java.lang.reflect.Method getter : srcClass.getMethods()) { + if (Modifier.isStatic(getter.getModifiers())) continue; + if (getter.getParameterCount() > 0) continue; + if ("getClass".equals(getter.getName())) continue; + if (!getter.getName().startsWith("get") && !getter.getName().startsWith("is")) continue; + java.lang.reflect.Method setter; + boolean is = getter.getName().startsWith("is"); + try { + setter = destClass.getMethod(getter.getName().replaceFirst(is ? "is" : "get", "set"), getter.getReturnType()); + if (excludeSetterPredicate != null && excludeSetterPredicate.test(setter)) continue; + } catch (Exception e) { + continue; + } + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, srcName, getter.getName(), Type.getMethodDescriptor(getter), false); + mv.visitMethodInsn(INVOKEVIRTUAL, destName, setter.getName(), Type.getMethodDescriptor(setter), false); + } + mv.visitVarInsn(ALOAD, 1); + mv.visitInsn(ARETURN); + mv.visitMaxs(3, 3); + mv.visitEnd(); + } + { + mv = (cw.visitMethod(ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC, "copy", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", null, null)); + //mv.setDebug(true); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitTypeInsn(CHECKCAST, destName); + mv.visitVarInsn(ALOAD, 2); + mv.visitTypeInsn(CHECKCAST, srcName); + mv.visitMethodInsn(INVOKEVIRTUAL, newDynName, "copy", "(" + destDesc + srcDesc + ")" + destDesc, false); + mv.visitInsn(ARETURN); + mv.visitMaxs(3, 3); + mv.visitEnd(); + } + cw.visitEnd(); + // ------------------------------------------------------------------------------ + byte[] bytes = cw.toByteArray(); + Class creatorClazz = new ClassLoader(loader) { + public final Class loadClass(String name, byte[] b) { + return defineClass(name, b, 0, b.length); + } + }.loadClass(newDynName.replace('/', '.'), bytes); + try { + return (Reproduce) creatorClazz.newInstance(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/src/com/wentch/redkale/util/ResourceFactory.java b/src/com/wentch/redkale/util/ResourceFactory.java new file mode 100644 index 000000000..7c0f64eb2 --- /dev/null +++ b/src/com/wentch/redkale/util/ResourceFactory.java @@ -0,0 +1,204 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.*; +import java.util.regex.Pattern; +import javax.annotation.Resource; + +/** + * + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public final class ResourceFactory { + + private static final Logger logger = Logger.getLogger(ResourceFactory.class.getSimpleName()); + + private final ResourceFactory parent; + + private static final ResourceFactory instance = new ResourceFactory(null); + + private final ConcurrentHashMap interceptmap = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap, ConcurrentHashMap> store = new ConcurrentHashMap<>(); + + private ResourceFactory(ResourceFactory parent) { + this.parent = parent; + } + + public static ResourceFactory root() { + return instance; + } + + public ResourceFactory createChild() { + return new ResourceFactory(this); + } + + public void release() { + this.store.clear(); + } + + public void register(final Class clazz, final Object rs) { + register("", clazz, rs); + } + + public void register(final Object rs) { + if (rs != null) register("", rs.getClass(), rs); + } + + public void add(final Type clazz, final Intercepter rs) { + if (clazz == null || rs == null) return; + interceptmap.put(clazz, rs); + } + + public void register(final String name, final Object rs) { + register(name, rs.getClass(), rs); + } + + public void register(final String name, final Class clazz, final A rs) { + ConcurrentHashMap map = this.store.get(clazz); + if (map == null) { + ConcurrentHashMap sub = new ConcurrentHashMap<>(); + sub.put(name, rs); + store.put(clazz, sub); + } else { + map.put(name, rs); + } + } + + public A find(Class clazz) { + return find("", clazz); + } + + public A find(String name, Class clazz) { + Map map = this.store.get(clazz); + if (map != null) { + A rs = (A) map.get(name); + if (rs != null) return rs; + } + if (parent != null) return parent.find(name, clazz); + return null; + } + + public A findChild(String name, Class clazz) { + A rs = find(name, clazz); + if (rs != null) return rs; + Optional, ConcurrentHashMap>> opt = this.store.entrySet().stream() + .filter(x -> clazz.isAssignableFrom(x.getKey()) && x.getValue().containsKey(name)) + .findFirst(); + return opt.isPresent() ? (A) opt.get().getValue().get(name) : null; + } + + public Map find(final Pattern reg, Class clazz, A exclude) { + Map result = new LinkedHashMap<>(); + load(reg, clazz, exclude, result); + return result; + } + + private void load(final Pattern reg, Class clazz, final A exclude, final Map result) { + ConcurrentHashMap map = this.store.get(clazz); + if (map != null) { + map.forEach((x, y) -> { + if (y != exclude && reg.matcher(x).find() && result.get(x) == null) result.put(x, (A) y); + }); + } + if (parent != null) parent.load(reg, clazz, exclude, result); + } + + public boolean inject(final Object src) { + return inject(src, new ArrayList<>()); + } + + private boolean inject(final Object src, final List list) { + if (src == null) return false; + try { + list.add(src); + Class clazz = src.getClass(); + do { + for (Field field : clazz.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + field.setAccessible(true); + final Class type = field.getType(); + Resource rc = field.getAnnotation(Resource.class); + if (rc == null) { + boolean flag = true; + Object ns = field.get(src); + for (Object o : list) { + if (o == ns) { + flag = false; + break; + } + } + if (ns == null) continue; + if (ns.getClass().isPrimitive() || ns.getClass().isArray() || ns.getClass().getName().startsWith("java")) continue; + if (flag) this.inject(ns, list); + continue; + } + if (Modifier.isFinal(field.getModifiers())) continue; + Object rs; + if (Map.class.isAssignableFrom(type)) { + rs = find(Pattern.compile(rc.name().isEmpty() ? ".+" : rc.name()), (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[1], src); + } else { + if (rc.name().startsWith("property.")) { + rs = find(rc.name(), String.class); + } else { + rs = find(rc.name(), type); + } + } + if (rs == null) { + Intercepter it = findIntercepter(field.getGenericType(), field); + if (it != null) it.invoke(this, src, field); + continue; + } + if (!rs.getClass().isPrimitive() && type.isPrimitive()) { + if (type == int.class) { + rs = Integer.decode(rs.toString()); + } else if (type == long.class) { + rs = Long.decode(rs.toString()); + } else if (type == short.class) { + rs = Short.decode(rs.toString()); + } else if (type == boolean.class) { + rs = "true".equalsIgnoreCase(rs.toString()); + } else if (type == byte.class) { + rs = Byte.decode(rs.toString()); + } else if (type == float.class) { + rs = Float.parseFloat(rs.toString()); + } else if (type == double.class) { + rs = Double.parseDouble(rs.toString()); + } + } + field.set(src, rs); + } + } while ((clazz = clazz.getSuperclass()) != Object.class); + return true; + } catch (Exception ex) { + logger.log(Level.FINER, "inject " + src + " error", ex); + return false; + } + } + + private Intercepter findIntercepter(Type ft, Field field) { + Intercepter it = this.interceptmap.get(ft); + if (it != null) return it; + Class c = field.getType(); + for (Map.Entry en : this.interceptmap.entrySet()) { + Type t = en.getKey(); + if (t == ft) return en.getValue(); + if (t instanceof Class && (((Class) t)).isAssignableFrom(c)) return en.getValue(); + } + return parent == null ? null : parent.findIntercepter(ft, field); + } + + public static interface Intercepter { + + public void invoke(ResourceFactory factory, Object src, Field field); + } + +} diff --git a/src/com/wentch/redkale/util/Sheet.java b/src/com/wentch/redkale/util/Sheet.java new file mode 100644 index 000000000..f010b56cb --- /dev/null +++ b/src/com/wentch/redkale/util/Sheet.java @@ -0,0 +1,89 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.util.*; + +/** + * + * @author zhangjx + * @param + */ +@SuppressWarnings("unchecked") +public class Sheet implements java.io.Serializable { + + private long total = -1; + + private Collection rows; + + public Sheet() { + super(); + } + + public Sheet(int total, Collection data) { + this((long) total, data); + } + + public Sheet(long total, Collection data) { + this.total = total; + this.rows = (Collection) data; + } + + public static Sheet asSheet(Collection data) { + return new Sheet<>(data.size(), data); + } + + public void copyTo(Sheet copy) { + if (copy == null) { + return; + } + copy.total = this.total; + if (this.getRows() != null) { + copy.setRows(new ArrayList<>(this.getRows())); + } else { + copy.rows = null; + } + } + + /** + * 判断数据列表是否为空 + * + * @return + */ + public boolean isEmpty() { + return this.rows == null || this.rows.isEmpty(); + } + + @Override + public String toString() { + return "Sheet[total=" + this.total + ", rows=" + this.rows + "]"; + } + + public long getTotal() { + return this.total; + } + + public void setTotal(long total) { + this.total = total; + } + + public Collection getRows() { + return this.rows; + } + + public List list() { + return list(false); + } + + public List list(boolean created) { + if (this.rows == null) return created ? new ArrayList<>() : null; + return (this.rows instanceof List) ? (List) this.rows : new ArrayList<>(this.rows); + } + + public void setRows(Collection data) { + this.rows = (Collection) data; + } +} diff --git a/src/com/wentch/redkale/util/TwoLong.java b/src/com/wentch/redkale/util/TwoLong.java new file mode 100644 index 000000000..b989d711b --- /dev/null +++ b/src/com/wentch/redkale/util/TwoLong.java @@ -0,0 +1,78 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +/** + * + * @author zhangjx + */ +public final class TwoLong extends Number implements Comparable { + + private long first; + + private long second; + + public TwoLong(long one, long two) { + this.first = one; + this.second = two; + } + + public long getFirst() { + return first; + } + + public long getSecond() { + return second; + } + + public boolean compare(long one, long two) { + return this.first == one && this.second == two; + } + + @Override + public int hashCode() { + return intValue(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + final TwoLong other = (TwoLong) obj; + return (this.first == other.first && this.second == other.second); + } + + @Override + public String toString() { + return this.first + "_" + this.second; + } + + @Override + public int intValue() { + return (int) longValue(); + } + + @Override + public long longValue() { + return first ^ second; + } + + @Override + public float floatValue() { + return (float) longValue(); + } + + @Override + public double doubleValue() { + return (double) longValue(); + } + + @Override + public int compareTo(TwoLong o) { + return (int) (first == o.first ? (second - o.second) : (first - o.first)); + } + +} diff --git a/src/com/wentch/redkale/util/TypeToken.java b/src/com/wentch/redkale/util/TypeToken.java new file mode 100644 index 000000000..63b67ed02 --- /dev/null +++ b/src/com/wentch/redkale/util/TypeToken.java @@ -0,0 +1,26 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * + * @author zhangjx + * @param + */ +public abstract class TypeToken { + + private final Type type; + + public TypeToken() { + type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + } + + public final Type getType() { + return type; + } +} diff --git a/src/com/wentch/redkale/util/Utility.java b/src/com/wentch/redkale/util/Utility.java new file mode 100644 index 000000000..8e11ffab0 --- /dev/null +++ b/src/com/wentch/redkale/util/Utility.java @@ -0,0 +1,303 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.util; + +import java.io.*; +import java.lang.reflect.Field; +import java.net.*; +import java.nio.ByteBuffer; +import java.util.*; + +/** + * + * @author zhangjx + */ +public final class Utility { + + private static final char hex[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private static final sun.misc.Unsafe UNSAFE; + + private static final long strvaloffset; + + static { + sun.misc.Unsafe usafe = null; + long fd = 0L; + try { + Field safeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); + safeField.setAccessible(true); + usafe = (sun.misc.Unsafe) safeField.get(null); + fd = usafe.objectFieldOffset(String.class.getDeclaredField("value")); + } catch (Exception e) { + throw new RuntimeException(e); //不可能会发生 + } + UNSAFE = usafe; + strvaloffset = fd; + } + + private Utility() { + } + + public static void println(ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) return; + byte[] bytes = new byte[buffer.remaining()]; + buffer.put(bytes); + buffer.flip(); + (System.out).println(Arrays.toString(bytes)); + } + + /** + * 返回本机的第一个内网IPv4地址, 没有则返回null + *

+ * @return + */ + public static InetAddress localInetAddress() { + InetAddress back = null; + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = nifs.nextElement(); + if (!nif.isUp()) continue; + Enumeration eis = nif.getInetAddresses(); + while (eis.hasMoreElements()) { + InetAddress ia = eis.nextElement(); + if (ia.isLoopbackAddress()) back = ia; + if (ia.isSiteLocalAddress()) return ia; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return back; + } + + public static int today() { + java.time.LocalDate today = java.time.LocalDate.now(); + return today.getYear() * 10000 + today.getMonthValue() * 100 + today.getDayOfMonth(); + } + + public static char[] binToHex(byte[] bytes) { + return binToHex(bytes, 0, bytes.length); + } + + public static char[] binToHex(byte[] bytes, int offset, int len) { + final char[] sb = new char[len * 2]; + final int end = offset + len; + int index = 0; + final char[] hexs = hex; + for (int i = offset; i < end; i++) { + byte b = bytes[i]; + sb[index++] = (hexs[((b >> 4) & 0xF)]); + sb[index++] = hexs[((b) & 0xF)]; + } + return sb; + } + + public static byte[] hexToBin(CharSequence src) { + return hexToBin(src, 0, src.length()); + } + + public static byte[] hexToBin(CharSequence src, int offset, int len) { + final int size = (len + 1) / 2; + final byte[] bytes = new byte[size]; + final int end = offset + len; + String digits = "0123456789abcdef"; + for (int i = 0; i < size; i++) { + int ch1 = src.charAt(offset + i * 2); + if ('A' <= ch1 && 'F' >= ch1) ch1 = ch1 - 'A' + 'a'; + int ch2 = src.charAt(offset + i * 2 + 1); + if ('A' <= ch2 && 'F' >= ch2) ch2 = ch2 - 'A' + 'a'; + int pos1 = digits.indexOf(ch1); + if (pos1 < 0) throw new NumberFormatException(); + int pos2 = digits.indexOf(ch2); + if (pos2 < 0) throw new NumberFormatException(); + bytes[i] = (byte) (pos1 * 0x10 + pos2); + } + return bytes; + } + + public static byte[] hexToBin(char[] src) { + return hexToBin(src, 0, src.length); + } + + public static byte[] hexToBin(char[] src, int offset, int len) { + final int size = (len + 1) / 2; + final byte[] bytes = new byte[size]; + final int end = offset + len; + String digits = "0123456789abcdef"; + for (int i = 0; i < size; i++) { + int ch1 = src[offset + i * 2]; + if ('A' <= ch1 && 'F' >= ch1) ch1 = ch1 - 'A' + 'a'; + int ch2 = src[offset + i * 2 + 1]; + if ('A' <= ch2 && 'F' >= ch2) ch2 = ch2 - 'A' + 'a'; + int pos1 = digits.indexOf(ch1); + if (pos1 < 0) throw new NumberFormatException(); + int pos2 = digits.indexOf(ch2); + if (pos2 < 0) throw new NumberFormatException(); + bytes[i] = (byte) (pos1 * 0x10 + pos2); + } + return bytes; + } + + //----------------------------------------------------------------------------- + public static char[] decodeUTF8(final byte[] array) { + return decodeUTF8(array, 0, array.length); + } + + public static char[] decodeUTF8(final byte[] array, final int start, final int len) { + byte b; + int size = len; + final byte[] bytes = array; + final int limit = start + len; + for (int i = start; i < limit; i++) { + b = bytes[i]; + if ((b >> 5) == -2) { + size--; + } else if ((b >> 4) == -2) { + size -= 2; + } + } + final char[] text = new char[size]; + size = 0; + for (int i = start; i < limit;) { + b = bytes[i++]; + if (b >= 0) { + text[size++] = (char) b; + } else if ((b >> 5) == -2) { + text[size++] = (char) (((b << 6) ^ bytes[i++]) ^ (((byte) 0xC0 << 6) ^ ((byte) 0x80))); + } else if ((b >> 4) == -2) { + text[size++] = (char) ((b << 12) ^ (bytes[i++] << 6) ^ (bytes[i++] ^ (((byte) 0xE0 << 12) ^ ((byte) 0x80 << 6) ^ ((byte) 0x80)))); + } + } + return text; + } + + public static byte[] encodeUTF8(final String value) { + if (value == null) return new byte[0]; + return encodeUTF8((char[]) UNSAFE.getObject(value, strvaloffset)); + } + + public static byte[] encodeUTF8(final char[] array) { + return encodeUTF8(array, 0, array.length); + } + + public static byte[] encodeUTF8(final char[] text, final int start, final int len) { + char c; + int size = 0; + final char[] chars = text; + final int limit = start + len; + for (int i = start; i < limit; i++) { + c = chars[i]; + if (c < 0x80) { + size++; + } else if (c < 0x800) { + size += 2; + } else { + size += 3; + } + } + final byte[] bytes = new byte[size]; + size = 0; + for (int i = start; i < limit; i++) { + c = chars[i]; + if (c < 0x80) { + bytes[size++] = (byte) c; + } else if (c < 0x800) { + bytes[size++] = (byte) (0xc0 | (c >> 6)); + bytes[size++] = (byte) (0x80 | (c & 0x3f)); + } else { + bytes[size++] = (byte) (0xe0 | ((c >> 12))); + bytes[size++] = (byte) (0x80 | ((c >> 6) & 0x3f)); + bytes[size++] = (byte) (0x80 | (c & 0x3f)); + } + } + return bytes; + } + + public static char[] charArray(String value) { + return value == null ? null : (char[]) UNSAFE.getObject(value, strvaloffset); + } + + public static ByteBuffer encodeUTF8(final ByteBuffer buffer, final char[] array) { + return encodeUTF8(buffer, array, 0, array.length); + } + + public static ByteBuffer encodeUTF8(final ByteBuffer buffer, int bytesLength, final char[] array) { + return encodeUTF8(buffer, bytesLength, array, 0, array.length); + } + + public static int encodeUTF8Length(String value) { + if (value == null) return -1; + return encodeUTF8Length((char[]) UNSAFE.getObject(value, strvaloffset)); + } + + public static int encodeUTF8Length(final char[] text) { + return encodeUTF8Length(text, 0, text.length); + } + + public static int encodeUTF8Length(final char[] text, final int start, final int len) { + char c; + int size = 0; + final char[] chars = text; + final int limit = start + len; + for (int i = start; i < limit; i++) { + c = chars[i]; + if (c < 0x80) { + size++; + } else if (c < 0x800) { + size += 2; + } else { + size += 3; + } + } + return size; + } + + public static ByteBuffer encodeUTF8(final ByteBuffer buffer, final char[] text, final int start, final int len) { + return encodeUTF8(buffer, encodeUTF8Length(text, start, len), text, start, len); + } + + public static ByteBuffer encodeUTF8(final ByteBuffer buffer, int bytesLength, final char[] text, final int start, final int len) { + char c; + char[] chars = text; + final int limit = start + len; + int remain = buffer.remaining(); + final ByteBuffer buffer2 = remain >= bytesLength ? null : ByteBuffer.allocate(bytesLength - remain + 3); //最差情况buffer最后两byte没有填充 + ByteBuffer buf = buffer; + for (int i = start; i < limit; i++) { + c = chars[i]; + if (c < 0x80) { + if (buf.remaining() < 1) buf = buffer2; + buf.put((byte) c); + } else if (c < 0x800) { + if (buf.remaining() < 2) buf = buffer2; + buf.put((byte) (0xc0 | (c >> 6))); + buf.put((byte) (0x80 | (c & 0x3f))); + } else { + if (buf.remaining() < 3) buf = buffer2; + buf.put((byte) (0xe0 | ((c >> 12)))); + buf.put((byte) (0x80 | ((c >> 6) & 0x3f))); + buf.put((byte) (0x80 | (c & 0x3f))); + } + } + if (buffer2 != null) buffer2.flip(); + return buffer2; + } + + //----------------------------------------------------------------------------- + public static String read(InputStream in) throws IOException { + return read(in, "UTF-8"); + } + + public static String read(InputStream in, String charsetName) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(1024); + byte[] bytes = new byte[1024]; + int pos; + while ((pos = in.read(bytes)) != -1) { + out.write(bytes, 0, pos); + } + return charsetName == null ? out.toString() : out.toString(charsetName); + } +} diff --git a/src/com/wentch/redkale/watch/WatchBean.java b/src/com/wentch/redkale/watch/WatchBean.java new file mode 100644 index 000000000..60bbe9ae9 --- /dev/null +++ b/src/com/wentch/redkale/watch/WatchBean.java @@ -0,0 +1,21 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.watch; + +/** + * + * @author zhangjx + */ +public interface WatchBean { + + public String getName(); + + public String getDescription(); + + public long getValue(); + + public boolean isInterval(); +} diff --git a/src/com/wentch/redkale/watch/WatchFactory.java b/src/com/wentch/redkale/watch/WatchFactory.java new file mode 100644 index 000000000..ec402fa1b --- /dev/null +++ b/src/com/wentch/redkale/watch/WatchFactory.java @@ -0,0 +1,98 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.watch; + +import java.lang.ref.WeakReference; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.LongSupplier; + +/** + * + * @author zhangjx + */ +public final class WatchFactory { + + private static final WatchFactory instance = new WatchFactory(null); + + private final List> beans = new CopyOnWriteArrayList<>(); + + private final WatchFactory parent; + + private WatchFactory(WatchFactory parent) { + this.parent = parent; + } + + public void register(WatchBean bean) { + if (bean != null) beans.add(new WeakReference<>(bean)); + } + + public static WatchFactory root() { + return instance; + } + +// public WatchFactory createChild() { +// return new WatchFactory(this); +// } + public WatchNumber createWatchNumber(String name) { + return createWatchNumber(name, "", false, 0); + } + + public WatchNumber createWatchNumber(String name, boolean interval) { + return createWatchNumber(name, "", interval, 0); + } + + public WatchNumber createWatchNumber(String name, String description) { + return createWatchNumber(name, description, false, 0); + } + + public WatchNumber createWatchNumber(String name, String description, long v) { + return createWatchNumber(name, description, false, 0); + } + + public WatchNumber createWatchNumber(String name, String description, boolean interval) { + return createWatchNumber(name, description, interval, 0); + } + + public WatchNumber createWatchNumber(String name, String description, boolean interval, long v) { + WatchNumber bean = new WatchNumber(name, description, interval, v); + register(bean); + return bean; + } + + public void register(String name, LongSupplier supplier) { + register(name, "", supplier); + } + + public void register(String name, String description, LongSupplier supplier) { + register(new WatchSupplier(name, description, supplier)); + } + + public boolean inject(final Object src) { + return inject(src, new ArrayList<>()); + } + + private boolean inject(final Object src, final List list) { + if (src == null) return false; + try { + list.add(src); + Class clazz = src.getClass(); + do { + for (Field field : clazz.getDeclaredFields()) { + if (Modifier.isFinal(field.getModifiers())) continue; + field.setAccessible(true); + final Class type = field.getType(); + WatchOn wo = field.getAnnotation(WatchOn.class); + + } + } while ((clazz = clazz.getSuperclass()) != Object.class); + return true; + } catch (Exception ex) { + return false; + } + } +} diff --git a/src/com/wentch/redkale/watch/WatchNumber.java b/src/com/wentch/redkale/watch/WatchNumber.java new file mode 100644 index 000000000..45a558345 --- /dev/null +++ b/src/com/wentch/redkale/watch/WatchNumber.java @@ -0,0 +1,49 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.watch; + +import java.util.concurrent.atomic.*; + +/** + * + * @author zhangjx + */ +public final class WatchNumber extends AtomicLong implements WatchBean { + + private final boolean interval; + + private final String name; + + private final String description; + + WatchNumber(String name, String description, boolean interval, long v) { + this.name = name; + this.description = description; + this.interval = interval; + this.set(v); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public long getValue() { + return super.longValue(); + } + + @Override + public boolean isInterval() { + return interval; + } + +} diff --git a/src/com/wentch/redkale/watch/WatchOn.java b/src/com/wentch/redkale/watch/WatchOn.java new file mode 100644 index 000000000..83d07c2cb --- /dev/null +++ b/src/com/wentch/redkale/watch/WatchOn.java @@ -0,0 +1,34 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.watch; + +import java.lang.annotation.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 该注解只能放在field类型为Collection, Map, 或者java.util.concurrent.atomic.AtomicXXX的Number类); + * + * @author zhangjx + */ +@Inherited +@Documented +@Target({FIELD}) +@Retention(RUNTIME) +public @interface WatchOn { + + String name(); + + String description() default ""; + + /** + * 该值指明是不是只收集阶段数据, 而且被注解的字段只能被赋予java.util.concurrent.atomic.AtomicXXX的Number类型字段。 + * 例如收集每分钟的注册用户数, 就需要将interval设置true。 + * + * @return + */ + boolean interval() default false; +} diff --git a/src/com/wentch/redkale/watch/WatchSupplier.java b/src/com/wentch/redkale/watch/WatchSupplier.java new file mode 100644 index 000000000..a7c41c77e --- /dev/null +++ b/src/com/wentch/redkale/watch/WatchSupplier.java @@ -0,0 +1,47 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.wentch.redkale.watch; + +import java.util.function.LongSupplier; + +/** + * + * @author zhangjx + */ +public final class WatchSupplier implements WatchBean { + + private final String name; + + private final String description; + + private final LongSupplier supplier; + + WatchSupplier(String name, String description, LongSupplier supplier) { + this.name = name; + this.description = description; + this.supplier = supplier; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public long getValue() { + return supplier == null ? Long.MIN_VALUE : supplier.getAsLong(); + } + + @Override + public boolean isInterval() { + return false; + } +}