From 04563874ddaac702d6c715eaa89c29b253f4c54e Mon Sep 17 00:00:00 2001 From: mikaelpeltier Date: Wed, 24 Jun 2015 14:31:11 +0200 Subject: Add simpleframework source files Change-Id: I18d01df16de2868ca5458f79a88e6070b75db2c3 (cherry picked from commit 3e9f84cf7b22f6970eb8041ca38d12d75c6bb270) --- simple/pom.xml | 15 + simple/simple-common/pom.xml | 132 + .../java/org/simpleframework/common/KeyMap.java | 93 + .../simpleframework/common/buffer/Allocator.java | 55 + .../common/buffer/ArrayAllocator.java | 111 + .../simpleframework/common/buffer/ArrayBuffer.java | 397 + .../org/simpleframework/common/buffer/Buffer.java | 129 + .../common/buffer/BufferAllocator.java | 229 + .../common/buffer/BufferException.java | 43 + .../common/buffer/FileAllocator.java | 137 + .../simpleframework/common/buffer/FileBuffer.java | 622 + .../simpleframework/common/buffer/FileWatcher.java | 179 + .../common/buffer/FilterAllocator.java | 123 + .../common/encode/Base64Encoder.java | 166 + .../common/encode/Base64InputStream.java | 123 + .../common/encode/Base64OutputStream.java | 138 + .../org/simpleframework/common/lease/Cleaner.java | 44 + .../org/simpleframework/common/lease/Contract.java | 77 + .../common/lease/ContractController.java | 84 + .../common/lease/ContractLease.java | 119 + .../common/lease/ContractMaintainer.java | 115 + .../common/lease/ContractQueue.java | 44 + .../simpleframework/common/lease/Expiration.java | 163 + .../org/simpleframework/common/lease/Lease.java | 85 + .../simpleframework/common/lease/LeaseCleaner.java | 155 + .../common/lease/LeaseException.java | 52 + .../simpleframework/common/lease/LeaseManager.java | 93 + .../org/simpleframework/common/lease/LeaseMap.java | 83 + .../common/lease/LeaseProvider.java | 60 + .../simpleframework/common/parse/MapParser.java | 251 + .../simpleframework/common/parse/ParseBuffer.java | 247 + .../org/simpleframework/common/parse/Parser.java | 197 + .../common/thread/ConcurrentExecutor.java | 109 + .../common/thread/ConcurrentScheduler.java | 122 + .../org/simpleframework/common/thread/Daemon.java | 164 + .../common/thread/DaemonFactory.java | 147 + .../common/thread/ExecutorQueue.java | 128 + .../simpleframework/common/thread/Scheduler.java | 57 + .../common/thread/SchedulerQueue.java | 127 + .../common/thread/SynchronousExecutor.java | 43 + .../java/org/simpleframework/common/KeyTest.java | 195 + .../common/buffer/ArrayBufferTest.java | 54 + .../common/buffer/BufferAllocatorTest.java | 79 + .../common/buffer/FileBufferTest.java | 45 + .../common/buffer/FileByteQueue.java | 109 + .../common/buffer/FileByteQueueTest.java | 22 + .../common/buffer/queue/ArrayByteQueue.java | 100 + .../common/buffer/queue/ArrayByteQueueTest.java | 119 + .../common/buffer/queue/BufferQueue.java | 67 + .../common/buffer/queue/BufferQueueTest.java | 44 + .../common/buffer/queue/ByteQueue.java | 13 + .../common/buffer/queue/ByteQueueStream.java | 40 + .../common/lease/ContractQueueTest.java | 57 + .../simpleframework/common/lease/ContractTest.java | 40 + .../common/lease/LeaseManagerTest.java | 227 + .../simpleframework/common/lease/LeaseTest.java | 87 + .../simpleframework/common/lease/TimeTestCase.java | 25 + .../common/thread/SchedulerTest.java | 65 + .../common/thread/TransientApplication.java | 54 + simple/simple-http/pom.xml | 142 + .../java/org/simpleframework/http/Address.java | 157 + .../simpleframework/http/ContentDisposition.java | 59 + .../java/org/simpleframework/http/ContentType.java | 142 + .../main/java/org/simpleframework/http/Cookie.java | 527 + .../main/java/org/simpleframework/http/Method.java | 70 + .../main/java/org/simpleframework/http/Part.java | 107 + .../main/java/org/simpleframework/http/Path.java | 166 + .../java/org/simpleframework/http/Principal.java | 48 + .../java/org/simpleframework/http/Protocol.java | 370 + .../main/java/org/simpleframework/http/Query.java | 99 + .../java/org/simpleframework/http/Request.java | 210 + .../org/simpleframework/http/RequestHeader.java | 201 + .../java/org/simpleframework/http/RequestLine.java | 98 + .../org/simpleframework/http/RequestWrapper.java | 520 + .../java/org/simpleframework/http/Response.java | 262 + .../org/simpleframework/http/ResponseHeader.java | 304 + .../org/simpleframework/http/ResponseWrapper.java | 747 + .../main/java/org/simpleframework/http/Scheme.java | 136 + .../main/java/org/simpleframework/http/Status.java | 320 + .../java/org/simpleframework/http/StatusLine.java | 122 + .../org/simpleframework/http/core/BodyEncoder.java | 108 + .../http/core/BodyEncoderException.java | 58 + .../http/core/BodyEncoderFactory.java | 118 + .../simpleframework/http/core/BodyObserver.java | 121 + .../simpleframework/http/core/ChunkedEncoder.java | 221 + .../simpleframework/http/core/CloseEncoder.java | 179 + .../org/simpleframework/http/core/Collector.java | 50 + .../org/simpleframework/http/core/Container.java | 62 + .../http/core/ContainerController.java | 161 + .../simpleframework/http/core/ContainerEvent.java | 93 + .../http/core/ContainerSocketProcessor.java | 155 + .../http/core/ContainerTransportProcessor.java | 96 + .../org/simpleframework/http/core/Controller.java | 100 + .../simpleframework/http/core/Conversation.java | 358 + .../simpleframework/http/core/EmptyEncoder.java | 132 + .../http/core/FixedLengthEncoder.java | 198 + .../simpleframework/http/core/QueryBuilder.java | 148 + .../simpleframework/http/core/QueryCombiner.java | 148 + .../http/core/RequestCertificate.java | 183 + .../http/core/RequestCollector.java | 184 + .../http/core/RequestDispatcher.java | 128 + .../simpleframework/http/core/RequestEntity.java | 398 + .../simpleframework/http/core/RequestMessage.java | 341 + .../simpleframework/http/core/RequestReader.java | 131 + .../simpleframework/http/core/ResponseBuffer.java | 303 + .../simpleframework/http/core/ResponseEncoder.java | 324 + .../simpleframework/http/core/ResponseEntity.java | 437 + .../http/core/ResponseException.java | 58 + .../simpleframework/http/core/ResponseMessage.java | 283 + .../http/core/ResponseObserver.java | 238 + .../java/org/simpleframework/http/core/Timer.java | 94 + .../http/message/ArrayConsumer.java | 184 + .../org/simpleframework/http/message/Body.java | 95 + .../simpleframework/http/message/BodyConsumer.java | 43 + .../http/message/BoundaryConsumer.java | 206 + .../simpleframework/http/message/BufferBody.java | 166 + .../simpleframework/http/message/BufferPart.java | 160 + .../simpleframework/http/message/ByteConsumer.java | 64 + .../http/message/ChunkedConsumer.java | 258 + .../http/message/ConsumerFactory.java | 201 + .../http/message/ContentConsumer.java | 226 + .../http/message/ContinueDispatcher.java | 88 + .../http/message/EmptyConsumer.java | 69 + .../http/message/EmptyInputStream.java | 44 + .../org/simpleframework/http/message/Entity.java | 75 + .../http/message/EntityConsumer.java | 184 + .../http/message/FileUploadConsumer.java | 272 + .../http/message/FixedLengthConsumer.java | 128 + .../org/simpleframework/http/message/Header.java | 213 + .../http/message/HeaderConsumer.java | 114 + .../org/simpleframework/http/message/Message.java | 273 + .../http/message/MessageHeader.java | 477 + .../http/message/PartBodyConsumer.java | 129 + .../simpleframework/http/message/PartConsumer.java | 135 + .../org/simpleframework/http/message/PartData.java | 101 + .../http/message/PartEntryConsumer.java | 112 + .../http/message/PartEntryFactory.java | 84 + .../simpleframework/http/message/PartFactory.java | 78 + .../http/message/PartHeaderConsumer.java | 85 + .../simpleframework/http/message/PartSeries.java | 68 + .../http/message/PartSeriesConsumer.java | 165 + .../http/message/RequestConsumer.java | 457 + .../org/simpleframework/http/message/Segment.java | 163 + .../http/message/SegmentConsumer.java | 750 + .../http/message/TokenConsumer.java | 113 + .../http/message/UpdateConsumer.java | 143 + .../simpleframework/http/parse/AddressParser.java | 1347 ++ .../http/parse/ContentDispositionParser.java | 296 + .../http/parse/ContentTypeParser.java | 556 + .../simpleframework/http/parse/CookieParser.java | 589 + .../org/simpleframework/http/parse/DateParser.java | 642 + .../simpleframework/http/parse/LanguageParser.java | 156 + .../org/simpleframework/http/parse/ListParser.java | 456 + .../org/simpleframework/http/parse/PathParser.java | 726 + .../http/parse/PrincipalParser.java | 362 + .../simpleframework/http/parse/QueryParser.java | 636 + .../simpleframework/http/parse/ValueParser.java | 108 + .../simpleframework/http/socket/BinaryData.java | 75 + .../org/simpleframework/http/socket/CloseCode.java | 150 + .../java/org/simpleframework/http/socket/Data.java | 51 + .../simpleframework/http/socket/DataConverter.java | 111 + .../org/simpleframework/http/socket/DataFrame.java | 212 + .../org/simpleframework/http/socket/Frame.java | 85 + .../simpleframework/http/socket/FrameChannel.java | 117 + .../simpleframework/http/socket/FrameListener.java | 64 + .../org/simpleframework/http/socket/FrameType.java | 142 + .../org/simpleframework/http/socket/Reason.java | 97 + .../org/simpleframework/http/socket/Session.java | 91 + .../org/simpleframework/http/socket/TextData.java | 75 + .../http/socket/service/AcceptToken.java | 127 + .../http/socket/service/DirectRouter.java | 107 + .../http/socket/service/FrameBuilder.java | 118 + .../http/socket/service/FrameCollector.java | 179 + .../http/socket/service/FrameConnection.java | 214 + .../http/socket/service/FrameConsumer.java | 162 + .../http/socket/service/FrameEncoder.java | 229 + .../http/socket/service/FrameHeader.java | 80 + .../http/socket/service/FrameHeaderConsumer.java | 235 + .../http/socket/service/FrameProcessor.java | 255 + .../http/socket/service/OutputBarrier.java | 99 + .../http/socket/service/PathRouter.java | 111 + .../http/socket/service/ProtocolRouter.java | 105 + .../http/socket/service/ReasonExtractor.java | 114 + .../http/socket/service/RequestValidator.java | 137 + .../http/socket/service/ResponseBuilder.java | 159 + .../http/socket/service/Router.java | 59 + .../http/socket/service/RouterContainer.java | 109 + .../http/socket/service/Service.java | 44 + .../http/socket/service/ServiceChannel.java | 149 + .../http/socket/service/ServiceDispatcher.java | 101 + .../http/socket/service/ServiceEvent.java | 97 + .../http/socket/service/ServiceSession.java | 139 + .../http/socket/service/SessionBuilder.java | 93 + .../http/socket/service/SessionDispatcher.java | 111 + .../http/socket/service/StatusChecker.java | 220 + .../http/socket/service/StatusResultListener.java | 93 + .../org/simpleframework/http/ConnectionTest.java | 267 + .../java/org/simpleframework/http/CookieTest.java | 63 + .../test/java/org/simpleframework/http/Debug.java | 11 + .../http/MockRenegotiationServer.java | 434 + .../java/org/simpleframework/http/MockSocket.java | 45 + .../java/org/simpleframework/http/MockTrace.java | 8 + .../simpleframework/http/RenegotiationExample.java | 351 + .../java/org/simpleframework/http/StatusTest.java | 20 + .../org/simpleframework/http/StreamTransport.java | 67 + .../simpleframework/http/core/AccumulatorTest.java | 99 + .../http/core/ChunkedProducerTest.java | 43 + .../org/simpleframework/http/core/Chunker.java | 52 + .../java/org/simpleframework/http/core/Client.java | 264 + .../org/simpleframework/http/core/Connector.java | 9 + .../http/core/ConversationTest.java | 126 + .../simpleframework/http/core/DribbleCursor.java | 62 + .../http/core/FixedConsumerTest.java | 80 + .../http/core/FixedProducerTest.java | 50 + .../org/simpleframework/http/core/MessageTest.java | 72 + .../org/simpleframework/http/core/MockChannel.java | 57 + .../simpleframework/http/core/MockController.java | 55 + .../org/simpleframework/http/core/MockEntity.java | 49 + .../simpleframework/http/core/MockObserver.java | 62 + .../org/simpleframework/http/core/MockPart.java | 49 + .../http/core/MockProxyRequest.java | 67 + .../org/simpleframework/http/core/MockRequest.java | 202 + .../simpleframework/http/core/MockResponse.java | 95 + .../org/simpleframework/http/core/MockSender.java | 75 + .../org/simpleframework/http/core/MockSocket.java | 42 + .../org/simpleframework/http/core/PayloadTest.java | 97 + .../http/core/ProducerExceptionTest.java | 23 + .../http/core/QueryBuilderTest.java | 35 + .../http/core/ReactorProcessorTest.java | 247 + .../org/simpleframework/http/core/ReactorTest.java | 178 + .../http/core/RequestConsumerTest.java | 138 + .../org/simpleframework/http/core/RequestTest.java | 144 + .../java/org/simpleframework/http/core/Result.java | 37 + .../org/simpleframework/http/core/StopTest.java | 176 + .../simpleframework/http/core/StreamCursor.java | 74 + .../simpleframework/http/core/ThreadDumper.java | 183 + .../java/org/simpleframework/http/core/Ticket.java | 22 + .../simpleframework/http/core/TicketProcessor.java | 28 + .../simpleframework/http/core/TransferTest.java | 195 + .../http/core/WebSocketUpgradeTest.java | 126 + .../http/message/BoundaryConsumerTest.java | 77 + .../http/message/ChunkedConsumerTest.java | 118 + .../http/message/ContentConsumerTest.java | 99 + .../http/message/FileUploadConsumerTest.java | 86 + .../http/message/MessageHeaderTest.java | 48 + .../org/simpleframework/http/message/MockBody.java | 47 + .../simpleframework/http/message/MockHeader.java | 22 + .../simpleframework/http/message/MockSegment.java | 83 + .../http/message/PartConsumerTest.java | 33 + .../http/message/PartSeriesConsumerTest.java | 157 + .../http/message/ReplyConsumer.java | 141 + .../http/message/SegmentConsumerTest.java | 103 + .../http/message/TokenConsumerTest.java | 55 + .../http/parse/AddressParserTest.java | 92 + .../http/parse/ContentDispositionParserTest.java | 33 + .../http/parse/ContentTypeParserTest.java | 74 + .../http/parse/CookieParserTest.java | 22 + .../simpleframework/http/parse/DateParserTest.java | 55 + .../http/parse/LanguageParserTest.java | 26 + .../simpleframework/http/parse/ListParserTest.java | 97 + .../simpleframework/http/parse/ParameterTest.java | 69 + .../simpleframework/http/parse/PathParserTest.java | 97 + .../http/parse/PriorityQueueTest.java | 48 + .../http/parse/QueryParserTest.java | 69 + .../http/socket/WebFrameTypeTest.java | 29 + .../http/socket/WebSocketAnalyzer.java | 66 + .../http/socket/WebSocketCertificate.java | 170 + .../http/socket/WebSocketChatApplication.java | 170 + .../http/socket/WebSocketChatLogin.html | 12 + .../http/socket/WebSocketChatRoom.html | 29 + .../http/socket/WebSocketChatRoom.java | 86 + .../http/socket/WebSocketChatRoomListener.java | 106 + .../http/socket/WebSocketKeyTest.java | 37 + .../http/socket/WebSocketTestClient.java | 25 + .../http/socket/service/PathRouterTest.java | 62 + .../socket/service/WebSocketPerformanceTest.java | 439 + .../http/socket/table/WebSocketTable.java | 151 + .../http/socket/table/WebSocketTableCell.java | 26 + .../http/socket/table/WebSocketTableChanger.java | 58 + .../http/socket/table/WebSocketTableColumn.java | 22 + .../socket/table/WebSocketTableColumnStyle.java | 35 + .../http/socket/table/WebSocketTableListener.java | 69 + .../http/socket/table/WebSocketTableRow.java | 84 + .../socket/table/WebSocketTableRowAnnotator.java | 89 + .../socket/table/WebSocketTableRowChanger.java | 73 + .../http/socket/table/WebSocketTableSchema.java | 40 + .../socket/table/WebSocketTableSubscription.java | 44 + .../http/socket/table/WebSocketTableSweeper.java | 33 + .../socket/table/WebSocketTableUpdateType.java | 17 + .../http/socket/table/WebSocketTableUpdater.java | 126 + .../table/WebSocketTableUpdaterApplication.java | 154 + .../http/socket/table/WebSocketValueEncoder.java | 53 + .../http/socket/table/bootstrap.css | 5774 ++++++++ .../http/socket/table/bootstrap.min.js | 6 + .../org/simpleframework/http/socket/table/delta.js | 344 + .../simpleframework/http/socket/table/failure.png | Bin 0 -> 3113 bytes .../http/socket/table/font-awesome.min.css | 33 + .../simpleframework/http/socket/table/grid.html | 103 + .../simpleframework/http/socket/table/index.html | 40 + .../http/socket/table/jquery-2.1.1.min.js | 4 + .../simpleframework/http/socket/table/login.html | 12 + .../simpleframework/http/socket/table/main.html | 182 + .../simpleframework/http/socket/table/pending.png | Bin 0 -> 4897 bytes .../simpleframework/http/socket/table/success.png | Bin 0 -> 4514 bytes .../simpleframework/http/socket/table/table.html | 347 + .../simpleframework/http/socket/table/w2ui-1.4.css | 2750 ++++ .../simpleframework/http/socket/table/w2ui-1.4.js | 13617 +++++++++++++++++++ .../http/socket/table/w2ui-1.4.min.css | 2 + .../http/socket/table/w2ui-1.4.min.js | 11 + simple/simple-transport/pom.xml | 137 + .../org/simpleframework/transport/ByteCursor.java | 131 + .../org/simpleframework/transport/ByteReader.java | 107 + .../org/simpleframework/transport/ByteWriter.java | 98 + .../org/simpleframework/transport/Certificate.java | 65 + .../transport/CertificateChallenge.java | 73 + .../org/simpleframework/transport/Channel.java | 128 + .../simpleframework/transport/FlushScheduler.java | 197 + .../simpleframework/transport/FlushSignaller.java | 120 + .../org/simpleframework/transport/Handshake.java | 652 + .../org/simpleframework/transport/Negotiation.java | 69 + .../transport/NegotiationState.java | 337 + .../transport/OperationFactory.java | 150 + .../java/org/simpleframework/transport/Phase.java | 165 + .../org/simpleframework/transport/PhaseType.java | 45 + .../simpleframework/transport/SecureTransport.java | 428 + .../simpleframework/transport/ServerCleaner.java | 86 + .../java/org/simpleframework/transport/Socket.java | 89 + .../simpleframework/transport/SocketBuffer.java | 308 + .../transport/SocketBufferAppender.java | 289 + .../transport/SocketBufferWriter.java | 103 + .../simpleframework/transport/SocketFlusher.java | 142 + .../simpleframework/transport/SocketProcessor.java | 61 + .../simpleframework/transport/SocketTransport.java | 262 + .../simpleframework/transport/SocketWrapper.java | 144 + .../org/simpleframework/transport/Transport.java | 91 + .../transport/TransportChannel.java | 195 + .../simpleframework/transport/TransportCursor.java | 260 + .../transport/TransportDispatcher.java | 114 + .../simpleframework/transport/TransportEvent.java | 91 + .../transport/TransportException.java | 55 + .../transport/TransportProcessor.java | 63 + .../simpleframework/transport/TransportReader.java | 229 + .../transport/TransportSocketProcessor.java | 163 + .../simpleframework/transport/TransportWriter.java | 150 + .../transport/connect/Connection.java | 73 + .../transport/connect/ConnectionEvent.java | 42 + .../transport/connect/ConnectionException.java | 58 + .../transport/connect/SocketAcceptor.java | 315 + .../transport/connect/SocketAnalyzer.java | 84 + .../transport/connect/SocketConnection.java | 141 + .../transport/connect/SocketListener.java | 125 + .../transport/connect/SocketListenerManager.java | 127 + .../transport/connect/SocketTrace.java | 75 + .../simpleframework/transport/reactor/Action.java | 71 + .../transport/reactor/ActionDistributor.java | 741 + .../transport/reactor/ActionSelector.java | 193 + .../transport/reactor/ActionSet.java | 269 + .../transport/reactor/CancelAction.java | 114 + .../transport/reactor/ExecuteAction.java | 121 + .../transport/reactor/ExecutorReactor.java | 132 + .../simpleframework/transport/reactor/Latch.java | 71 + .../transport/reactor/Operation.java | 69 + .../transport/reactor/OperationDistributor.java | 62 + .../transport/reactor/PartitionDistributor.java | 136 + .../simpleframework/transport/reactor/Reactor.java | 79 + .../transport/reactor/ReactorEvent.java | 120 + .../transport/reactor/SynchronousReactor.java | 107 + .../org/simpleframework/transport/trace/Trace.java | 57 + .../transport/trace/TraceAnalyzer.java | 59 + .../org/simpleframework/transport/MockSocket.java | 45 + .../simpleframework/transport/ServerBuffer.java | 75 + .../transport/SocketBufferTest.java | 86 + .../transport/SocketTransportPipeTest.java | 92 + .../transport/SocketTransportTest.java | 51 + .../simpleframework/transport/StreamTransport.java | 66 + .../transport/TransportCursorTest.java | 83 + .../simpleframework/transport/TransportTest.java | 404 + .../transport/reactor/DistributorTest.java | 269 + .../transport/trace/CompareQueueTest.java | 174 + .../simpleframework/transport/trace/MockTrace.java | 6 + 380 files changed, 76677 insertions(+) create mode 100644 simple/pom.xml create mode 100644 simple/simple-common/pom.xml create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/KeyMap.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/Allocator.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayAllocator.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayBuffer.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/Buffer.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferAllocator.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferException.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileAllocator.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileBuffer.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileWatcher.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/buffer/FilterAllocator.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64Encoder.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64InputStream.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64OutputStream.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/Cleaner.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/Contract.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractController.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractLease.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractMaintainer.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractQueue.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/Expiration.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/Lease.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseCleaner.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseException.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseManager.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseMap.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseProvider.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/parse/MapParser.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/parse/ParseBuffer.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/parse/Parser.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentExecutor.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentScheduler.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/Daemon.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/DaemonFactory.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/ExecutorQueue.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/Scheduler.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/SchedulerQueue.java create mode 100644 simple/simple-common/src/main/java/org/simpleframework/common/thread/SynchronousExecutor.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/KeyTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/ArrayBufferTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/BufferAllocatorTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileBufferTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueue.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueueTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueue.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueueTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueue.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueueTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueue.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueueStream.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractQueueTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseManagerTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/lease/TimeTestCase.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/thread/SchedulerTest.java create mode 100644 simple/simple-common/src/test/java/org/simpleframework/common/thread/TransientApplication.java create mode 100644 simple/simple-http/pom.xml create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Address.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/ContentDisposition.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/ContentType.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Cookie.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Method.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Part.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Path.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Principal.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Protocol.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Query.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Request.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/RequestHeader.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/RequestLine.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/RequestWrapper.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Response.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/ResponseHeader.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/ResponseWrapper.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Scheme.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/Status.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/StatusLine.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderException.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderFactory.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/BodyObserver.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ChunkedEncoder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/CloseEncoder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/Collector.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/Container.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerController.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerEvent.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerSocketProcessor.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerTransportProcessor.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/Controller.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/Conversation.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/EmptyEncoder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/FixedLengthEncoder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/QueryBuilder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/QueryCombiner.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCertificate.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCollector.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/RequestDispatcher.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/RequestEntity.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/RequestMessage.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/RequestReader.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseBuffer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEncoder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEntity.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseException.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseMessage.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseObserver.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/core/Timer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/ArrayConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/Body.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/BodyConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/BoundaryConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/BufferBody.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/BufferPart.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/ByteConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/ChunkedConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/ConsumerFactory.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/ContentConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/ContinueDispatcher.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyInputStream.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/Entity.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/EntityConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/FileUploadConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/FixedLengthConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/Header.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/HeaderConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/Message.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/MessageHeader.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartBodyConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartData.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryFactory.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartFactory.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartHeaderConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeries.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeriesConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/RequestConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/Segment.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/TokenConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/message/UpdateConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/AddressParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentDispositionParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentTypeParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/CookieParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/DateParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/LanguageParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/ListParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/PathParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/PrincipalParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/QueryParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/parse/ValueParser.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java create mode 100644 simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/ConnectionTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/CookieTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/Debug.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/MockRenegotiationServer.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/MockSocket.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/MockTrace.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/RenegotiationExample.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/StatusTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/StreamTransport.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/AccumulatorTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/ChunkedProducerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/Chunker.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/Client.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/Connector.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/ConversationTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/DribbleCursor.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/FixedConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/FixedProducerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MessageTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockChannel.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockController.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockEntity.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockObserver.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockPart.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockProxyRequest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockRequest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockResponse.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockSender.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/MockSocket.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/PayloadTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/ProducerExceptionTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/QueryBuilderTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorProcessorTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/RequestTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/Result.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/StopTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/StreamCursor.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/ThreadDumper.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/Ticket.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/TicketProcessor.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/TransferTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/core/WebSocketUpgradeTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/BoundaryConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/ChunkedConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/ContentConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/FileUploadConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/MessageHeaderTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/MockBody.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/MockHeader.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/MockSegment.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/PartConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/PartSeriesConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/ReplyConsumer.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/message/TokenConsumerTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/AddressParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentDispositionParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentTypeParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/CookieParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/DateParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/LanguageParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/ListParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/ParameterTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/PathParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/PriorityQueueTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/parse/QueryParserTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebFrameTypeTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketAnalyzer.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketCertificate.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatApplication.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatLogin.html create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.html create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoomListener.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketKeyTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketTestClient.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/service/PathRouterTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/service/WebSocketPerformanceTest.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTable.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableCell.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableChanger.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumn.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumnStyle.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableListener.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRow.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowAnnotator.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowChanger.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSchema.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSubscription.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSweeper.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdateType.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdater.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdaterApplication.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketValueEncoder.java create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.css create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.min.js create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/delta.js create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/failure.png create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/font-awesome.min.css create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/grid.html create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/index.html create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/jquery-2.1.1.min.js create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/login.html create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/main.html create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/pending.png create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/success.png create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/table.html create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.css create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.js create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.css create mode 100644 simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.js create mode 100644 simple/simple-transport/pom.xml create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/ByteCursor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/ByteReader.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/ByteWriter.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/Certificate.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/CertificateChallenge.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/Channel.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/FlushScheduler.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/FlushSignaller.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/Handshake.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/Negotiation.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/NegotiationState.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/OperationFactory.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/Phase.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/PhaseType.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SecureTransport.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/ServerCleaner.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/Socket.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBuffer.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferAppender.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferWriter.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SocketFlusher.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SocketProcessor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SocketTransport.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/SocketWrapper.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/Transport.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportChannel.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportCursor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportDispatcher.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportEvent.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportException.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportProcessor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportReader.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportSocketProcessor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/TransportWriter.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/Connection.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionEvent.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionException.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAcceptor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAnalyzer.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketConnection.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListener.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListenerManager.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketTrace.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Action.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionDistributor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSelector.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSet.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/CancelAction.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecuteAction.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecutorReactor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Latch.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Operation.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/OperationDistributor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/PartitionDistributor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Reactor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ReactorEvent.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/SynchronousReactor.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/trace/Trace.java create mode 100644 simple/simple-transport/src/main/java/org/simpleframework/transport/trace/TraceAnalyzer.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/MockSocket.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/ServerBuffer.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/SocketBufferTest.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportPipeTest.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportTest.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/StreamTransport.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/TransportCursorTest.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/TransportTest.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/reactor/DistributorTest.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/trace/CompareQueueTest.java create mode 100644 simple/simple-transport/src/test/java/org/simpleframework/transport/trace/MockTrace.java diff --git a/simple/pom.xml b/simple/pom.xml new file mode 100644 index 0000000..d731e35 --- /dev/null +++ b/simple/pom.xml @@ -0,0 +1,15 @@ + + 4.0.0 + org.simpleframework + simple + pom + 6.0.1 + Simple + + simple-common + simple-transport + simple-http + + + + diff --git a/simple/simple-common/pom.xml b/simple/simple-common/pom.xml new file mode 100644 index 0000000..e1a4b3a --- /dev/null +++ b/simple/simple-common/pom.xml @@ -0,0 +1,132 @@ + + + org.sonatype.oss + oss-parent + 7 + + 4.0.0 + org.simpleframework + simple-common + jar + 6.0.1 + Simple Common + http://www.simpleframework.org + Simple is a high performance asynchronous HTTP framework for Java + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + http://simpleweb.svn.sourceforge.net/viewvc/simpleweb.tags/simple-common-6.0.1 + scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-common-6.0.1 + scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-common-6.0.1 + + + + niallg + Niall Gallagher + niallg@users.sf.net + + + + UTF-8 + UTF-8 + UTF-8 + + + + junit + junit + 3.8.1 + test + + + + + + org.codehaus.mojo + cobertura-maven-plugin + + + + + + + org.apache.maven.wagon + wagon-ssh-external + 1.0-alpha-5 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + org.apache.maven.plugins + maven-source-plugin + + UTF-8 + UTF-8 + + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + + release-sign-artifacts + + + performRelease + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.1 + + + sign-artifacts + verify + + sign + + + + + + + + + + diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/KeyMap.java b/simple/simple-common/src/main/java/org/simpleframework/common/KeyMap.java new file mode 100644 index 0000000..6f450bb --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/KeyMap.java @@ -0,0 +1,93 @@ +/* + * KeyMap.java May 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + +/** + * The KeyMap object is used to represent a map of values + * keyed using a known string. This also ensures that the keys and + * the values added to this hash map can be acquired in an independent + * list of values, ensuring that modifications to the map do not have + * an impact on the lists provided, and vice versa. The key map can + * also be used in a fore each look using the string keys. + * + * @author Niall Gallagher + */ +public class KeyMap extends LinkedHashMap implements Iterable { + + /** + * Constructor for the KeyMap object. This creates + * a hash map that can expose the keys and values of the map as + * an independent List containing the values. This + * can also be used within a for loop for convenience. + */ + public KeyMap() { + super(); + } + + /** + * This is used to produce an Iterator of values + * that can be used to acquire the contents of the key map within + * a for each loop. The key map can be modified while it is been + * iterated as the iterator is an independent list of values. + * + * @return this returns an iterator of the keys in the map + */ + public Iterator iterator() { + return getKeys().iterator(); + } + + /** + * This is used to produce a List of the keys in + * the map. The list produced is a copy of the internal keys and + * so can be modified and used without affecting this map object. + * + * @return this returns an independent list of the key values + */ + public List getKeys() { + Set keys = keySet(); + + if(keys == null) { + return new ArrayList(); + } + return new ArrayList(keys); + } + + /** + * This is used to produce a List of the values in + * the map. The list produced is a copy of the internal values and + * so can be modified and used without affecting this map object. + * + * @return this returns an independent list of the values + */ + public List getValues() { + Collection values = values(); + + if(values == null) { + return new ArrayList(); + } + return new ArrayList(values); + } + } \ No newline at end of file diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/Allocator.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/Allocator.java new file mode 100644 index 0000000..aa20b76 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/Allocator.java @@ -0,0 +1,55 @@ +/* + * Allocator.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.IOException; + +/** + * The Allocator interface is used to describe a resource + * that can allocate a buffer. This is used so that memory allocation + * can be implemented as a strategy allowing many different sources of + * memory. Typically memory will be allocated as an array of bytes but + * can be a mapped region of shared memory or a file. + * + * @author Niall Gallagher + */ +public interface Allocator { + + /** + * This method is used to allocate a default buffer. Typically this + * will allocate a buffer of predetermined size, allowing it to + * grow to an upper limit to accommodate extra data. If the buffer + * can not be allocated for some reason this throws an exception. + * + * @return this returns an allocated buffer with a default size + */ + Buffer allocate() throws IOException; + + /** + * This method is used to allocate a default buffer. This is used + * to allocate a buffer of the specified size, allowing it to + * grow to an upper limit to accommodate extra data. If the buffer + * can not be allocated for some reason this throws an exception. + * + * @param size this is the initial capacity the buffer should have + * + * @return this returns an allocated buffer with a specified size + */ + Buffer allocate(long size) throws IOException; +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayAllocator.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayAllocator.java new file mode 100644 index 0000000..e2dbb3c --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayAllocator.java @@ -0,0 +1,111 @@ +/* + * ArrayAllocator.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.IOException; + +/** + * The ArrayAllocator object is used to provide a means + * to allocate buffers using a single byte array. This essentially uses + * the heap to allocate all buffers. As a result the performance of the + * resulting buffers is good, however for very large buffers this will + * use quote allot of the usable heap space. For very large buffers a + * mapped region of shared memory of a file should be considered. + * + * @author Niall Gallagher + */ +public class ArrayAllocator implements Allocator { + + /** + * This represents the largest portion of memory that is allowed. + */ + private int limit; + + /** + * This represents the default capacity of all allocated buffers. + */ + private int size; + + /** + * Constructor for the ArrayAllocator object. This is + * used to instantiate the allocator with a default buffer size of + * half a kilobyte. This ensures that it can be used for general + * purpose byte storage and for minor I/O tasks. + */ + public ArrayAllocator() { + this(512); + } + + /** + * Constructor for the ArrayAllocator object. This is + * used to instantiate the allocator with a specified buffer size. + * This is typically used when a very specific buffer capacity is + * required, for example a request body with a known length. + * + * @param size the initial capacity of the allocated buffers + */ + public ArrayAllocator(int size) { + this(size, 1048576); + } + + /** + * Constructor for the ArrayAllocator object. This is + * used to instantiate the allocator with a specified buffer size. + * This is typically used when a very specific buffer capacity is + * required, for example a request body with a known length. + * + * @param size the initial capacity of the allocated buffers + * @param limit this is the maximum buffer size created by this + */ + public ArrayAllocator(int size, int limit) { + this.limit = Math.max(size, limit); + this.size = size; + } + + /** + * This method is used to allocate a default buffer. This will + * allocate a buffer of predetermined size, allowing it to grow + * to an upper limit to accommodate extra data. If the buffer + * requested is larger than the limit an exception is thrown. + * + * @return this returns an allocated buffer with a default size + */ + public Buffer allocate() throws IOException { + return allocate(size); + } + + /** + * This method is used to allocate a default buffer. This will + * allocate a buffer of predetermined size, allowing it to grow + * to an upper limit to accommodate extra data. If the buffer + * requested is larger than the limit an exception is thrown. + * + * @param size the initial capacity of the allocated buffer + * + * @return this returns an allocated buffer with a default size + */ + public Buffer allocate(long size) throws IOException { + int required = (int)size; + + if(size > limit) { + throw new BufferException("Specified size %s beyond limit", size); + } + return new ArrayBuffer(required, limit); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayBuffer.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayBuffer.java new file mode 100644 index 0000000..972ad92 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayBuffer.java @@ -0,0 +1,397 @@ +/* + * ArrayBuffer.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * The ArrayBuffer is intended to be a general purpose + * byte buffer that stores bytes in an single internal byte array. The + * intended use of this buffer is to provide a simple buffer object to + * read and write bytes with. In particular this provides a high + * performance buffer that can be used to read and write bytes fast. + *

+ * This provides several convenience methods which make the use of the + * buffer easy and useful. This buffer allows an initial capacity to be + * specified however if there is a need for extra space to be added to + * buffer then the append methods will expand the capacity + * of the buffer as needed. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.buffer.ArrayAllocator + */ +public class ArrayBuffer implements Buffer { + + /** + * This is the internal array used to store the buffered bytes. + */ + private byte[] buffer; + + /** + * This is used to determine whether this buffer has been closed. + */ + private boolean closed; + + /** + * This is the count of the number of bytes buffered. + */ + private int count; + + /** + * This is the maximum allowable buffer capacity for this. + */ + private int limit; + + /** + * Constructor for the ArrayBuffer object. The initial + * capacity of the default buffer object is set to 16, the capacity + * will be expanded when the append methods are used and there is + * not enough space to accommodate the extra bytes. + */ + public ArrayBuffer() { + this(16); + } + + /** + * Constructor for the ArrayBuffer object. The initial + * capacity of the buffer object is set to given size, the capacity + * will be expanded when the append methods are used and there is + * not enough space to accommodate the extra bytes. + * + * @param size the initial capacity of this buffer instance + */ + public ArrayBuffer(int size) { + this(size, size); + } + + /** + * Constructor for the ArrayBuffer object. The initial + * capacity of the buffer object is set to given size, the capacity + * will be expanded when the append methods are used and there is + * not enough space to accommodate the extra bytes. + * + * @param size the initial capacity of this buffer instance + * @param limit this is the maximum allowable buffer capacity + */ + public ArrayBuffer(int size, int limit) { + this.buffer = new byte[size]; + this.limit = limit; + } + + /** + * This method is used so that the buffer can be represented as a + * stream of bytes. This provides a quick means to access the data + * that has been written to the buffer. It wraps the buffer within + * an input stream so that it can be read directly. + * + * @return a stream that can be used to read the buffered bytes + */ + public InputStream open() { + return new ByteArrayInputStream(buffer, 0, count); + } + + /** + * This method is used to allocate a segment of this buffer as a + * separate buffer object. This allows the buffer to be sliced in + * to several smaller independent buffers, while still allowing the + * parent buffer to manage a single buffer. This is useful if the + * parent is split in to logically smaller segments. + * + * @return this returns a buffer which is a segment of this buffer + */ + public Buffer allocate() throws IOException { + return new Segment(this,count); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. If the UTF-8 + * content encoding is not supported the platform default is + * used, however this is unlikely as UTF-8 should be supported. + * + * @return this returns a UTF-8 encoding of the buffer contents + */ + public String encode() throws IOException { + return encode("UTF-8"); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. This will convert + * the bytes using the specified character encoding format. + * + * @return this returns the encoding of the buffer contents + */ + public String encode(String charset) throws IOException { + return new String(buffer,0,count, charset); + } + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + * + * @return this returns this buffer for another operation + */ + public Buffer append(byte[] array) throws IOException { + return append(array, 0, array.length); + } + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + * @param off this is the offset to begin reading the bytes from + * @param size the number of bytes to be read from the array + * + * @return this returns this buffer for another operation + */ + public Buffer append(byte[] array, int off, int size) throws IOException { + if(closed) { + throw new BufferException("Buffer is closed"); + } + if(size + count > buffer.length) { + expand(count + size); + } + if(size > 0) { + System.arraycopy(array, off, buffer, count, size); + count += size; + } + return this; + } + + /** + * This is used to ensure that there is enough space in the buffer + * to allow for more bytes to be added. If the buffer is already + * larger than the required capacity the this will do nothing. + * + * @param capacity the minimum size needed for this buffer object + */ + private void expand(int capacity) throws IOException { + if(capacity > limit) { + throw new BufferException("Capacity limit %s exceeded", limit); + } + int resize = buffer.length * 2; + int size = Math.max(capacity, resize); + byte[] temp = new byte[size]; + + System.arraycopy(buffer, 0, temp, 0, count); + buffer = temp; + } + + /** + * This will clear all data from the buffer. This simply sets the + * count to be zero, it will not clear the memory occupied by the + * instance as the internal buffer will remain. This allows the + * memory occupied to be reused as many times as is required. + */ + public void clear() throws IOException { + if(closed) { + throw new BufferException("Buffer is closed"); + } + count = 0; + } + + /** + * This method is used to ensure the buffer can be closed. Once + * the buffer is closed it is an immutable collection of bytes and + * can not longer be modified. This ensures that it can be passed + * by value without the risk of modification of the bytes. + */ + public void close() throws IOException { + closed = true; + } + + /** + * This is used to provide the number of bytes that have been + * written to the buffer. This increases as bytes are appended + * to the buffer. if the buffer is cleared this resets to zero. + * + * @return this returns the number of bytes within the buffer + */ + public long length() { + return count; + } + + /** + * A Segment represents a segment within a buffer. It + * is used to allow a buffer to be split in to several logical parts + * without the need to create several separate buffers. This means + * that the buffer can be represented in a single memory space, as + * both a single large buffer and as several individual buffers. + */ + private class Segment implements Buffer { + + /** + * This is the parent buffer which is used for collecting data. + */ + private Buffer parent; + + /** + * This is used to determine if the buffer has closed or not. + */ + private boolean closed; + + /** + * This represents the start of the segment within the buffer. + */ + private int start; + + /** + * This represents the number of bytes this segment contains. + */ + private int length; + + /** + * Constructor for the Segment object. This is used + * to create a buffer within a buffer. A segment is a region of + * bytes within the original buffer. It allows the buffer to be + * split in to several logical parts of a single buffer. + * + * @param parent this is the parent buffer used to append to + * @param start this is the start within the buffer to read + */ + public Segment(Buffer parent, int start) { + this.parent = parent; + this.start = start; + } + + /** + * This method is used so that the buffer can be represented as a + * stream of bytes. This provides a quick means to access the data + * that has been written to the buffer. It wraps the buffer within + * an input stream so that it can be read directly. + * + * @return a stream that can be used to read the buffered bytes + */ + public InputStream open() throws IOException { + return new ByteArrayInputStream(buffer,start,length); + } + + /** + * This method is used to allocate a segment of this buffer as a + * separate buffer object. This allows the buffer to be sliced in + * to several smaller independent buffers, while still allowing the + * parent buffer to manage a single buffer. This is useful if the + * parent is split in to logically smaller segments. + * + * @return this returns a buffer which is a segment of this buffer + */ + public Buffer allocate() throws IOException { + return new Segment(this,count); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. If the UTF-8 + * content encoding is not supported the platform default is + * used, however this is unlikely as UTF-8 should be supported. + * + * @return this returns a UTF-8 encoding of the buffer contents + */ + public String encode() throws IOException { + return encode("UTF-8"); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. This will convert + * the bytes using the specified character encoding format. + * + * @return this returns the encoding of the buffer contents + */ + public String encode(String charset) throws IOException { + return new String(buffer,start,length, charset); + } + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + */ + public Buffer append(byte[] array) throws IOException { + return append(array, 0, array.length); + } + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + * @param off this is the offset to begin reading the bytes from + * @param size the number of bytes to be read from the array + */ + public Buffer append(byte[] array, int off, int size) throws IOException { + if(closed) { + throw new BufferException("Buffer is closed"); + } + if(size > 0) { + parent.append(array, off, size); + length += size; + } + return this; + } + + /** + * This will clear all data from the buffer. This simply sets the + * count to be zero, it will not clear the memory occupied by the + * instance as the internal buffer will remain. This allows the + * memory occupied to be reused as many times as is required. + */ + public void clear() throws IOException { + length = 0; + } + + /** + * This method is used to ensure the buffer can be closed. Once + * the buffer is closed it is an immutable collection of bytes and + * can not longer be modified. This ensures that it can be passed + * by value without the risk of modification of the bytes. + */ + public void close() throws IOException { + closed = true; + } + + /** + * This is used to provide the number of bytes that have been + * written to the buffer. This increases as bytes are appended + * to the buffer. if the buffer is cleared this resets to zero. + * + * @return this returns the number of bytes within the buffer + */ + public long length() { + return length; + } + } +} + diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/Buffer.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/Buffer.java new file mode 100644 index 0000000..ebde280 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/Buffer.java @@ -0,0 +1,129 @@ +/* + * Buffer.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.IOException; +import java.io.InputStream; + +/** + * The Buffer interface represents a collection of bytes + * that can be written to and later read. This is used to provide a + * region of memory is such a way that the underlying representation + * of that memory is independent of its use. Typically buffers are + * implemented as either allocated byte arrays or files. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.buffer.Allocator + */ +public interface Buffer { + + /** + * This method is used to allocate a segment of this buffer as a + * separate buffer object. This allows the buffer to be sliced in + * to several smaller independent buffers, while still allowing the + * parent buffer to manage a single buffer. This is useful if the + * parent is split in to logically smaller segments. + * + * @return this returns a buffer which is a segment of this buffer + */ + Buffer allocate() throws IOException; + + /** + * This method is used so that a buffer can be represented as a + * stream of bytes. This provides a quick means to access the data + * that has been written to the buffer. It wraps the buffer within + * an input stream so that it can be read directly. + * + * @return a stream that can be used to read the buffered bytes + */ + InputStream open() throws IOException; + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. If the UTF-8 + * content encoding is not supported the platform default is + * used, however this is unlikely as UTF-8 should be supported. + * + * @return this returns a UTF-8 encoding of the buffer contents + */ + String encode() throws IOException; + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. This will convert + * the bytes using the specified character encoding format. + * + * @param charset this is the charset to encode the data with + * + * @return this returns the encoding of the buffer contents + */ + String encode(String charset) throws IOException; + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + * + * @return this returns this buffer for another operation + */ + Buffer append(byte[] array) throws IOException; + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + * @param len the number of bytes to be read from the array + * @param off this is the offset to begin reading the bytes from + * + * @return this returns this buffer for another operation + */ + Buffer append(byte[] array, int off, int len) throws IOException; + + /** + * This will clear all data from the buffer. This simply sets the + * count to be zero, it will not clear the memory occupied by the + * instance as the internal buffer will remain. This allows the + * memory occupied to be reused as many times as is required. + */ + void clear() throws IOException; + + /** + * This method is used to ensure the buffer can be closed. Once + * the buffer is closed it is an immutable collection of bytes and + * can not longer be modified. This ensures that it can be passed + * by value without the risk of modification of the bytes. + */ + void close() throws IOException; + + /** + * This is used to provide the number of bytes that have been + * written to the buffer. This increases as bytes are appended + * to the buffer. if the buffer is cleared this resets to zero. + * + * @return this returns the number of bytes within the buffer + */ + long length(); +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferAllocator.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferAllocator.java new file mode 100644 index 0000000..033ba3a --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferAllocator.java @@ -0,0 +1,229 @@ +/* + * BufferAllocator.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.IOException; +import java.io.InputStream; + +/** + * The BufferAllocator object is used to provide a means + * to allocate buffers using a single underlying buffer. This uses a + * buffer from a existing allocator to create the region of memory to + * use to allocate all other buffers. As a result this allows a single + * buffer to acquire the bytes in a number of associated buffers. This + * has the advantage of allowing bytes to be read in sequence without + * joining data from other buffers or allocating multiple regions. + * + * @author Niall Gallagher + */ +public class BufferAllocator extends FilterAllocator implements Buffer { + + /** + * This is the underlying buffer all other buffers are within. + */ + private Buffer buffer; + + /** + * Constructor for the BufferAllocator object. This is + * used to instantiate the allocator with a default buffer size of + * half a kilobyte. This ensures that it can be used for general + * purpose byte storage and for minor I/O tasks. + * + * @param source this is where the underlying buffer is allocated + */ + public BufferAllocator(Allocator source) { + super(source); + } + + /** + * Constructor for the BufferAllocator object. This is + * used to instantiate the allocator with a specified buffer size. + * This is typically used when a very specific buffer capacity is + * required, for example a request body with a known length. + * + * @param source this is where the underlying buffer is allocated + * @param capacity the initial capacity of the allocated buffers + */ + public BufferAllocator(Allocator source, long capacity) { + super(source, capacity); + } + + /** + * Constructor for the BufferAllocator object. This is + * used to instantiate the allocator with a specified buffer size. + * This is typically used when a very specific buffer capacity is + * required, for example a request body with a known length. + * + * @param source this is where the underlying buffer is allocated + * @param capacity the initial capacity of the allocated buffers + * @param limit this is the maximum buffer size created by this + */ + public BufferAllocator(Allocator source, long capacity, long limit) { + super(source, capacity, limit); + } + + /** + * This method is used so that a buffer can be represented as a + * stream of bytes. This provides a quick means to access the data + * that has been written to the buffer. It wraps the buffer within + * an input stream so that it can be read directly. + * + * @return a stream that can be used to read the buffered bytes + */ + public InputStream open() throws IOException { + if(buffer == null) { + allocate(); + } + return buffer.open(); + } + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. If the UTF-8 + * content encoding is not supported the platform default is + * used, however this is unlikely as UTF-8 should be supported. + * + * @return this returns a UTF-8 encoding of the buffer contents + */ + public String encode() throws IOException { + if(buffer == null) { + allocate(); + } + return buffer.encode(); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. This will convert + * the bytes using the specified character encoding format. + * + * @return this returns the encoding of the buffer contents + */ + public String encode(String charset) throws IOException { + if(buffer == null) { + allocate(); + } + return buffer.encode(charset); + } + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + * + * @return this returns this buffer for another operation + */ + public Buffer append(byte[] array) throws IOException { + return append(array, 0, array.length); + } + + /** + * This method is used to append bytes to the end of the buffer. + * This will expand the capacity of the buffer if there is not + * enough space to accommodate the extra bytes. + * + * @param array this is the byte array to append to this buffer + * @param size the number of bytes to be read from the array + * @param off this is the offset to begin reading the bytes from + * + * @return this returns this buffer for another operation + */ + public Buffer append(byte[] array, int off, int size) throws IOException { + if(buffer == null) { + allocate(size); + } + return buffer.append(array, off, size); + } + + /** + * This will clear all data from the buffer. This simply sets the + * count to be zero, it will not clear the memory occupied by the + * instance as the internal buffer will remain. This allows the + * memory occupied to be reused as many times as is required. + */ + public void clear() throws IOException { + if(buffer != null) { + buffer.clear(); + } + } + + /** + * This method is used to ensure the buffer can be closed. Once + * the buffer is closed it is an immutable collection of bytes and + * can not longer be modified. This ensures that it can be passed + * by value without the risk of modification of the bytes. + */ + public void close() throws IOException { + if(buffer == null) { + allocate(); + } + buffer.close(); + } + + /** + * This method is used to allocate a default buffer. This will + * allocate a buffer of predetermined size, allowing it to grow + * to an upper limit to accommodate extra data. If the buffer + * requested is larger than the limit an exception is thrown. + * + * @return this returns an allocated buffer with a default size + */ + @Override + public Buffer allocate() throws IOException { + return allocate(capacity); + } + + /** + * This method is used to allocate a default buffer. This will + * allocate a buffer of predetermined size, allowing it to grow + * to an upper limit to accommodate extra data. If the buffer + * requested is larger than the limit an exception is thrown. + * + * @param size the initial capacity of the allocated buffer + * + * @return this returns an allocated buffer with a default size + */ + @Override + public Buffer allocate(long size) throws IOException { + if(size > limit) { + throw new BufferException("Specified size %s beyond limit", size); + } + if(capacity > size) { // lazily create backing buffer + size = capacity; + } + if(buffer == null) { + buffer = source.allocate(size); + } + return buffer.allocate(); + } + + /** + * This is used to provide the number of bytes that have been + * written to the buffer. This increases as bytes are appended + * to the buffer. if the buffer is cleared this resets to zero. + * + * @return this returns the number of bytes within the buffer + */ + public long length() { + return buffer.length(); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferException.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferException.java new file mode 100644 index 0000000..4ec2019 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferException.java @@ -0,0 +1,43 @@ +/* + * BufferException.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.IOException; + +/** + * The BufferException is used to report problems that + * can occur during the use or allocation of a buffer. Typically + * this is thrown if the upper capacity limit is exceeded. + * + * @author Niall Gallagher + */ +public class BufferException extends IOException { + + /** + * Constructor for the BufferException object. The + * exception can be provided with a message describing the issue + * that has arisen in the use or allocation of the buffer. + * + * @param format this is the template for the exception + * @param values these are the values to be added to the template + */ + public BufferException(String format, Object... values) { + super(String.format(format, values)); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileAllocator.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileAllocator.java new file mode 100644 index 0000000..c91b1dc --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileAllocator.java @@ -0,0 +1,137 @@ +/* + * FileAllocator.java February 2008 + * + * Copyright (C) 2008, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.File; +import java.io.IOException; + +/** + * The FileAllocator object is used to create buffers + * that can be written to the file system. This creates buffers as + * files if they are larger than the specified limit. This ensures + * that buffers of arbitrary large size can be created. All buffer + * sizes under the limit are created using byte arrays allocated + * on the executing VM heap. This ensures that optimal performance + * is maintained for buffers of reasonable size. + * + * @author Niall Gallagher + */ +public class FileAllocator implements Allocator { + + /** + * This is the default prefix used when none has been specified. + */ + private static final String PREFIX = "temp"; + + /** + * This is the file manager used to create the buffer files. + */ + private FileWatcher manager; + + /** + * This is the limit up to which buffers are allocated in memory. + */ + private int limit; + + /** + * Constructor for the FileAllocator object. This is + * used to create buffers in memory up to a threshold size. If a + * buffer is required over the threshold size then the data is + * written to a file, where it can be retrieved at a later point. + */ + public FileAllocator() { + this(1048576); + } + + /** + * Constructor for the FileAllocator object. This is + * used to create buffers in memory up to a threshold size. If a + * buffer is required over the threshold size then the data is + * written to a file, where it can be retrieved at a later point. + * + * @param limit this is the maximum size for a heap buffer + */ + public FileAllocator(int limit) { + this(PREFIX, limit); + } + + /** + * Constructor for the FileAllocator object. This is + * used to create buffers in memory up to a threshold size. If a + * buffer is required over the threshold size then the data is + * written to a file, where it can be retrieved at a later point. + * + * @param prefix this is the file prefix for the file buffers + */ + public FileAllocator(String prefix) { + this(prefix, 1048576); + } + + /** + * Constructor for the FileAllocator object. This is + * used to create buffers in memory up to a threshold size. If a + * buffer is required over the threshold size then the data is + * written to a file, where it can be retrieved at a later point. + * + * @param prefix this is the file prefix for the file buffers + * @param limit this is the maximum size for a heap buffer + */ + public FileAllocator(String prefix, int limit) { + this.manager = new FileWatcher(prefix); + this.limit = limit; + } + + /** + * This will allocate a file buffer which will write data for the + * buffer to a file. Buffers allocated by this method can be of + * arbitrary size as data is appended directly to a temporary + * file. This ensures there is no upper limit for appended data. + * + * @return a buffer which will write to a temporary file + */ + public Buffer allocate() throws IOException { + File file = manager.create(); + + if(!file.exists()) { + throw new BufferException("Could not create file %s", file); + } + return new FileBuffer(file); + } + + /** + * This will allocate a file buffer which will write data for the + * buffer to a file. Buffers allocated by this method can be of + * arbitrary size as data is appended directly to a temporary + * file. This ensures there is no upper limit for appended data. + * If the size required is less than the limit then the buffer + * is an in memory array which provides optimal performance. + * + * @param size this is the size of the buffer to be created + * + * @return a buffer which will write to a created temporary file + */ + public Buffer allocate(long size) throws IOException { + int required = (int)size; + + if(size <= limit) { + return new ArrayBuffer(required); + } + return allocate(); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileBuffer.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileBuffer.java new file mode 100644 index 0000000..8fcea77 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileBuffer.java @@ -0,0 +1,622 @@ +/* + * FileBuffer.java February 2008 + * + * Copyright (C) 2008, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The FileBuffer object is used to create a buffer + * which will write the appended data to an underlying file. This + * is typically used for buffers that are too large for to allocate + * in memory. Data appended to the buffer can be retrieved at a + * later stage by acquiring the InputStream for the + * underlying file. To ensure that excessive file system space is + * not occupied the buffer files are cleaned every five minutes. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.buffer.FileAllocator + */ +class FileBuffer implements Buffer { + + /** + * This is the file output stream used for this buffer object. + */ + private OutputStream buffer; + + /** + * This represents the last file segment that has been created. + */ + private Segment segment; + + /** + * This is the path for the file that this buffer appends to. + */ + private File file; + + /** + * This is the number of bytes currently appended to the buffer. + */ + private long count; + + /** + * This is used to determine if this buffer has been closed. + */ + private boolean closed; + + /** + * Constructor for the FileBuffer object. This will + * create a buffer using the provided file. All data appended to + * this buffer will effectively written to the underlying file. + * If the appended data needs to be retrieved at a later stage + * then it can be acquired using the buffers input stream. + * + * @param file this is the file used for the file buffer + */ + public FileBuffer(File file) throws IOException { + this.buffer = new FileOutputStream(file); + this.file = file; + } + + /** + * This is used to allocate a segment within this buffer. If the + * buffer is closed this will throw an exception, if however the + * buffer is still open then a segment is created which will + * write all appended data to this buffer. However it can be + * treated as an independent source of data. + * + * @return this returns a buffer which is a segment of this + */ + public Buffer allocate() throws IOException { + if(closed) { + throw new BufferException("Buffer has been closed"); + } + if(segment != null) { + segment.close(); + } + if(!closed) { + segment = new Segment(this, count); + } + return segment; + } + + /** + * This is used to append the specified data to the underlying + * file. All bytes appended to the file can be consumed at a + * later stage by acquiring the InputStream from + * this buffer. Also if require the data can be encoded as a + * string object in a required character set. + * + * @param array this is the array to write the the file + * + * @return this returns this buffer for further operations + */ + public Buffer append(byte[] array) throws IOException { + return append(array, 0, array.length); + } + + /** + * This is used to append the specified data to the underlying + * file. All bytes appended to the file can be consumed at a + * later stage by acquiring the InputStream from + * this buffer. Also if require the data can be encoded as a + * string object in a required character set. + * + * @param array this is the array to write the the file + * @param off this is the offset within the array to write + * @param size this is the number of bytes to be appended + * + * @return this returns this buffer for further operations + */ + public Buffer append(byte[] array, int off, int size) throws IOException { + if(closed) { + throw new BufferException("Buffer has been closed"); + } + if(size > 0) { + buffer.write(array, off, size); + count += size; + } + return this; + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. If the UTF-8 + * content encoding is not supported the platform default is + * used, however this is unlikely as UTF-8 should be supported. + * + * @return this returns a UTF-8 encoding of the buffer contents + */ + public String encode() throws IOException { + return encode("UTF-8"); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. This will convert + * the bytes using the specified character encoding format. + * + * @param charset this is the charset to encode the data with + * + * @return this returns the encoding of the buffer contents + */ + public String encode(String charset) throws IOException { + InputStream source = open(); + int size = (int)count; + + if(count <= 0) { + return new String(); + } + return convert(source, charset, size); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. This will convert + * the bytes using the specified character encoding format. + * + * @param source this is the source stream that is to be encoded + * @param charset this is the charset to encode the data with + * @param count this is the number of bytes to be encoded + * + * @return this returns the encoding of the buffer contents + */ + private String convert(InputStream source, String charset, int count) throws IOException { + byte[] buffer = new byte[count]; + int left = count; + + while(left > 0) { + int size = source.read(buffer, 0, left); + + if(size == -1) { + throw new BufferException("Could not read buffer"); + } + left -= count; + } + return new String(buffer, charset); + } + + /** + * This method is used so that a buffer can be represented as a + * stream of bytes. This provides a quick means to access the data + * that has been written to the buffer. It wraps the buffer within + * an input stream so that it can be read directly. + * + * @return a stream that can be used to read the buffered bytes + */ + public InputStream open() throws IOException { + if(!closed) { + close(); + } + return open(file); + } + + /** + * This method is used so that a buffer can be represented as a + * stream of bytes. This provides a quick means to access the data + * that has been written to the buffer. It wraps the buffer within + * an input stream so that it can be read directly. + * + * @param file this is the file used to create the input stream + * + * @return a stream that can be used to read the buffered bytes + */ + private InputStream open(File file) throws IOException { + InputStream source = new FileInputStream(file); + + if(count <= 0) { + source.close(); // release file descriptor + } + return new Range(source, count); + } + + /** + * This will clear all data from the buffer. This simply sets the + * count to be zero, it will not clear the memory occupied by the + * instance as the internal buffer will remain. This allows the + * memory occupied to be reused as many times as is required. + */ + public void clear() throws IOException { + if(closed) { + throw new BufferException("Buffer has been closed"); + } + } + + /** + * This method is used to ensure the buffer can be closed. Once + * the buffer is closed it is an immutable collection of bytes and + * can not longer be modified. This ensures that it can be passed + * by value without the risk of modification of the bytes. + */ + public void close() throws IOException { + if(!closed) { + buffer.close(); + closed = true; + } + if(segment != null) { + segment.close(); + } + } + + /** + * This is used to provide the number of bytes that have been + * written to the buffer. This increases as bytes are appended + * to the buffer. if the buffer is cleared this resets to zero. + * + * @return this returns the number of bytes within the buffer + */ + public long length() { + return count; + } + + /** + * The Segment object is used to create a segment of + * the parent buffer. The segment will write to the parent however + * if can be read as a unique range of bytes starting with the + * first sequence of bytes appended to the segment. A segment can + * be used to create a collection of buffers backed by the same + * underlying file, as is require with multipart uploads. + */ + private class Segment implements Buffer { + + /** + * This is an internal segment created from this buffer object. + */ + private Segment segment; + + /** + * This is the parent buffer that bytes are to be appended to. + */ + private Buffer parent; + + /** + * This is the offset of the first byte within the sequence. + */ + private long first; + + /** + * This is the last byte within the segment for this segment. + */ + private long last; + + /** + * This determines if the segment is currently open or closed. + */ + private boolean closed; + + /** + * Constructor for the Segment object. This is used + * to create a segment from a parent buffer. A segment is a part + * of the parent buffer and appends its bytes to the parent. It + * can however be treated as an independent source of bytes. + * + * @param parent this is the parent buffer to be appended to + * @param first this is the offset for the first byte in this + */ + public Segment(Buffer parent, long first) { + this.parent = parent; + this.first = first; + this.last = first; + } + + /** + * This is used to allocate a segment within this buffer. If the + * buffer is closed this will throw an exception, if however the + * buffer is still open then a segment is created which will + * write all appended data to this buffer. However it can be + * treated as an independent source of data. + * + * @return this returns a buffer which is a segment of this + */ + public Buffer allocate() throws IOException { + if(closed) { + throw new BufferException("Buffer has been closed"); + } + if(segment != null) { + segment.close(); + } + if(!closed) { + segment = new Segment(this, last); + } + return segment; + } + + /** + * This is used to append the specified data to the underlying + * file. All bytes appended to the file can be consumed at a + * later stage by acquiring the InputStream from + * this buffer. Also if require the data can be encoded as a + * string object in a required character set. + * + * @param array this is the array to write the the file + * + * @return this returns this buffer for further operations + */ + public Buffer append(byte[] array) throws IOException { + return append(array, 0, array.length); + } + + /** + * This is used to append the specified data to the underlying + * file. All bytes appended to the file can be consumed at a + * later stage by acquiring the InputStream from + * this buffer. Also if require the data can be encoded as a + * string object in a required character set. + * + * @param array this is the array to write the the file + * @param off this is the offset within the array to write + * @param size this is the number of bytes to be appended + * + * @return this returns this buffer for further operations + */ + public Buffer append(byte[] array, int off, int size) throws IOException { + if(closed) { + throw new BufferException("Buffer has been closed"); + } + if(size > 0) { + parent.append(array, off, size); + last += size; + } + return this; + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. If the UTF-8 + * content encoding is not supported the platform default is + * used, however this is unlikely as UTF-8 should be supported. + * + * @return this returns a UTF-8 encoding of the buffer contents + */ + public String encode() throws IOException { + return encode("UTF-8"); + } + + /** + * This method is used to acquire the buffered bytes as a string. + * This is useful if the contents need to be manipulated as a + * string or transferred into another encoding. This will convert + * the bytes using the specified character encoding format. + * + * @param charset this is the charset to encode the data with + * + * @return this returns the encoding of the buffer contents + */ + public String encode(String charset) throws IOException { + InputStream source = open(); + long count = last - first; + int size = (int)count; + + if(count <= 0) { + return new String(); + } + return convert(source, charset, size); + } + + /** + * This method is used so that a buffer can be represented as a + * stream of bytes. This provides a quick means to access the data + * that has been written to the buffer. It wraps the buffer within + * an input stream so that it can be read directly. + * + * @return a stream that can be used to read the buffered bytes + */ + public InputStream open() throws IOException { + InputStream source = new FileInputStream(file); + long length = last - first; + + if(first > 0) { + source.skip(first); + } + return new Range(source, length); + } + + /** + * This will clear all data from the buffer. This simply sets the + * count to be zero, it will not clear the memory occupied by the + * instance as the internal buffer will remain. This allows the + * memory occupied to be reused as many times as is required. + */ + public void clear() throws IOException { + if(closed) { + throw new BufferException("Buffer is closed"); + } + } + + /** + * This method is used to ensure the buffer can be closed. Once + * the buffer is closed it is an immutable collection of bytes and + * can not longer be modified. This ensures that it can be passed + * by value without the risk of modification of the bytes. + */ + public void close() throws IOException { + if(!closed) { + closed = true; + } + if(segment != null) { + segment.close(); + } + } + + /** + * This determines how much space is left in the buffer. If there + * is no limit to the buffer size this will return the maximum + * long value. Typically this is the capacity minus the length. + * + * @return this is the space that is available within the buffer + */ + public long space() { + return Long.MAX_VALUE; + } + + /** + * This is used to provide the number of bytes that have been + * written to the buffer. This increases as bytes are appended + * to the buffer. if the buffer is cleared this resets to zero. + * + * @return this returns the number of bytes within the buffer + */ + public long length() { + return last - first; + } + + } + + /** + * The Range object is used to provide a stream that + * can read a range of bytes from a provided input stream. This + * allows buffer segments to be allocated from the main buffer. + * Providing a range in this manner ensures that only one backing + * file is needed for the primary buffer allocated. + */ + private class Range extends FilterInputStream { + + /** + * This is the length of the bytes that exist in the range. + */ + private long length; + + /** + * This is used to close the stream once it has been read. + */ + private boolean closed; + + /** + * Constructor for the Range object. This ensures + * that only a limited number of bytes can be consumed from a + * backing input stream giving the impression of an independent + * stream of bytes for a segmented region of the parent buffer. + * + * @param source this is the input stream used to read data + * @param length this is the number of bytes that can be read + */ + public Range(InputStream source, long length) { + super(source); + this.length = length; + } + + /** + * This will read data from the underlying stream up to the + * number of bytes this range is allowed to read. When all of + * the bytes are exhausted within the stream this returns -1. + * + * @return this returns the octet from the underlying stream + */ + @Override + public int read() throws IOException { + if(length-- > 0) { + return in.read(); + } + if(length <= 0) { + close(); + } + return -1; + } + + /** + * This will read data from the underlying stream up to the + * number of bytes this range is allowed to read. When all of + * the bytes are exhausted within the stream this returns -1. + * + * @param array this is the array to read the bytes in to + * @param off this is the start offset to append the bytes to + * @param size this is the number of bytes that are required + * + * @return this returns the number of bytes that were read + */ + @Override + public int read(byte[] array, int off, int size) throws IOException { + int left = (int)Math.min(length, size); + + if(left > 0) { + int count = in.read(array, off, left); + + if(count > 0){ + length -= count; + } + if(length <= 0) { + close(); + } + return count; + } + return -1; + } + + /** + * This returns the number of bytes that can be read from the + * range. This will be the actual number of bytes the range + * contains as the underlying file will not block reading. + * + * @return this returns the number of bytes within the range + */ + @Override + public int available() throws IOException { + return (int)length; + } + + /** + * This is the number of bytes to skip from the buffer. This + * will allow up to the number of remaining bytes within the + * range to be read. When all the bytes have been read this + * will return zero indicating no bytes were skipped. + * + * @param size this returns the number of bytes to skip + * + * @return this returns the number of bytes that were skipped + */ + @Override + public long skip(long size) throws IOException { + long left = Math.min(length, size); + long skip = in.skip(left); + + if(skip > 0) { + length -= skip; + } + if(length <= 0) { + close(); + } + return skip; + } + + /** + * This is used to close the range once all of the content has + * been fully read. The Range object forces the + * close of the stream once all the content has been consumed + * to ensure that excessive file descriptors are used. Also + * this will ensure that the files can be deleted. + */ + @Override + public void close() throws IOException { + if(!closed) { + in.close(); + closed =true; + } + } + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileWatcher.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileWatcher.java new file mode 100644 index 0000000..6d1f3b2 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileWatcher.java @@ -0,0 +1,179 @@ +/* + * FileWatcher.java February 2008 + * + * Copyright (C) 2008, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; + +/** + * The FileWatcher object is used to create files that + * are to be used for file buffers. All files created by this are + * created in the java.io.tmpdir path. Temporary files + * created in this directory last for a configurable length of time + * before they are deleted. + * + * @author Niall Gallagher + */ +class FileWatcher implements FileFilter { + + /** + * This is the prefix for the temporary files created. + */ + private final String prefix; + + /** + * This is the duration the files created will exist for. + */ + private final long duration; + + /** + * Constructor for the FileWatcher object. This will + * allow temporary files to exist for five minutes. After this + * time the will be removed from the underlying directory. Any + * request for a new file will result in a sweep of the temporary + * directory for all matching files, if they have expired they + * will be deleted. + * + * @param prefix this is the file name prefix for the files + */ + public FileWatcher(String prefix) { + this(prefix, 300000); + } + + /** + * Constructor for the FileWatcher object. This will + * allow temporary files to exist for a configurable length of time. + * After this time the will be removed from the underlying directory. + * Any request for a new file will result in a sweep of the temporary + * directory for all matching files, if they have expired they + * will be deleted. + * + * @param prefix this is the file name prefix for the files + * @param duration this is the duration the files exist for + */ + public FileWatcher(String prefix, long duration) { + this.duration = duration; + this.prefix = prefix; + } + + /** + * This will create a temporary file which can be used as a buffer + * for FileBuffer objects. The file returned by this + * method will be created before it is returned, which ensures it + * can be used as a means to buffer bytes. All files are created + * in the java.io.tmpdir location, which represents + * the underlying file system temporary file destination. + * + * @return this returns a created temporary file for buffers + */ + public File create() throws IOException { + File path = create(prefix); + + if(!path.isDirectory()) { + File parent = path.getParentFile(); + + if(parent.isDirectory()) { + clean(parent); + } + } + return path; + } + + /** + * This will create a temporary file which can be used as a buffer + * for FileBuffer objects. The file returned by this + * method will be created before it is returned, which ensures it + * can be used as a means to buffer bytes. All files are created + * in the java.io.tmpdir location, which represents + * the underlying file system temporary file destination. + * + * @param prefix this is the prefix of the file to be created + * + * @return this returns a created temporary file for buffers + */ + private File create(String prefix) throws IOException { + File file = File.createTempFile(prefix, null); + + if(!file.exists()) { + file.createNewFile(); + } + return file; + } + + /** + * When this method is invoked the files that match the pattern + * of the temporary files are evaluated for deletion. Only those + * files that have not been modified in the duration period can + * be deleted. This ensures the file system is not exhausted. + * + * @param path this is the path of the file to be evaluated + */ + private void clean(File path) throws IOException { + File[] list = path.listFiles(this); + + for(File next : list) { + for(int i = 0; i < 3; i++) { + if(next.delete()) { + break; + } + } + } + } + + /** + * This determines if the file provided is an acceptable file for + * deletion. Acceptable files are those that match the pattern + * of files created by this file system object. If the file is + * a matching file then it is a candidate for deletion. + * + * @param file this is the file to evaluate for deletion + * + * @return this returns true if the file matches the pattern + */ + public boolean accept(File file) { + String name = file.getName(); + + if(file.isDirectory()) { + return false; + } + return accept(file, name); + } + + /** + * This determines if the file provided is an acceptable file for + * deletion. Acceptable files are those that match the pattern + * of files created by this file system object. If the file is + * a matching file then it is a candidate for deletion. + * + * @param file this is the file to evaluate for deletion + * @param name this is the name of the file to be evaluated + * + * @return this returns true if the file matches the pattern + */ + private boolean accept(File file, String name) { + long time = System.currentTimeMillis(); + long modified = file.lastModified(); + + if(modified + duration > time) { // not yet expired + return false; + } + return name.startsWith(prefix); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FilterAllocator.java b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FilterAllocator.java new file mode 100644 index 0000000..ab97423 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/buffer/FilterAllocator.java @@ -0,0 +1,123 @@ +/* + * FilterAllocator.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.buffer; + +import java.io.IOException; + +/** + * The FilterAllocator object is used to provide a means + * to provide a general set of constraints around buffer allocation. + * It can ensure that a minimum capacity is used for default allocation + * and that an upper limit is used for allocation. In general this can + * be used in conjunction with another Allocator which may + * not have such constraints. It ensures that a set of requirements can + * be observed when allocating buffers. + * + * @author Niall Gallagher + */ +public class FilterAllocator implements Allocator { + + /** + * This is the allocator the underlying buffer is allocated with. + */ + protected Allocator source; + + /** + * This is the default initial minimum capacity of the buffer. + */ + protected long capacity; + + /** + * This is the maximum number of bytes that can be allocated. + */ + protected long limit; + + /** + * Constructor for the FilterAllocator object. This is + * used to instantiate the allocator with a default buffer size of + * half a kilobyte. This ensures that it can be used for general + * purpose byte storage and for minor I/O tasks. + * + * @param source this is where the underlying buffer is allocated + */ + public FilterAllocator(Allocator source) { + this(source, 512, 1048576); + } + + /** + * Constructor for the FilterAllocator object. This is + * used to instantiate the allocator with a specified buffer size. + * This is typically used when a very specific buffer capacity is + * required, for example a request body with a known length. + * + * @param source this is where the underlying buffer is allocated + * @param capacity the initial capacity of the allocated buffers + */ + public FilterAllocator(Allocator source, long capacity) { + this(source, capacity, 1048576); + } + + /** + * Constructor for the FilterAllocator object. This is + * used to instantiate the allocator with a specified buffer size. + * This is typically used when a very specific buffer capacity is + * required, for example a request body with a known length. + * + * @param source this is where the underlying buffer is allocated + * @param capacity the initial capacity of the allocated buffers + * @param limit this is the maximum buffer size created by this + */ + public FilterAllocator(Allocator source, long capacity, long limit) { + this.limit = Math.max(capacity, limit); + this.capacity = capacity; + this.source = source; + } + + /** + * This method is used to allocate a default buffer. This will + * allocate a buffer of predetermined size, allowing it to grow + * to an upper limit to accommodate extra data. If the buffer + * requested is larger than the limit an exception is thrown. + * + * @return this returns an allocated buffer with a default size + */ + public Buffer allocate() throws IOException { + return allocate(capacity); + } + + /** + * This method is used to allocate a default buffer. This will + * allocate a buffer of predetermined size, allowing it to grow + * to an upper limit to accommodate extra data. If the buffer + * requested is larger than the limit an exception is thrown. + * + * @param size the initial capacity of the allocated buffer + * + * @return this returns an allocated buffer with a default size + */ + public Buffer allocate(long size) throws IOException { + if(size > limit) { + throw new BufferException("Specified size %s beyond limit", size); + } + if(capacity > size) { + size = capacity; + } + return source.allocate(size); + } +} \ No newline at end of file diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64Encoder.java b/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64Encoder.java new file mode 100644 index 0000000..54c820a --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64Encoder.java @@ -0,0 +1,166 @@ +/* + * Base64Encoder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.encode; + +/** + * The Base64Encoder is used to encode and decode base64 + * content. The implementation used here provides a reasonably fast + * memory efficient encoder for use with input and output streams. It + * is possible to achieve higher performance, however, ease of use + * and convenience are the priorities with this implementation. This + * can only decode complete blocks. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.encode.Base64OutputStream + * @see org.simpleframework.common.encode.Base64InputStream + */ +public class Base64Encoder { + + /** + * This maintains reference data used to fast decoding. + */ + private static final int[] REFERENCE = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, + 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 0, 0, 0, 0, 0,}; + + /** + * This contains the base64 alphabet used for encoding. + */ + private static final char[] ALPHABET = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', }; + + /** + * This method is used to encode the specified byte array of binary + * data in to base64 data. The block is complete and must be decoded + * as a complete block. + * + * @param buf this is the binary data to be encoded + * + * @return this is the base64 encoded value of the data + */ + public static char[] encode(byte[] buf) { + return encode(buf, 0, buf.length); + } + + /** + * This method is used to encode the specified byte array of binary + * data in to base64 data. The block is complete and must be decoded + * as a complete block. + * + * @param buf this is the binary data to be encoded + * @param off this is the offset to read the binary data from + * @param len this is the length of data to encode from the array + * + * @return this is the base64 encoded value of the data + */ + public static char[] encode(byte[] buf, int off, int len) { + char[] text = new char[((len + 2) / 3) * 4]; + int last = off + len; + int a = 0; + int i = 0; + + while (i < last) { + byte one = buf[i++]; + byte two = (i < len) ? buf[i++] : 0; + byte three = (i < len) ? buf[i++] : 0; + + int mask = 0x3F; + text[a++] = ALPHABET[(one >> 2) & mask]; + text[a++] = ALPHABET[((one << 4) | ((two & 0xFF) >> 4)) & mask]; + text[a++] = ALPHABET[((two << 2) | ((three & 0xFF) >> 6)) & mask]; + text[a++] = ALPHABET[three & mask]; + } + switch (len % 3) { + case 1: + text[--a] = '='; + case 2: + text[--a] = '='; + } + return text; + } + + /** + * This is used to decode the provide base64 data back in to an + * array of binary data. The data provided here must be a full block + * of base 64 data in order to be decoded. + * + * @param text this is the base64 text to be decoded + * + * @return this returns the resulting byte array + */ + public static byte[] decode(char[] text) { + return decode(text, 0, text.length); + } + + /** + * This is used to decode the provide base64 data back in to an + * array of binary data. The data provided here must be a full block + * of base 64 data in order to be decoded. + * + * @param text this is the base64 text to be decoded + * @param off this is the offset to read the text data from + * @param len this is the length of data to decode from the text + * + * @return this returns the resulting byte array + */ + public static byte[] decode(char[] text, int off, int len) { + int delta = 0; + + if (text[off + len - 1] == '=') { + delta = text[off + len - 2] == '=' ? 2 : 1; + } + byte[] buf = new byte[len * 3 / 4 - delta]; + int mask = 0xff; + int index = 0; + + for (int i = 0; i < len; i += 4) { + int pos = off + i; + int one = REFERENCE[text[pos]]; + int two = REFERENCE[text[pos + 1]]; + + buf[index++] = (byte) (((one << 2) | (two >> 4)) & mask); + + if (index >= buf.length) { + return buf; + } + int three = REFERENCE[text[pos + 2]]; + + buf[index++] = (byte) (((two << 4) | (three >> 2)) & mask); + + if (index >= buf.length) { + return buf; + } + int four = REFERENCE[text[pos + 3]]; + buf[index++] = (byte) (((three << 6) | four) & mask); + } + return buf; + } + +} \ No newline at end of file diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64InputStream.java b/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64InputStream.java new file mode 100644 index 0000000..8aa28af --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64InputStream.java @@ -0,0 +1,123 @@ +/* + * Base64InputStream.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.encode; + +import java.io.IOException; +import java.io.InputStream; + +/** + * The Base64InputStream is used to read base64 text in + * the form of a string through a conventional input stream. This is + * provided for convenience so that it is possible to encode and + * decode binary data as base64 for implementations that would + * normally use a binary format. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.encode.Base64Encoder + */ +public class Base64InputStream extends InputStream { + + /** + * This is that original base64 text that is to be decoded. + */ + private char[] encoded; + + /** + * This is used to accumulate the decoded text as an array. + */ + private byte[] decoded; + + /** + * This is a temporary buffer used to read one byte at a time. + */ + private byte[] temp; + + /** + * This is the total number of bytes that have been read. + */ + private int count; + + /** + * Constructor for the Base64InputStream object. + * This takes an encoded string and reads it as binary data. + * + * @param source this string containing the encoded data + */ + public Base64InputStream(String source) { + this.encoded = source.toCharArray(); + this.temp = new byte[1]; + } + + /** + * This is used to read the next byte decoded from the text. If + * the data has been fully consumed then this will return the + * standard -1. + * + * @return this returns the next octet decoded + */ + @Override + public int read() throws IOException { + int count = read(temp); + + if (count == -1) { + return -1; + } + return temp[0] & 0xff; + } + + /** + * This is used to read the next byte decoded from the text. If + * the data has been fully consumed then this will return the + * standard -1. + * + * @param array this is the array to decode the text to + * @param offset this is the offset to decode in to the array + * @param this is the number of bytes available to decode to + * + * @return this returns the number of octets decoded + */ + @Override + public int read(byte[] array, int offset, int length) throws IOException { + if (decoded == null) { + decoded = Base64Encoder.decode(encoded); + } + if (count >= decoded.length) { + return -1; + } + int size = Math.min(length, decoded.length - count); + + if (size > 0) { + System.arraycopy(decoded, count, array, offset, size); + count += size; + } + return size; + } + + /** + * This returns the original base64 text that was encoded. This + * is useful for debugging purposes to see the source data. + * + * @return this returns the original base64 text to decode + */ + @Override + public String toString() { + return new String(encoded); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64OutputStream.java b/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64OutputStream.java new file mode 100644 index 0000000..a8f425e --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64OutputStream.java @@ -0,0 +1,138 @@ +/* + * Base64OutputStream.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.encode; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +/** + * The Base64OutputStream is used to write base64 text + * in the form of a string through a conventional output stream. This + * is provided for convenience so that it is possible to encode and + * decode binary data as base64 for implementations that would + * normally use a binary format. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.encode.Base64Encoder + */ +public class Base64OutputStream extends OutputStream { + + private char[] encoded; + private byte[] buffer; + private byte[] temp; + private int count; + + /** + * Constructor for the Base64OutputStream object. A + * stream created with this constructor uses an initial capacity + * of one kilobyte, the capacity is increased as bytes are written. + */ + public Base64OutputStream() { + this(1024); + } + + /** + * Constructor for the Base64OutputStream object. A + * stream created with this constructor can have an initial capacity + * specified. Typically it is a good rule of thumb to use a capacity + * that is just over an additional third of the source binary data. + * + * @param capacity this is the initial capacity of the buffer + */ + public Base64OutputStream(int capacity) { + this.buffer = new byte[capacity]; + this.temp = new byte[1]; + } + + /** + * This method is used to write data as base64 to an internal buffer. + * The toString method can be used to acquire the text + * encoded from the written binary data. + * + * @param octet the octet to encode in to the internal buffer + */ + @Override + public void write(int octet) throws IOException { + temp[0] = (byte) octet; + write(temp); + } + + /** + * This method is used to write data as base64 to an internal buffer. + * The toString method can be used to acquire the text + * encoded from the written binary data. + * + * @param array the octets to encode to the internal buffer + * @param offset this is the offset in the array to encode from + * @param length this is the number of bytes to be encoded + */ + @Override + public void write(byte[] array, int offset, int length) throws IOException { + if (encoded != null) { + throw new IOException("Stream has been closed"); + } + if (count + length > buffer.length) { + expand(count + length); + } + System.arraycopy(array, offset, buffer, count, length); + count += length; + } + + /** + * This will expand the size of the internal buffer. To allow for + * a variable length number of bytes to be written the internal + * buffer can grow as demand exceeds space available. + * + * @param capacity this is the minimum capacity required + */ + private void expand(int capacity) throws IOException { + int length = Math.max(buffer.length * 2, capacity); + + if (buffer.length < capacity) { + buffer = Arrays.copyOf(buffer, length); + } + } + + /** + * This is used to close the stream and encode the buffered bytes + * to base64. Once this method is invoked no further data can be + * encoded with the stream. The toString method can + * be used to acquire the base64 encoded text. + */ + @Override + public void close() throws IOException { + if (encoded == null) { + encoded = Base64Encoder.encode(buffer, 0, count); + } + } + + /** + * This returns the base64 text encoded from the bytes written to + * the stream. This is the primary means for acquiring the base64 + * encoded text once the stream has been closed. + * + * @return this returns the base64 text encoded + */ + @Override + public String toString() { + return new String(encoded); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/Cleaner.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Cleaner.java new file mode 100644 index 0000000..08d7fb0 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Cleaner.java @@ -0,0 +1,44 @@ +/* + * Cleaner.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +/** + * The Cleaner represents an object that is used to + * clean up after the keyed resource. Typically this is used when + * a Lease referring a resource has expired meaning + * that any memory, file descriptors, or other such limited data + * should be released for the keyed resource. The resource keys + * used should be distinct over time to avoid conflicts. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.lease.Lease + */ +public interface Cleaner { + + /** + * This method is used to clean up after a the keyed resource. + * To ensure that the leasing infrastructure operates properly + * this should not block releasing resources. If required this + * should spawn a thread to perform time consuming tasks. + * + * @param key this is the key for the resource to clean + */ + void clean(T key) throws Exception; +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/Contract.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Contract.java new file mode 100644 index 0000000..ac05682 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Contract.java @@ -0,0 +1,77 @@ +/* + * Contract.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * A Contract is used to represent the contract a + * lease has been issued. This contains all relevant information + * regarding the lease, such as the keyed resource that has been + * leased and the duration of the lease. Delays for the contract + * can be measured in any TimeUnit for convinienct. + * + * @author Niall Gallagher + */ +interface Contract extends Delayed { + + /** + * This returns the key for the resource this represents. + * This is used when the contract has expired to clean resources + * associated with the lease. It is passed in to the cleaner as + * an parameter to the callback. The cleaner is then responsible + * for cleaning any resources associated with the lease. + * + * @return returns the resource key that this represents + */ + T getKey(); + + /** + * This method will return the number of TimeUnit + * seconds that remain in the contract. If the value returned is + * less than or equal to zero then it should be assumed that the + * lease has expired, if greater than zero the lease is active. + * + * @return returns the duration in time unit remaining + */ + long getDelay(TimeUnit unit); + + /** + * This method is used to set the number of TimeUnit + * seconds that should remain within the contract. This is used + * when the contract is to be reissued. Once a new duration has + * been set the contract for the lease has been changed and the + * previous expiry time is ignores, so only one clean is called. + * + * @param delay this is the delay to be used for this contract + * @param unit this is the time unit measurment for the delay + */ + void setDelay(long delay, TimeUnit unit); + + /** + * This is used to provide a description of the contract that the + * instance represents. A description well contain the key owned + * by the contract as well as the expiry time expected for it. + * This is used to provide descriptive messages in the exceptions. + * + * @return a descriptive message describing the contract object + */ + String toString(); +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractController.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractController.java new file mode 100644 index 0000000..0353647 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractController.java @@ -0,0 +1,84 @@ +/* + * ContractController.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import org.simpleframework.common.lease.LeaseException; + +/** + * The ContractController forms the interface to the + * lease management system. There are two actions permitted for + * leased resources, these are lease issue and lease renewal. When + * the lease is first issued it is scheduled for the contract + * duration. Once issued the lease can be renewed with another + * duration, which can be less than the previous duration used. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.lease.ContractMaintainer + */ +interface ContractController { + + /** + * This method will establish a contract for the given duration. + * If the contract duration expires before it is renewed then a + * notification is sent, typically to a Cleaner to + * to signify that the resource should be released. The contract + * can also be cancelled by providing a zero length duration. + * + * @param contract a contract representing a leased resource + * + * @exception Exception if the lease could not be done + */ + void issue(Contract contract) throws LeaseException; + + /** + * This ensures that the contract is renewed for the duration on + * the contract, which may have changed since it was issued or + * last renewed. If the duration on the contract has changed this + * will insure the previous contract duration is revoked and the + * new duration is used to maintain the leased resource. + * + * @param contract a contract representing a leased resource + * + * @exception Exception if the lease could not be done + */ + void renew(Contract contract) throws LeaseException; + + /** + * This will cancel the lease and release the resource. This + * has the same effect as the renew method with + * a zero length duration. Once this has been called the + * Cleaner used should be notified immediately. + * If the lease has already expired this throws an exception. + * + * @param contract a contract representing a leased resource + * + * @exception Exception if the expiry has been passed + */ + void cancel(Contract contract) throws LeaseException; + + /** + * This method is used to cancel all outstanding leases and to + * close the controller. Closing the controller ensures that it + * can no longer be used to issue or renew leases. All resources + * occupied by the controller are released, including threads, + * memory, and all leased resources occupied by the instance. + */ + void close(); +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractLease.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractLease.java new file mode 100644 index 0000000..60cd474 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractLease.java @@ -0,0 +1,119 @@ +/* + * ContractLease.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import java.util.concurrent.TimeUnit; + +/** + * The ContractLease is used to maintain contracts by + * using a controller object. This will invoke the controller with + * the contract when a lease operation is performed. A lease is + * renewed by changing the contract duration and passing that to + * the controller which will reestablish the expiry time for it. + * + * @author Niall Gallagher + */ +class ContractLease implements Lease { + + /** + * This is the controller object used to handle contracts. + */ + private final ContractController handler; + + /** + * This is the contract object representing the lease. + */ + private final Contract contract; + + /** + * Constructor for the ContractLease object. This is + * used to create a lease which will maintain a contract using a + * controller object. Lease renewals are performed by changing the + * expiry duration on the contract and notifying the controller. + * + * @param handler this is used to manage the contract expiration + * @param contract this is the contract representing the lease + */ + public ContractLease(ContractController handler, Contract contract) { + this.handler = handler; + this.contract = contract; + } + + /** + * Determines the duration remaining before the lease expires. + * The expiry is given as the number of TimeUnit + * seconds remaining before the lease expires. If this value is + * negative it should be assumed that the lease has expired. + * + * @param unit this is the time unit used for the duration + * + * @return the duration remaining within this lease instance + * + * @exception LeaseException if the lease expiry has passed + */ + public long getExpiry(TimeUnit unit) throws LeaseException { + return contract.getDelay(unit); + } + + /** + * This ensures that the leased resource is maintained for the + * specified number of TimeUnit seconds. Allowing + * the duration unit to be specified enables the lease system + * to maintain a resource with a high degree of accuracy. The + * accuracy of the leasing system is dependant on how long it + * takes to clean the resource associated with the lease. + * + * @param duration this is the length of time to renew for + * @param unit this is the time unit used for the duration + * + * @exception LeaseException if the expiry has been passed + */ + public void renew(long duration, TimeUnit unit) throws LeaseException { + if(duration >= 0) { + contract.setDelay(duration, unit); + } + handler.renew(contract); + } + + /** + * This will cancel the lease and release the resource. This + * has the same effect as the renew method with + * a zero length duration. Once this has been called the + * Cleaner used should be notified immediately. + * If the lease has already expired this throws an exception. + * + * @exception LeaseException if the expiry has been passed + */ + public void cancel() throws LeaseException { + handler.cancel(contract); + } + + /** + * Provides the key for the resource that this lease represents. + * This can be used to identify the resource should the need + * arise. Also, this provides a convenient means of identifying + * leases when using or storing it as an Object. + * + * @return this returns the key for the resource represented + */ + public T getKey() { + return contract.getKey(); + } +} + diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractMaintainer.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractMaintainer.java new file mode 100644 index 0000000..3e65039 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractMaintainer.java @@ -0,0 +1,115 @@ +/* + * ContractMaintainer.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + * The ContractMaintainer is used provide a controller + * uses a cleaner. This simple delegates to the cleaner queue when + * a renewal is required. Renewals are performed by revoking the + * contract and then reissuing it. This will ensure that the delay + * for expiry of the contract is reestablished within the queue. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.lease.LeaseCleaner + */ +class ContractMaintainer implements ContractController { + + /** + * The queue that is used to issue and revoke contracts. + */ + private final LeaseCleaner queue; + + /** + * Constructor for the ContractMaintainer object. This + * is used to create a controller for contracts which will ensure + * that the lease expiry durations are met. All notifications of + * expiry will be delivered to the provided cleaner instance. + * + * @param cleaner this is used to receive expiry notifications + */ + public ContractMaintainer(Cleaner cleaner) { + this.queue = new LeaseCleaner(cleaner); + } + + /** + * This method will establish a contract for the given duration. + * If the contract duration expires before it is renewed then a + * notification is sent, typically to a Cleaner to + * to signify that the resource should be released. The contract + * can also be cancelled by providing a zero length duration. + * + * @param contract a contract representing a leased resource + */ + public synchronized void issue(Contract contract) { + queue.issue(contract); + } + + /** + * This ensures that the contract is renewed for the duration on + * the contract, which may have changed since it was issued or + * last renewed. If the duration on the contract has changed this + * will insure the previous contract duration is revoked and the + * new duration is used to maintain the leased resource. + * + * @param contract a contract representing a leased resource + */ + public synchronized void renew(Contract contract) { + boolean active = queue.revoke(contract); + + if(!active) { + throw new LeaseException("Lease has expired for " + contract); + } + queue.issue(contract); + } + + /** + * This will cancel the lease and release the resource. This + * has the same effect as the renew method with + * a zero length duration. Once this has been called the + * Cleaner used should be notified immediately. + * If the lease has already expired this throws an exception. + * + * @param contract a contract representing a leased resource + */ + public synchronized void cancel(Contract contract) { + boolean active = queue.revoke(contract); + + if(!active) { + throw new LeaseException("Lease has expired for " + contract); + } + contract.setDelay(0, MILLISECONDS); + queue.issue(contract); + } + + /** + * This method is used to cancel all outstanding leases and to + * close the controller. Closing the controller ensures that it + * can no longer be used to issue or renew leases. All resources + * occupied by the controller are released, including threads, + * memory, and all leased resources occupied by the instance. + * + * @throws LeaseException if the controller can not be closed + */ + public synchronized void close() { + queue.close(); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractQueue.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractQueue.java new file mode 100644 index 0000000..df881dc --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractQueue.java @@ -0,0 +1,44 @@ +/* + * ContractQueue.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import java.util.concurrent.DelayQueue; + +/** + * The ContraceQueue object is used to queue contracts + * between two asynchronous threads of execution. This allows the + * controller to schedule the lease contract for expiry. Taking the + * contracts from the queue is delayed for the contract duration. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.lease.Contract + */ +class ContractQueue extends DelayQueue> { + + /** + * Constructor for the ContractQueue object. This + * is used to create a queue for passing contracts between two + * asynchronous threads of execution. This is used by the + * lease controller to schedule the lease contract for expiry. + */ + public ContractQueue() { + super(); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/Expiration.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Expiration.java new file mode 100644 index 0000000..fca1c14 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Expiration.java @@ -0,0 +1,163 @@ +/* + * Expiration.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * A Expiration is used to represent the expiration + * for a lease. This contains all relevant information for the + * the lease, such as the keyed resource that has been leased and + * the duration of the lease. Durations for the contract can be + * measured in any TimeUnit for convenience. + * + * @author Niall Gallagher + */ +class Expiration implements Contract { + + /** + * This is the expiration time in nanoseconds for this. + */ + private volatile long time; + + /** + * This is the key representing the resource being lease. + */ + private T key; + + /** + * Constructor for the Expiration object. This is used + * to create a contract with an initial expiry period. Once this + * is created the time is taken and the contract can be issued. + * + * @param key this is the key that this contract represents + * @param lease this is the initial lease duration to be used + * @param scale this is the time unit scale that is to be used + */ + public Expiration(T key, long lease, TimeUnit scale) { + this.time = getTime() + scale.toNanos(lease); + this.key = key; + } + + /** + * This returns the key for the resource this represents. + * This is used when the contract has expired to clean resources + * associated with the lease. It is passed in to the cleaner as + * an parameter to the callback. The cleaner is then responsible + * for cleaning any resources associated with the lease. + * + * @return returns the resource key that this represents + */ + public T getKey() { + return key; + } + + /** + * This method will return the number of TimeUnit + * seconds that remain in the contract. If the value returned is + * less than or equal to zero then it should be assumed that the + * lease has expired, if greater than zero the lease is active. + * + * @return returns the duration in the time unit remaining + */ + public long getDelay(TimeUnit unit) { + return unit.convert(time - getTime(), NANOSECONDS); + } + + /** + * This method is used to set the number of TimeUnit + * seconds that should remain within the contract. This is used + * when the contract is to be reissued. Once a new duration has + * been set the contract for the lease has been changed and the + * previous expiry time is ignores, so only one clean is called. + * + * @param delay this is the delay to be used for this contract + * @param unit this is the time unit measurment for the delay + */ + public void setDelay(long delay, TimeUnit unit) { + this.time = getTime() + unit.toNanos(delay); + } + + /** + * This method returns the current time in nanoseconds. This is + * used to allow the duration of the lease to be calculated with + * any given time unit which allows flexibility in setting and + * getting the current delay for the contract. + * + * @return returns the current time in nanoseconds remaining + */ + private long getTime() { + return System.nanoTime(); + } + + /** + * This is used to compare the specified delay to this delay. The + * result of this operation is used to prioritize contracts in + * order of first to expire. Contracts that expire first reach + * the top of the contract queue and are taken off for cleaning. + * + * @param other this is the delay to be compared with this + * + * @return this returns zero if equal otherwise the difference + */ + public int compareTo(Delayed other) { + Expiration value = (Expiration) other; + + if(other == this) { + return 0; + } + return compareTo(value); + } + + /** + * This is used to compare the specified delay to this delay. The + * result of this operation is used to prioritize contracts in + * order of first to expire. Contracts that expire first reach + * the top of the contract queue and are taken off for cleaning. + * + * @param value this is the expiration to be compared with this + * + * @return this returns zero if equal otherwise the difference + */ + private int compareTo(Expiration value) { + long diff = time - value.time; + + if(diff < 0) { + return -1; + } else if(diff > 0) { + return 1; + } + return 0; + } + + /** + * This is used to provide a description of the contract that the + * instance represents. A description well contain the key owned + * by the contract as well as the expiry time expected for it. + * This is used to provide descriptive messages in the exceptions. + * + * @return a descriptive message describing the contract object + */ + public String toString() { + return String.format("contract %s", key); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/Lease.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Lease.java new file mode 100644 index 0000000..d2a9785 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/Lease.java @@ -0,0 +1,85 @@ +/* + * Lease.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import java.util.concurrent.TimeUnit; + +/** + * The Lease object is used to keep a keyed resource + * active. This provides a very simple lease that can be used to + * track the activity of a resource or system. Keeping track of + * activity allows resources to be maintained until such time + * that they are no longer required, allowing the server to clean + * up any allocated memory, file descriptors, or other such data. + * + * @author Niall Gallagher + */ +public interface Lease { + + /** + * Determines the duration remaining before the lease expires. + * The expiry is given as the number of TimeUnit + * seconds remaining before the lease expires. If this value is + * negative it should be assumed that the lease has expired. + * + * @param unit this is the time unit used for the duration + * + * @return the duration remaining within this lease instance + * + * @exception Exception if the expiry could not be acquired + */ + long getExpiry(TimeUnit unit) throws LeaseException; + + /** + * This ensures that the leased resource is maintained for the + * specified number of TimeUnit seconds. Allowing + * the duration unit to be specified enables the lease system + * to maintain a resource with a high degree of accuracy. The + * accuracy of the leasing system is dependent on how long it + * takes to clean the resource associated with the lease. + * + * @param duration this is the length of time to renew for + * @param unit this is the time unit used for the duration + * + * @exception Exception if the lease could not be renewed + */ + void renew(long duration, TimeUnit unit) throws LeaseException; + + /** + * This will cancel the lease and release the resource. This + * has the same effect as the renew method with + * a zero length duration. Once this has been called the + * Cleaner used should be notified immediately. + * If the lease has already expired this throws an exception. + * + * @exception Exception if the expiry has been passed + */ + void cancel() throws LeaseException; + + /** + * Provides the key for the resource that this lease represents. + * This can be used to identify the resource should the need + * arise. Also, this provides a convenient means of identifying + * leases when using or storing it as an Object. + * + * @return this returns the key for the resource represented + */ + T getKey(); + +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseCleaner.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseCleaner.java new file mode 100644 index 0000000..d1fe912 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseCleaner.java @@ -0,0 +1,155 @@ +/* + * LeaseCleaner.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import org.simpleframework.common.thread.Daemon; + +/** + * The LeaseCleaner provides a means of providing + * callbacks to clean a leased resource once the contract duration + * has expired. This will acquire contracts from the queue and + * invoke the Cleaner notification method. This will + * wait until the current clean operation has completed before it + * attempts to clean the next contract. + * + * @author Niall Gallagher + */ +class LeaseCleaner extends Daemon { + + /** + * This is used to queue contracts that are to be cleaned. + */ + private final ContractQueue queue; + + /** + * This is the cleaner that is invoked to clean contracts. + */ + private final Cleaner cleaner; + + /** + * Constructor for the LeaseCleaner object. This + * can be used to issue, update, and expire leases. When a lease + * expires notification is sent to the Cleaner + * object provided. This allows an implementation independent + * means to clean up once a specific lease has expired. + * + * @param cleaner this will receive expiration notifications + */ + public LeaseCleaner(Cleaner cleaner) { + this.queue = new ContractQueue(); + this.cleaner = cleaner; + this.start(); + } + + /** + * This revokes a contract that has previously been issued. This + * is used when the contract duration has changed so that it can + * be reissued again with a new duration. This returns true if + * the contract was still active and false if it did not exist. + * + * @param contract this is the contract that contains details + */ + public boolean revoke(Contract contract) throws LeaseException { + if(!isActive()) { + throw new LeaseException("Lease can not be revoked"); + } + return queue.remove(contract); + } + + /** + * This method will establish a contract for a given resource. + * If the contract duration expires before it is renewed then + * a notification is sent, to the issued Cleaner + * implementation, to signify that the resource has expired. + * + * @param contract this is the contract that contains details + */ + public boolean issue(Contract contract) throws LeaseException { + if(!isActive()) { + throw new LeaseException("Lease can not be issued"); + } + return queue.offer(contract); + } + + /** + * This acquires expired lease contracts from the queue once the + * expiry duration has passed. This will deliver notification to + * the Cleaner object once the contract has been + * taken from the queue. This allows the cleaner to clean up any + * resources associated with the lease before the next expiration. + */ + public void run() { + while(isActive()) { + try { + clean(); + } catch(Throwable e) { + continue; + } + } + purge(); + } + + /** + * This method is used to take the lease from the queue and give + * it to the cleaner for expiry. This effectively waits until the + * next contract expiry has passed, once it has passed the key + * for that contract is given to the cleaner to clean up resources. + */ + private void clean() throws Exception { + Contract next = queue.take(); + T key = next.getKey(); + + if(key != null) { + cleaner.clean(key); + } + } + + /** + * Here all of the existing contracts are purged when the invoker + * is closed. This ensures that each leased resource has a chance + * to clean up after the lease manager has been closed. All of the + * contracts are given a zero delay and cleaned immediately such + * that once this method has finished the queue will be empty. + */ + private void purge() { + for(Contract next : queue) { + T key = next.getKey(); + + try { + next.setDelay(0L, NANOSECONDS); + cleaner.clean(key); + } catch(Throwable e) { + continue; + } + } + } + + /** + * Here we shutdown the lease maintainer so that the thread will + * die. Shutting down the maintainer is done by interrupting the + * thread and setting the dead flag to true. Once this is invoked + * then the thread will no longer be running for this object. + */ + public void close() { + stop(); + interrupt(); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseException.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseException.java new file mode 100644 index 0000000..3a47949 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseException.java @@ -0,0 +1,52 @@ +/* + * LeaseException.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +/** + * The LeaseException is used to indicate that some + * operation failed when using the lease after the lease duration + * has expired. Typically this will be thrown when the lease is + * renewed after the expiry period has passed. + * + * @author Niall Gallagher + */ +public class LeaseException extends RuntimeException { + + /** + * This constructor is used if there is a description of the + * event that caused the exception required. This can be given + * a message used to describe the situation for the exception. + * + * @param message this is a description of the exception + */ + public LeaseException(String template) { + super(template); + } + + /** + * This constructor is used if there is a description of the + * event that caused the exception required. This can be given + * a message used to describe the situation for the exception. + * + * @param message this is a description of the exception + */ + public LeaseException(String template, Throwable cause) { + super(template, cause); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseManager.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseManager.java new file mode 100644 index 0000000..93459fd --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseManager.java @@ -0,0 +1,93 @@ +/* + * LeaseManager.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import java.util.concurrent.TimeUnit; + +/** + * The LeaseManager is used to issue a lease for a + * named resource. This is effectively used to issue a request + * for a keyed resource to be released when a lease has expired. + * The use of a Lease simplifies the interface to + * the notification and also enables other objects to manage the + * lease without any knowledge of the resource it represents. + * + * @author Niall Gallagher + */ +public class LeaseManager implements LeaseProvider { + + /** + * This is the controller used to handle lease operations. + */ + private ContractController handler; + + /** + * Constructor for the LeaseManager object. This + * instance is created using a specified notification handler. + * The specified Cleaner will be notified when + * the lease for a named resource expires, which will allow + * the cleaner object to perform a clean up for that resource. + * + * @param cleaner the cleaner object receiving notifications + */ + public LeaseManager(Cleaner cleaner) { + this.handler = new ContractMaintainer(cleaner); + } + + /** + * This method will issue a Lease object that + * can be used to manage the release of a keyed resource. If + * the lease duration expires before it is renewed then the + * notification is sent, typically to a Cleaner + * implementation, to signify that the resource should be + * recovered. The issued lease can also be canceled. + * + * @param key this is the key for the leased resource + * @param duration this is the duration of the issued lease + * @param unit this is the time unit to issue the lease with + * + * @return a lease that can be used to manage notification + */ + public Lease lease(T key, long duration, TimeUnit unit) { + Contract contract = new Expiration(key, duration, unit); + + try { + handler.issue(contract); + } catch(Exception e) { + throw new LeaseException("Could not issue lease", e); + } + return new ContractLease(handler, contract); + } + + /** + * This is used to close the lease provider such that all of + * the outstanding leases are canceled. This also ensures the + * provider can no longer be used to issue new leases, such + * that further invocations of the lease method + * will result in null leases. Once the provider has been + * closes all threads and other such resources are released. + */ + public void close() { + try { + handler.close(); + } catch(Exception e) { + return; + } + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseMap.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseMap.java new file mode 100644 index 0000000..711a466 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseMap.java @@ -0,0 +1,83 @@ +/* + * LeaseMap.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * The LeaseMap object is used to map lease keys to the + * lease objects managing those objects. This allows components that + * are using the leasing framework to associate an object with its + * lease and vice versa. Such a capability enables lease renewals to + * be performed without the need for a direct handle on the lease. + * + * @author Niall Gallagher + */ +public class LeaseMap extends ConcurrentHashMap> { + + /** + * Constructor for the LeaseMap object. This will + * create a map for mapping leased resource keys to the leases + * that manage them. Having such a map allows leases to be + * maintained without having a direct handle on the lease. + */ + public LeaseMap() { + super(); + } + + /** + * Constructor for the LeaseMap object. This will + * create a map for mapping leased resource keys to the leases + * that manage them. Having such a map allows leases to be + * maintained without having a direct handle on the lease. + * + * @param capacity this is the initial capacity of the map + */ + public LeaseMap(int capacity) { + super(capacity); + } + + /** + * This is used to acquire the Lease object that is + * mapped to the specified key. Overriding this method ensures + * that even without generic parameters a type safe method for + * acquiring the registered lease objects can be used. + * + * @param key this is the key used to acquire the lease object + * + * @return this is the lease that is associated with the key + */ + public Lease get(Object key) { + return super.get(key); + } + + /** + * This is used to remove the Lease object that is + * mapped to the specified key. Overriding this method ensures + * that even without generic parameters a type safe method for + * removing the registered lease objects can be used. + * + * @param key this is the key used to remove the lease object + * + * @return this is the lease that is associated with the key + */ + public Lease remove(Object key) { + return super.remove(key); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseProvider.java b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseProvider.java new file mode 100644 index 0000000..e5e7d8c --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseProvider.java @@ -0,0 +1,60 @@ +/* + * LeaseProvider.java May 2004 + * + * Copyright (C) 2004, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.lease; + +import java.util.concurrent.TimeUnit; + +/** + * The LeaseProvider is used to issue a lease for a + * named resource. This is effectively used to issue a request + * for a keyed resource to be released when a lease has expired. + * The use of a Lease simplifies the interface to + * the notification and also enables other objects to manage the + * lease without any knowledge of the resource it represents. + * + * @author Niall Gallagher + */ +public interface LeaseProvider { + + /** + * This method will issue a Lease object that + * can be used to manage the release of a keyed resource. If + * the lease duration expires before it is renewed then the + * notification is sent, typically to a Cleaner + * implementation, to signify that the resource should be + * recovered. The issued lease can also be canceled. + * + * @param key this is the key for the leased resource + * @param duration this is the duration of the issued lease + * @param unit this is the time unit to issue the lease with + * + * @return a lease that can be used to manage notification + */ + Lease lease(T key, long duration, TimeUnit unit); + + /** + * This is used to close the lease provider such that all of + * the outstanding leases are canceled. This also ensures the + * provider can no longer be used to issue new leases, such + * that further invocations of the lease method + * will result in null leases. Once the provider has been + * closes all threads and other such resources are released. + */ + void close(); +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/parse/MapParser.java b/simple/simple-common/src/main/java/org/simpleframework/common/parse/MapParser.java new file mode 100644 index 0000000..d242937 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/parse/MapParser.java @@ -0,0 +1,251 @@ +/* + * MapParser.java February 2005 + * + * Copyright (C) 2005, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.parse; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The MapParser object represents a parser for name + * value pairs. Any parser extending this will typically be parsing + * name=value tokens or the like, and inserting these pairs into + * the internal map. This type of parser is useful as it exposes all + * pairs extracted using the java.util.Map interface + * and as such can be used with the Java collections framework. The + * internal map used by this is a Hashtable, however + * subclasses are free to assign a different type to the map used. + * + * @author Niall Gallagher + */ +public abstract class MapParser extends Parser implements Map { + + /** + * Represents all values inserted to the map as a list of values. + */ + protected Map> all; + + /** + * Represents the last value inserted into this map instance. + */ + protected Map map; + + /** + * Constructor for the MapParser object. This is + * used to create a new parser that makes use of a thread safe + * map implementation. The HashMap is used so + * that the resulting parser can be accessed in a concurrent + * environment with the fear of data corruption. + */ + protected MapParser(){ + this.all = new HashMap>(); + this.map = new HashMap(); + } + + /** + * This is used to determine whether a token representing the + * name of a pair has been inserted into the internal map. The + * object passed into this method should be a string, as all + * tokens stored within the map will be stored as strings. + * + * @param name this is the name of a pair within the map + * + * @return this returns true if the pair of that name exists + */ + public boolean containsKey(Object name) { + return map.containsKey(name); + } + + /** + * This method is used to determine whether any pair that has + * been inserted into the internal map had the presented value. + * If one or more pairs within the collected tokens contains + * the value provided then this method will return true. + * + * @param value this is the value that is to be searched for + * + * @return this returns true if any value is equal to this + */ + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + /** + * This method is used to acquire the name and value pairs that + * have currently been collected by this parser. This is used + * to determine which tokens have been extracted from the + * source. It is useful when the tokens have to be gathered. + * + * @return this set of token pairs that have been extracted + */ + public Set> entrySet() { + return map.entrySet(); + } + + /** + * The get method is used to acquire the value for + * a named pair. So if a pair of name=value has been parsed and + * inserted into the collection of tokens this will return the + * value given the name. The value returned will be a string. + * + * @param name this is a string used to search for the value + * + * @return this is the value, as a string, that has been found + */ + public T get(Object name) { + return map.get(name); + } + + /** + * This method is used to acquire a List for all of + * the values that have been put in to the map. The list allows + * all values associated with the specified key. This enables a + * parser to collect a number of associated tokens. + * + * @param key this is the key used to search for the value + * + * @return this is the list of values associated with the key + */ + public List getAll(Object key) { + return all.get(key); + } + + /** + * This method is used to determine whether the parser has any + * tokens available. If the size is zero then the + * parser is empty and this returns true. The is acts as a + * proxy the the isEmpty of the internal map. + * + * @return this is true if there are no available tokens + */ + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * This is used to acquire the names for all the tokens that + * have currently been collected by this parser. This is used + * to determine which tokens have been extracted from the + * source. It is useful when the tokens have to be gathered. + * + * @return the set of name tokens that have been extracted + */ + public Set keySet() { + return map.keySet(); + } + + /** + * The put method is used to insert the name and + * value provided into the collection of tokens. Although it is + * up to the parser to decide what values will be inserted it + * is generally the case that the inserted tokens will be text. + * + * @param name this is the name token from a name=value pair + * @param value this is the value token from a name=value pair + * + * @return this returns the previous value if there was any + */ + public T put(T name, T value) { + List list = all.get(name); + T first = map.get(name); + + if(list == null) { + list = new ArrayList(); + all.put(name, list); + } + list.add(value); + + if(first == null) { + return map.put(name, value); + } + return null; + } + + /** + * This method is used to insert a collection of tokens into + * the parsers map. This is used when another source of tokens + * is required to populate the connection currently maintained + * within this parsers internal map. Any tokens that currently + * exist with similar names will be overwritten by this. + * + * @param data this is the collection of tokens to be added + */ + public void putAll(Map data) { + Set keySet = data.keySet(); + + for(T key : keySet) { + T value = data.get(key); + + if(value != null) { + put(key, value); + } + } + } + + /** + * The remove method is used to remove the named + * token pair from the collection of tokens. This acts like a + * take, in that it will get the token value and remove if + * from the collection of tokens the parser has stored. + * + * @param name this is a string used to search for the value + * + * @return this is the value, as a string, that is removed + */ + public T remove(Object name) { + return map.remove(name); + } + + /** + * This obviously enough provides the number of tokens that + * have been inserted into the internal map. This acts as + * a proxy method for the internal map size. + * + * @return this returns the number of tokens are available + */ + public int size() { + return map.size(); + } + + /** + * This method is used to acquire the value for all tokens that + * have currently been collected by this parser. This is used + * to determine which tokens have been extracted from the + * source. It is useful when the tokens have to be gathered. + * + * @return the list of value tokens that have been extracted + */ + public Collection values() { + return map.values(); + } + + /** + * The clear method is used to wipe out all the + * currently existing tokens from the collection. This is used + * to recycle the parser so that it can be used to parse some + * other source of tokens without any lingering state. + */ + public void clear() { + all.clear(); + map.clear(); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/parse/ParseBuffer.java b/simple/simple-common/src/main/java/org/simpleframework/common/parse/ParseBuffer.java new file mode 100644 index 0000000..d680a08 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/parse/ParseBuffer.java @@ -0,0 +1,247 @@ +/* + * ParseBuffer.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.parse; + +/** + * This is primarily used to replace the StringBuffer + * class, as a way for the Parser to store the char's + * for a specific region within the parse data that constitutes a + * desired value. The methods are not synchronized so it enables + * the char's to be taken quicker than the + * StringBuffer class. + * + * @author Niall Gallagher + */ +public class ParseBuffer { + + /** + * This is used to quicken toString. + */ + protected String cache; + + /** + * The char's this buffer accumulated. + */ + protected char[] buf; + + /** + * This is the number of char's stored. + */ + protected int count; + + /** + * Constructor for ParseBuffer. The default + * ParseBuffer stores 16 char's + * before a resize is needed to accommodate + * extra characters. + */ + public ParseBuffer(){ + this(16); + } + + /** + * This creates a ParseBuffer with a specific + * default size. The buffer will be created the with the + * length specified. The ParseBuffer can grow + * to accommodate a collection of char's larger + * the the size specified. + * + * @param size initial size of this ParseBuffer + */ + public ParseBuffer(int size){ + this.buf = new char[size]; + } + + /** + * This will add a char to the end of the buffer. + * The buffer will not overflow with repeated uses of the + * append, it uses an ensureCapacity + * method which will allow the buffer to dynamically grow in + * size to accommodate more char's. + * + * @param c the char to be appended + */ + public void append(char c){ + ensureCapacity(count+ 1); + buf[count++] = c; + } + + /** + * This will add a String to the end of the buffer. + * The buffer will not overflow with repeated uses of the + * append, it uses an ensureCapacity + * method which will allow the buffer to dynamically grow in + * size to accommodate large String objects. + * + * @param text the String to be appended to this + */ + public void append(String text){ + ensureCapacity(count+ text.length()); + text.getChars(0,text.length(),buf,count); + count += text.length(); + } + + /** + * This will reset the buffer in such a way that the buffer is + * cleared of all contents and then has the given string appended. + * This is used when a value is to be set into the buffer value. + * See the append(String) method for reference. + * + * @param text this is the text that is to be appended to this + */ + public void reset(String text) { + clear(); + append(text); + } + + /** + * This will add a ParseBuffer to the end of this. + * The buffer will not overflow with repeated uses of the + * append, it uses an ensureCapacity + * method which will allow the buffer to dynamically grow in + * size to accommodate large ParseBuffer objects. + * + * @param text the ParseBuffer to be appended + */ + public void append(ParseBuffer text){ + append(text.buf, 0, text.count); + } + + /** + * This will reset the buffer in such a way that the buffer is + * cleared of all contents and then has the given string appended. + * This is used when a value is to be set into the buffer value. + * See the append(ParseBuffer) method for reference. + * + * @param text this is the text that is to be appended to this + */ + public void reset(ParseBuffer text) { + clear(); + append(text); + } + /** + * This will add a char to the end of the buffer. + * The buffer will not overflow with repeated uses of the + * append, it uses an ensureCapacity + * method which will allow the buffer to dynamically grow in + * size to accommodate large char arrays. + * + * @param c the char array to be appended to this + * @param off the read offset for the array + * @param len the number of char's to add + */ + public void append(char[] c, int off, int len){ + ensureCapacity(count+ len); + System.arraycopy(c,off,buf,count,len); + count+=len; + } + + /** + * This will add a String to the end of the buffer. + * The buffer will not overflow with repeated uses of the + * append, it uses an ensureCapacity + * method which will allow the buffer to dynamically grow in + * size to accommodate large String objects. + * + * @param str the String to be appended to this + * @param off the read offset for the String + * @param len the number of char's to add + */ + public void append(String str, int off, int len){ + ensureCapacity(count+ len); + str.getChars(off,len,buf,count); + count += len; + } + + + /** + * This will add a ParseBuffer to the end of this. + * The buffer will not overflow with repeated uses of the + * append, it uses an ensureCapacity + * method which will allow the buffer to dynamically grow in + * size to accommodate large ParseBuffer objects. + * + * @param text the ParseBuffer to be appended + * @param off the read offset for the ParseBuffer + * @param len the number of char's to add + */ + public void append(ParseBuffer text, int off, int len){ + append(text.buf, off, len); + } + + /** + * This ensure that there is enough space in the buffer to + * allow for more char's to be added. If + * the buffer is already larger than min then the buffer + * will not be expanded at all. + * + * @param min the minimum size needed + */ + protected void ensureCapacity(int min) { + if(buf.length < min) { + int size = buf.length * 2; + int max = Math.max(min, size); + char[] temp = new char[max]; + System.arraycopy(buf, 0, temp, 0, count); + buf = temp; + } + } + + /** + * This will empty the ParseBuffer so that the + * toString parameter will return null. + * This is used so that the same ParseBuffer can be + * recycled for different tokens. + */ + public void clear(){ + cache = null; + count = 0; + } + + /** + * This will return the number of bytes that have been appended + * to the ParseBuffer. This will return zero after + * the clear method has been invoked. + * + * @return the number of char's within the buffer + */ + public int length(){ + return count; + } + + /** + * This will return the characters that have been appended to the + * ParseBuffer as a String object. + * If the String object has been created before then + * a cached String object will be returned. This + * method will return null after clear is invoked. + * + * @return the char's appended as a String + */ + public String toString(){ + if(count <= 0) { + return null; + } + if(cache != null) { + return cache; + } + cache = new String(buf,0,count); + return cache; + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/parse/Parser.java b/simple/simple-common/src/main/java/org/simpleframework/common/parse/Parser.java new file mode 100644 index 0000000..5ba7b52 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/parse/Parser.java @@ -0,0 +1,197 @@ +/* + * Parser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.parse; + +/** + * This Parser object is to be used as a simple template + * for parsing uncomplicated expressions. This object is used to parse + * a String. This provides a few methods that can be used + * to store and track the reading of data from a buffer. There are two + * abstract methods provided to allow this to be subclassed to create + * a Parser for a given String. + * + * @author Niall Gallagher + */ +public abstract class Parser { + + /** + * This is the buffer that is being parsed. + */ + protected char[] buf; + + /** + * This represents the current read offset. + */ + protected int off; + + /** + * This represents the length of the buffer. + */ + protected int count; + + /** + * This is a no argument constructor for the Parser. + * This will be invoked by each subclass of this object. It will + * set the buffer to a zero length buffer so that when the + * ensureCapacity method is used the buf's + * length can be checked. + */ + protected Parser(){ + this.buf = new char[0]; + } + + /** + * This is used to parse the String given to it. This + * will ensure that the char buffer has enough space + * to contain the characters from the String. This + * will firstly ensure that the buffer is resized if nessecary. The + * second step in this parse method is to initialize + * the Parser object so that multiple parse invocations + * can be made. The init method will reset this to an + * prepared state. Then finally the parse method is + * called to parse the char buffer. + * + * @param text the String to be parsed with this + * Parser + */ + public void parse(String text){ + if(text != null){ + ensureCapacity(text.length()); + count = text.length(); + text.getChars(0, count, buf,0); + init(); + parse(); + } + } + + /** + * This ensure that there is enough space in the buffer to allow + * for more char's to be added. If the buffer is + * already larger than min then the buffer will not be expanded + * at all. + * + * @param min the minimum size needed to accommodate the characters + */ + protected void ensureCapacity(int min) { + if(buf.length < min) { + int size = buf.length * 2; + int max = Math.max(min, size); + char[] temp = new char[max]; + buf = temp; + } + } + + /** + * This is used to determine if a given ISO-8859-1 character is + * a space character. That is a whitespace character this sees + * the, space, carriage return and line feed characters as + * whitespace characters. + * + * @param c the character that is being determined by this + * + * @return true if the character given it is a space character + */ + protected boolean space(char c) { + switch(c){ + case ' ': case '\t': + case '\n': case '\r': + return true; + default: + return false; + } + } + + /** + * This is used to determine weather or not a given character is + * a digit character. It assumes iso-8859-1 encoding to compare. + * + * @param c the character being determined by this method + * + * @return true if the character given is a digit character + */ + protected boolean digit(char c){ + return c <= '9' && '0' <= c; + } + + /** + * This takes a unicode character and assumes an encoding of + * ISO-8859-1. This then checks to see if the given character + * is uppercase if it is it converts it into is ISO-8859-1 + * lowercase char. + * + * @param c the char to be converted to lowercase + * + * @return the lowercase ISO-8859-1 of the given character + */ + protected char toLower(char c) { + if(c >= 'A' && c <= 'Z') { + return (char)((c - 'A') + 'a'); + } + return c; + } + + /** This is used to skip an arbitrary String within the + * char buf. It checks the length of the String + * first to ensure that it will not go out of bounds. A comparison + * is then made with the buffers contents and the String + * if the reigon in the buffer matched the String then the + * offset within the buffer is increased by the String's + * length so that it has effectively skipped it. + * + * @param text this is the String value to be skipped + * + * @return true if the String was skipped + */ + protected boolean skip(String text){ + int size = text.length(); + int read = 0; + + if(off + size > count){ + return false; + } + while(read < size){ + char a = text.charAt(read); + char b = buf[off + read++]; + + if(toLower(a) != toLower(b)){ + return false; + } + } + off += size; + return true; + } + + /** + * This will initialize the Parser when it is ready + * to parse a new String. This will reset the + * Parser to a ready state. The init + * method is invoked by the Parser when the + * parse method is invoked. + */ + protected abstract void init(); + + /** + * This is the method that should be implemented to read + * the buf. This method should attempt to extract tokens + * from the buffer so that thes tokens may some how be + * used to determine the semantics. This method is invoked + * after the init method is invoked. + */ + protected abstract void parse(); +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentExecutor.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentExecutor.java new file mode 100644 index 0000000..9f99025 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentExecutor.java @@ -0,0 +1,109 @@ +/* + * ConcurrentExecutor.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import java.util.concurrent.Executor; + +/** + * The ConcurrentExecutor object is used to execute tasks + * in a thread pool. This creates a thread pool with an unbounded list + * of outstanding tasks, which ensures that any system requesting + * a task to be executed will not block when handing it over. + * + * @author Niall Gallagher + */ +public class ConcurrentExecutor implements Executor { + + /** + * This is the queue used to enqueue the tasks for execution. + */ + private final ExecutorQueue queue; + + /** + * Constructor for the ConcurrentExecutor object. This + * is used to create a pool of threads that can be used to execute + * arbitrary Runnable tasks. If the threads are + * busy this will simply enqueue the tasks and return. + * + * @param type this is the type of runnable that this accepts + */ + public ConcurrentExecutor(Class type) { + this(type, 10); + } + + /** + * Constructor for the ConcurrentExecutor object. This + * is used to create a pool of threads that can be used to execute + * arbitrary Runnable tasks. If the threads are + * busy this will simply enqueue the tasks and return. + * + * @param type this is the type of runnable that this accepts + * @param size this is the number of threads to use in the pool + */ + public ConcurrentExecutor(Class type, int size) { + this(type, size, size); + } + + /** + * Constructor for the ConcurrentExecutor object. This + * is used to create a pool of threads that can be used to execute + * arbitrary Runnable tasks. If the threads are + * busy this will simply enqueue the tasks and return. + * + * @param type this is the type of runnable that this accepts + * @param rest this is the number of threads to use in the pool + * @param active this is the maximum size the pool can grow to + */ + public ConcurrentExecutor(Class type, int rest, int active) { + this.queue = new ExecutorQueue(type, rest, active); + } + + /** + * The execute method is used to queue the task for + * execution. If all threads are busy the provided task is queued + * and waits until all current and outstanding tasks are finished. + * + * @param task this is the task to be queued for execution + */ + public void execute(Runnable task) { + queue.execute(task); + } + + /** + * This is used to stop the executor by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return once it has been stopped, and no further tasks will be + * accepted by this pool for execution. + */ + public void stop() { + stop(60000); + } + + /** + * This is used to stop the executor by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return once it has been stopped, and no further tasks will be + * accepted by this pool for execution. + * + * @param wait the number of milliseconds to wait for it to stop + */ + public void stop(long wait) { + queue.stop(wait); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentScheduler.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentScheduler.java new file mode 100644 index 0000000..bb4a117 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentScheduler.java @@ -0,0 +1,122 @@ +/* + * ConcurrentScheduler.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import java.util.concurrent.TimeUnit; + +/** + * The ConcurrentScheduler object is used to schedule tasks + * for execution. This queues the task for the requested period of + * time before it is executed. It ensures that the delay is adhered + * to such that tasks can be timed for execution in an accurate way. + * + * @author Niall Gallagher + */ +public class ConcurrentScheduler implements Scheduler { + + /** + * This is the scheduler queue used to enque tasks to execute. + */ + private final SchedulerQueue queue; + + /** + * Constructor for the ConcurrentScheduler object. + * This will create a scheduler with a fixed number of threads to + * use before execution. Depending on the types of task that are + * to be executed this should be increased for accuracy. + * + * @param type this is the type of the worker threads + */ + public ConcurrentScheduler(Class type) { + this(type, 10); + } + + /** + * Constructor for the ConcurrentScheduler object. + * This will create a scheduler with a fixed number of threads to + * use before execution. Depending on the types of task that are + * to be executed this should be increased for accuracy. + * + * @param type this is the type of the worker threads + * @param size this is the number of threads for the scheduler + */ + public ConcurrentScheduler(Class type, int size) { + this.queue = new SchedulerQueue(type, size); + } + + /** + * This will execute the task within the executor immediately + * as it uses a delay duration of zero milliseconds. This can + * be used if the scheduler is to be used as a thread pool. + * + * @param task this is the task to schedule for execution + */ + public void execute(Runnable task) { + queue.execute(task); + } + + /** + * This will execute the task within the executor after the time + * specified has expired. If the time specified is zero then it + * will be executed immediately. Once the scheduler has been + * stopped then this method will no longer accept runnable tasks. + * + * @param task this is the task to schedule for execution + * @param delay the time in milliseconds to wait for execution + */ + public void execute(Runnable task, long delay) { + execute(task, delay, TimeUnit.MILLISECONDS); + } + + /** + * This will execute the task within the executor after the time + * specified has expired. If the time specified is zero then it + * will be executed immediately. Once the scheduler has been + * stopped then this method will no longer accept runnable tasks. + * + * @param task this is the task to schedule for execution + * @param delay this is the delay to wait before execution + * @param unit this is the duration time unit to wait for + */ + public void execute(Runnable task, long delay, TimeUnit unit) { + queue.execute(task, delay, unit); + } + + /** + * This is used to stop the scheduler by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return immediately once it has been stopped, and not further + * tasks will be accepted by this pool for execution. + */ + public void stop() { + stop(60000); + } + + /** + * This is used to stop the scheduler by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return once it has been stopped, and no further tasks will be + * accepted by this pool for execution. + * + * @param wait the number of milliseconds to wait for it to stop + */ + public void stop(long wait) { + queue.stop(wait); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/Daemon.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/Daemon.java new file mode 100644 index 0000000..3b7b5bf --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/Daemon.java @@ -0,0 +1,164 @@ +/* + * Daemon.java February 2009 + * + * Copyright (C) 2009, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * The Daemon object provides a named thread which will + * execute the run method when started. This offers + * some convenience in that it hides the normal thread methods and + * also allows the object extending this to provide the name of the + * internal thread, which is given an incrementing sequence number + * appended to the name provided. + * + * @author Niall Gallagher + */ +public abstract class Daemon implements Runnable { + + /** + * This is the current thread executing this service. + */ + private final AtomicReference reference; + + /** + * This is the internal thread used by this daemon instance. + */ + private final DaemonFactory factory; + + /** + * This is used to determine if the daemon is active. + */ + private final AtomicBoolean active; + + /** + * This is the internal thread that is executed by this. + */ + private final Runnable delegate; + + /** + * Constructor for the Daemon object. This will + * create the internal thread and ensure it is a daemon. When it + * is started the name of the internal thread is set using the + * name of the instance as taken from getName. If + * the name provided is null then no name is set for the thread. + */ + protected Daemon() { + this.reference = new AtomicReference(); + this.delegate = new RunnableDelegate(this); + this.factory = new DaemonFactory(); + this.active = new AtomicBoolean(); + } + + /** + * This is used to determine if the runner is active. If it is not + * active then it is assumed that no thread is executing. Also, if + * this is extended then any executing thread to stop as soon as + * this method returns false. + * + * @return this returns true if the runner is active + */ + public boolean isActive() { + return active.get(); + } + + /** + * This is used to start the internal thread. Once started the + * internal thread will execute the run method of + * this instance. Aside from starting the thread this will also + * ensure the internal thread has a unique name. + */ + public void start() { + Class type = getClass(); + + if (!active.get()) { + Thread thread = factory.newThread(delegate, type); + + reference.set(thread); + active.set(true); + thread.start(); + } + } + + /** + * This is used to interrupt the internal thread. This is used + * when there is a need to wake the thread from a sleeping or + * waiting state so that some other operation can be performed. + * Typically this is required when killing the thread. + */ + public void interrupt() { + Thread thread = reference.get(); + + if(thread != null) { + thread.interrupt(); + } + } + + /** + * This method is used to stop the thread without forcing it to + * stop. It acts as a means to deactivate it. It is up to the + * implementor to ensure that the isActive method + * is checked to determine whether it should continue to run. + */ + public void stop() { + active.set(false); + } + + /** + * The RunnableDelegate object is used to actually + * invoke the run method. A delegate is used to ensure + * that once the task has finished it is inactive so that it can + * be started again with a new thread. + */ + private class RunnableDelegate implements Runnable { + + /** + * This is the runnable that is to be executed. + */ + private final Runnable task; + + /** + * Constructor for the RunnableDelegate object. The + * delegate requires the actual runnable that is to be executed. + * As soon as the task has finished the runner becomes inactive. + * + * @param task this is the task to be executed + */ + public RunnableDelegate(Runnable task) { + this.task = task; + } + + /** + * This is used to execute the task. Once the task has finished + * the runner becomes inactive and any reference to the internal + * thread is set to null. This ensures the runner can be started + * again at a later time if desired. + */ + public void run() { + try { + task.run(); + } finally { + reference.set(null); + active.set(false); + } + } + + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/DaemonFactory.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/DaemonFactory.java new file mode 100644 index 0000000..d5da16a --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/DaemonFactory.java @@ -0,0 +1,147 @@ +/* + * DaemonFactory.java February 2009 + * + * Copyright (C) 2009, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import java.util.concurrent.ThreadFactory; + +/** + * The DaemonFactory object is used to build threads + * and prefix the thread with a type name. Prefixing the threads with + * the type that it represents allows the purpose of the thread to + * be determined and also provides better debug information. + * + * @author Niall Gallagher + */ +public class DaemonFactory implements ThreadFactory { + + /** + * This is the type of the task this pool will execute. + */ + private final Class type; + + /** + * Constructor for the DaemonFactory object. This + * will provide a thread factory that names the threads based + * on the type of Runnable the pool executes. + */ + public DaemonFactory() { + this(null); + } + + /** + * Constructor for the DaemonFactory object. This + * will provide a thread factory that names the threads based + * on the type of Runnable the pool executes. Each + * of the threads is given a unique sequence number. + * + * @param type this is the type of runnable this will execute + */ + public DaemonFactory(Class type) { + this.type = type; + } + + /** + * This is used to create a thread from the provided runnable. The + * thread created will contain a unique name which is prefixed with + * the type of task it has been created to execute. This provides + * some detail as to what the thread should be doing. + * + * @param task this is the task that the thread is to execute + * + * @return this returns a thread that will executed the given task + */ + public Thread newThread(Runnable task) { + Thread thread = newThread(task, type); + String name = createName(task, thread); + + if(!thread.isAlive()) { + thread.setName(name); + } + return thread; + } + + /** + * This is used to create a thread from the provided runnable. The + * thread created will contain a unique name which is prefixed with + * the type of task it has been created to execute. This provides + * some detail as to what the thread should be doing. + * + * @param task this is the task that the thread is to execute + * @param type this is the type of object the thread is to execute + * + * @return this returns a thread that will executed the given task + */ + public Thread newThread(Runnable task, Class type) { + Thread thread = createThread(task); + String name = createName(type, thread); + + if(!thread.isAlive()) { + thread.setName(name); + } + return thread; + } + + /** + * This will create a thread name that is unique. The thread name + * is a combination of the original thread name with a prefix + * of the type of the object that will be running within it. + * + * @param task this is the task to be run within the thread + * @param thread this is the thread containing the original name + * + * @return this will return the new name of the thread + */ + private String createName(Runnable task, Thread thread) { + Class type = task.getClass(); + String prefix = type.getSimpleName(); + String name = thread.getName(); + + return String.format("%s: %s", prefix, name); + } + + /** + * This will create a thread name that is unique. The thread name + * is a combination of the original thread name with a prefix + * of the type of the object that will be running within it. + * + * @param type this is the type of object to be executed + * @param thread this is the thread containing the original name + * + * @return this will return the new name of the thread + */ + private String createName(Class type, Thread thread) { + String prefix = type.getSimpleName(); + String name = thread.getName(); + + return String.format("%s: %s", prefix, name); + } + + /** + * This is used to create the thread that will be used to execute + * the provided task. The created thread will be renamed after + * it has been created and before it has been started. + * + * @param task this is the task that is to be executed + * + * @return this returns a thread to execute the given task + */ + private Thread createThread(Runnable task) { + return new Thread(task); + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/ExecutorQueue.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/ExecutorQueue.java new file mode 100644 index 0000000..99e8fb2 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/ExecutorQueue.java @@ -0,0 +1,128 @@ +/* + * ExecutorQueue.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * The ExecutorQueue object is used to queue tasks in + * a thread pool. This creates a thread pool with no limit to the + * number of tasks that can be enqueued, which ensures that any + * system requesting a task to be executed will not block when + * handing it over, it also means the user must use caution. + * + * @author Niall Gallagher + * + * @see org.simpleframework.common.thread.ConcurrentExecutor + */ +class ExecutorQueue { + + /** + * This is the task queue that contains tasks due to execute. + */ + private final BlockingQueue queue; + + /** + * This is the actual thread pool implementation used. + */ + private final ThreadPoolExecutor executor; + + /** + * This is used to create the pool worker threads. + */ + private final ThreadFactory factory; + + /** + * Constructor for the ExecutorQueue object. This is + * used to create a pool of threads that can be used to execute + * arbitrary Runnable tasks. If the threads are + * busy this will simply enqueue the tasks and return. + * + * @param type this is the type of runnable that this accepts + * @param rest this is the number of threads to use in the pool + * @param active this is the maximum size the pool can grow to + */ + public ExecutorQueue(Class type, int rest, int active) { + this(type, rest, active, 120, TimeUnit.SECONDS); + } + + /** + * Constructor for the ExecutorQueue object. This is + * used to create a pool of threads that can be used to execute + * arbitrary Runnable tasks. If the threads are + * busy this will simply enqueue the tasks and return. + * + * @param type this is the type of runnable that this accepts + * @param rest this is the number of threads to use in the pool + * @param active this is the maximum size the pool can grow to + * @param duration the duration active threads remain idle for + * @param unit this is the time unit used for the duration + */ + public ExecutorQueue(Class type, int rest, int active, long duration, TimeUnit unit) { + this.queue = new LinkedBlockingQueue(); + this.factory = new DaemonFactory(type); + this.executor = new ThreadPoolExecutor(rest, active, duration, unit, queue, factory); + } + + /** + * The execute method is used to queue the task for + * execution. If all threads are busy the provided task is queued + * and waits until all current and outstanding tasks are finished. + * + * @param task this is the task to be queued for execution + */ + public void execute(Runnable task) { + executor.execute(task); + } + + /** + * This is used to stop the executor by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return once it has been stopped, and no further tasks will be + * accepted by this pool for execution. + */ + public void stop() { + stop(60000); + } + + /** + * This is used to stop the executor by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return once it has been stopped, and no further tasks will be + * accepted by this pool for execution. + * + * @param wait the number of milliseconds to wait for it to stop + */ + public void stop(long wait) { + if(!executor.isTerminated()) { + try { + executor.shutdown(); + executor.awaitTermination(wait, MILLISECONDS); + } catch(Exception e) { + throw new IllegalStateException("Could not stop pool", e); + } + } + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/Scheduler.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/Scheduler.java new file mode 100644 index 0000000..d24fa17 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/Scheduler.java @@ -0,0 +1,57 @@ +/* + * Scheduler.java October 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * The Scheduler interface represents a means to execute + * a task immediately or after a specified delay. This queues the + * task for the requested period of time before it is executed, if a + * delay is specified. How the task is executed is dependent on the + * implementation, however it will normally use a thread pool. + * + * @author Niall Gallagher + */ +public interface Scheduler extends Executor { + + /** + * This will execute the task within the executor after the time + * specified has expired. If the time specified is zero then it + * will be executed immediately. Once the scheduler has been + * stopped then this method will no longer accept runnable tasks. + * + * @param task this is the task to schedule for execution + * @param delay the time in milliseconds to wait for execution + */ + void execute(Runnable task, long delay); + + /** + * This will execute the task within the executor after the time + * specified has expired. If the time specified is zero then it + * will be executed immediately. Once the scheduler has been + * stopped then this method will no longer accept runnable tasks. + * + * @param task this is the task to schedule for execution + * @param delay this is the delay to wait before execution + * @param unit this is the duration time unit to wait for + */ + void execute(Runnable task, long delay, TimeUnit unit); +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/SchedulerQueue.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/SchedulerQueue.java new file mode 100644 index 0000000..67385ed --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/SchedulerQueue.java @@ -0,0 +1,127 @@ +/* + * SchedulerQueue.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * The SchedulerQueue object is used to schedule tasks + * for execution. This queues the task for the requested period of + * time before it is executed. It ensures that the delay is adhered + * to such that tasks can be timed for execution in an accurate way. + * + * @author Niall Gallagher + */ +class SchedulerQueue { + + /** + * This is the actual scheduler used to schedule the tasks. + */ + private final ScheduledThreadPoolExecutor executor; + + /** + * This is the factory used to create the worker threads. + */ + private final ThreadFactory factory; + + /** + * Constructor for the SchedulerQueue object. This + * will create a scheduler with a fixed number of threads to use + * before execution. Depending on the types of task that are + * to be executed this should be increased for accuracy. + * + * @param type this is the type of task to execute + * @param size this is the number of threads for the scheduler + */ + public SchedulerQueue(Class type, int size) { + this.factory = new DaemonFactory(type); + this.executor = new ScheduledThreadPoolExecutor(size, factory); + } + + /** + * The execute method is used to queue the task for + * execution. If all threads are busy the provided task is queued + * and waits until all current and outstanding tasks are finished. + * + * @param task this is the task to be queued for execution + */ + public void execute(Runnable task) { + executor.execute(task); + } + + /** + * This will execute the task within the executor after the time + * specified has expired. If the time specified is zero then it + * will be executed immediately. Once the scheduler has been + * stopped then this method will no longer accept runnable tasks. + * + * @param task this is the task to schedule for execution + * @param delay the time in milliseconds to wait for execution + */ + public void execute(Runnable task, long delay) { + execute(task, delay, TimeUnit.MILLISECONDS); + } + + /** + * This will execute the task within the executor after the time + * specified has expired. If the time specified is zero then it + * will be executed immediately. Once the scheduler has been + * stopped then this method will no longer accept runnable tasks. + * + * @param task this is the task to schedule for execution + * @param delay this is the delay to wait before execution + * @param unit this is the duration time unit to wait for + */ + public void execute(Runnable task, long delay, TimeUnit unit) { + executor.schedule(task, delay, unit); + } + + /** + * This is used to stop the executor by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return once it has been stopped, and no further tasks will be + * accepted by this pool for execution. + */ + public void stop() { + stop(60000); + } + + /** + * This is used to stop the executor by interrupting all running + * tasks and shutting down the threads within the pool. This will + * return once it has been stopped, and no further tasks will be + * accepted by this pool for execution. + * + * @param wait the number of milliseconds to wait for it to stop + */ + public void stop(long wait) { + if(!executor.isTerminated()) { + try { + executor.shutdown(); + executor.awaitTermination(wait, MILLISECONDS); + } catch(Exception e) { + throw new IllegalStateException("Could not stop pool", e); + } + } + } +} diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/thread/SynchronousExecutor.java b/simple/simple-common/src/main/java/org/simpleframework/common/thread/SynchronousExecutor.java new file mode 100644 index 0000000..decd41d --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/thread/SynchronousExecutor.java @@ -0,0 +1,43 @@ +/* + * SynchronousExecutor.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common.thread; + +import java.util.concurrent.Executor; + +/** + * The SynchronousExecutor object is used for synchronous + * execution of tasks. This simple acts as an adapter for running + * a Runnable implementation and can be used wherever + * the executor interface is required. + * + * @author Niall Gallagher + */ +public class SynchronousExecutor implements Executor { + + /** + * This will execute the provided Runnable within + * the current thread. This implementation will simple invoke + * the run method of the task and wait for it to complete. + * + * @param task this is the task that is to be executed + */ + public void execute(Runnable task) { + task.run(); + } +} \ No newline at end of file diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/KeyTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/KeyTest.java new file mode 100644 index 0000000..f9056fe --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/KeyTest.java @@ -0,0 +1,195 @@ +package org.simpleframework.common; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * Test for fast case insensitive mapping for headers that have been taken + * from the request HTTP header or added to the response HTTP header. + * + * @author Niall Gallagher + */ +public class KeyTest extends TestCase { + + public class Index implements Name { + + private final String value; + + public Index(String value) { + this.value = value.toLowerCase(); + } + + public int hashCode() { + return value.hashCode(); + } + + public boolean equals(Object key) { + if(key instanceof Name) { + return key.equals(value); + } + if(key instanceof String) { + return key.equals(value); + } + return false; + } + } + + public interface Name { + + public int hashCode(); + public boolean equals(Object value); + } + + public class ArrayName implements Name { + + private String cache; + private byte[] array; + private int off; + private int size; + private int hash; + + public ArrayName(byte[] array) { + this(array, 0, array.length); + } + + public ArrayName(byte[] array, int off, int size) { + this.array = array; + this.size = size; + this.off = off; + } + + public boolean equals(Object value) { + if(value instanceof String) { + String text = value.toString(); + + return equals(text); + } + return false; + } + + public boolean equals(String value) { + int length = value.length(); + + if(length != size) { + return false; + } + for(int i = 0; i < size; i++) { + int left = value.charAt(i); + int right = array[off + i]; + + if(right >= 'A' && right <= 'Z') { + right = (right - 'A') + 'a'; + } + if(left != right) { + return false; + } + } + return true; + } + + public int hashCode() { + int code = hash; + + if(code == 0) { + int pos = off; + + for(int i = 0; i < size; i++) { + int next = array[pos++]; + + if(next >= 'A' && next <= 'Z') { + next = (next - 'A') + 'a'; + } + code = 31*code + next; + } + hash = code; + } + return code; + } + } + + public class StringName implements Name { + + private final String value; + private final String key; + + public StringName(String value) { + this.key = value.toLowerCase(); + this.value = value; + } + + public int hashCode() { + return key.hashCode(); + } + + public boolean equals(Object value) { + return value.equals(key); + } + } + + public class NameTable { + + private final Map map; + + public NameTable() { + this.map = new HashMap(); + } + + public void put(Name key, T value) { + map.put(key, value); + } + + public void put(String text, T value) { + Name key = new StringName(text); + + map.put(key, value); + } + + public T get(String key) { + Index index = new Index(key); + + return map.get(index); + } + + public T remove(String key) { + Index index = new Index(key); + + return map.remove(index); + } + } + + public void testName() { + Name contentLength = new ArrayName("Content-Length".getBytes()); + Name contentType = new ArrayName("Content-Type".getBytes()); + Name transferEncoding = new ArrayName("Transfer-Encoding".getBytes()); + Name userAgent = new ArrayName("User-Agent".getBytes()); + NameTable map = new NameTable(); + + assertEquals(contentLength.hashCode(), "Content-Length".toLowerCase().hashCode()); + assertEquals(contentType.hashCode(), "Content-Type".toLowerCase().hashCode()); + assertEquals(transferEncoding.hashCode(), "Transfer-Encoding".toLowerCase().hashCode()); + assertEquals(userAgent.hashCode(), "User-Agent".toLowerCase().hashCode()); + + map.put(contentLength, "1024"); + map.put(contentType, "text/html"); + map.put(transferEncoding, "chunked"); + map.put(userAgent, "Mozilla/4.0"); + map.put("Date", "18/11/1977"); + map.put("Accept", "text/plain, text/html, image/gif"); + + assertEquals(map.get("Content-Length"), "1024"); + assertEquals(map.get("CONTENT-LENGTH"), "1024"); + assertEquals(map.get("content-length"), "1024"); + assertEquals(map.get("Content-length"), "1024"); + assertEquals(map.get("Content-Type"), "text/html"); + assertEquals(map.get("Transfer-Encoding"), "chunked"); + assertEquals(map.get("USER-AGENT"), "Mozilla/4.0"); + assertEquals(map.get("Accept"), "text/plain, text/html, image/gif"); + assertEquals(map.get("ACCEPT"), "text/plain, text/html, image/gif"); + assertEquals(map.get("accept"), "text/plain, text/html, image/gif"); + assertEquals(map.get("DATE"), "18/11/1977"); + assertEquals(map.get("Date"), "18/11/1977"); + assertEquals(map.get("date"), "18/11/1977"); + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/ArrayBufferTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/ArrayBufferTest.java new file mode 100644 index 0000000..1f6f494 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/ArrayBufferTest.java @@ -0,0 +1,54 @@ +package org.simpleframework.common.buffer; + +import org.simpleframework.common.buffer.ArrayBuffer; +import org.simpleframework.common.buffer.Buffer; + +import junit.framework.TestCase; + +public class ArrayBufferTest extends TestCase { + + public void testBuffer() throws Exception { + Buffer buffer = new ArrayBuffer(1, 2); + + buffer.append(new byte[]{'a'}).append(new byte[]{'b'}); + + assertEquals(buffer.encode(), "ab"); + assertEquals(buffer.encode("ISO-8859-1"), "ab"); + + boolean overflow = false; + + try { + buffer.append(new byte[]{'c'}); + } catch(Exception e) { + overflow = true; + } + assertTrue(overflow); + + buffer.clear(); + + assertEquals(buffer.encode(), ""); + assertEquals(buffer.encode("UTF-8"), ""); + + buffer = new ArrayBuffer(1024, 2048); + buffer.append("abcdefghijklmnopqrstuvwxyz".getBytes()); + + Buffer alphabet = buffer.allocate(); + alphabet.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes()); + + Buffer digits = buffer.allocate(); + digits.append("0123456789".getBytes()); + + assertEquals(alphabet.encode(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + assertEquals(digits.encode(), "0123456789"); + assertEquals(buffer.encode(), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + + Buffer extra = digits.allocate(); + extra.append("#@?".getBytes()); + + assertEquals(extra.encode(), "#@?"); + assertEquals(digits.encode(), "0123456789#@?"); + assertEquals(buffer.encode(), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#@?"); + assertEquals(buffer.length(), 65); + } + +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/BufferAllocatorTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/BufferAllocatorTest.java new file mode 100644 index 0000000..9584047 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/BufferAllocatorTest.java @@ -0,0 +1,79 @@ +package org.simpleframework.common.buffer; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.common.buffer.BufferAllocator; + +import junit.framework.TestCase; + +public class BufferAllocatorTest extends TestCase { + + public void testBuffer() throws Exception { + Allocator allocator = new ArrayAllocator(1, 2); + Buffer buffer = new BufferAllocator(allocator, 1, 2); + + buffer.append(new byte[]{'a'}).append(new byte[]{'b'}); + + assertEquals(buffer.encode(), "ab"); + assertEquals(buffer.encode("ISO-8859-1"), "ab"); + + boolean overflow = false; + + try { + buffer.append(new byte[]{'c'}); + } catch(Exception e) { + overflow = true; + } + assertTrue(overflow); + + buffer.clear(); + + assertEquals(buffer.encode(), ""); + assertEquals(buffer.encode("UTF-8"), ""); + + allocator = new ArrayAllocator(1024, 2048); + buffer = new BufferAllocator(allocator, 1024, 2048); + buffer.append("abcdefghijklmnopqrstuvwxyz".getBytes()); + + Buffer alphabet = buffer.allocate(); + alphabet.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes()); + + Buffer digits = buffer.allocate(); + digits.append("0123456789".getBytes()); + + assertEquals(alphabet.encode(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + assertEquals(digits.encode(), "0123456789"); + assertEquals(buffer.encode(), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + + Buffer extra = digits.allocate(); + extra.append("#@?".getBytes()); + + assertEquals(extra.encode(), "#@?"); + assertEquals(extra.length(), 3); + assertEquals(digits.encode(), "0123456789#@?"); + assertEquals(digits.length(), 13); + assertEquals(buffer.encode(), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#@?"); + assertEquals(buffer.length(), 65); + } + + public void testCascadingBufferAllocator() throws Exception { + Allocator allocator = new ArrayAllocator(1024, 2048); + allocator = new BufferAllocator(allocator, 1024, 2048); + allocator = new BufferAllocator(allocator, 1024, 2048); + allocator = new BufferAllocator(allocator, 1024, 2048); + allocator = new BufferAllocator(allocator, 1024, 2048); + + Buffer buffer = allocator.allocate(1024); + + buffer.append("abcdefghijklmnopqrstuvwxyz".getBytes()); + + assertEquals(buffer.encode(), "abcdefghijklmnopqrstuvwxyz"); + + buffer.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes()); + + assertEquals(buffer.encode(), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); + assertEquals(buffer.length(), 52); + } + +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileBufferTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileBufferTest.java new file mode 100644 index 0000000..54cb142 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileBufferTest.java @@ -0,0 +1,45 @@ +package org.simpleframework.common.buffer; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.common.buffer.FileBuffer; + +import junit.framework.TestCase; + +public class FileBufferTest extends TestCase { + + public void testFileBuffer() throws Exception { + File tempFile = File.createTempFile(FileBufferTest.class.getSimpleName(), null); + Buffer buffer = new FileBuffer(tempFile); + buffer.append("abcdefghijklmnopqrstuvwxyz".getBytes()); + + Buffer alphabet = buffer.allocate(); + alphabet.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes()); + + Buffer digits = buffer.allocate(); + digits.append("0123456789".getBytes()); + + expect(buffer, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".getBytes()); + expect(alphabet, "ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes()); + expect(digits, "0123456789".getBytes()); + } + + private void expect(Buffer buffer, byte[] expect) throws IOException { + InputStream result = buffer.open(); + + for(int i =0; i < expect.length; i++) { + byte octet = expect[i]; + int value = result.read(); + + if(value < 0) { + throw new IOException("Buffer exhausted too early"); + } + assertEquals(octet, (byte)value); + } + assertEquals(-1, result.read()); + } + +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueue.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueue.java new file mode 100644 index 0000000..b404562 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueue.java @@ -0,0 +1,109 @@ +package org.simpleframework.common.buffer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.common.buffer.BufferAllocator; + +public class FileByteQueue { + + private BlockingQueue blocks; + private BlockAllocator allocator; + private Block source; + + public FileByteQueue(Allocator allocator) throws IOException { + this.blocks = new LinkedBlockingQueue(); + this.allocator = new BlockAllocator(allocator); + } + + public int read(byte[] array, int off, int size) throws Exception { + int left = blocks.size(); + int mark = size; + + for(int i = 0; source != null || i < left; i++) { + if(source == null) { + source = blocks.take(); + } + int remain = source.remaining(); + int read = Math.min(remain, size); + + if(read > 0) { + source.read(array, off, size); + size -= read; + off += read; + } + if(remain == 0) { + source.close(); // clear up file handles + source = null; + } + if(size <= 0) { + return mark; + } + } + return mark - size; + } + + public void write(byte[] array, int off, int size) throws Exception { + Block buffer = allocator.allocate(array, off, size); + + if(size > 0) { + blocks.offer(buffer); + } + } + + private class BlockAllocator { + + private Allocator allocator; + + public BlockAllocator(Allocator allocator) { + this.allocator = new BufferAllocator(allocator); + } + + public Block allocate(byte[] array, int off, int size) throws IOException { + Buffer buffer = allocator.allocate(); + + if(size > 0) { + buffer.append(array, off, size); + } + return new Block(buffer, size); + } + } + + private class Block { + + private InputStream source; + private int remaining; + private int size; + + public Block(Buffer buffer, int size) throws IOException { + this.source = buffer.open(); + this.remaining = size; + this.size = size; + } + + public int read(byte[] array, int off, int size) throws IOException { + int count = source.read(array, off, size); + + if(count > 0) { + remaining -= size; + } + return count; + } + + public void close() throws IOException { + source.close(); + } + + public int remaining() { + return remaining; + } + + public int size() { + return size; + } + } +} \ No newline at end of file diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueueTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueueTest.java new file mode 100644 index 0000000..9699454 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueueTest.java @@ -0,0 +1,22 @@ +package org.simpleframework.common.buffer; + +import junit.framework.TestCase; + +public class FileByteQueueTest extends TestCase { + + public void testQueue() throws Exception { + /* Allocator allocator = new FileAllocator(); + FileByteQueue queue = new FileByteQueue(allocator); + for(int i = 0; i < 26; i++) { + queue.write(new byte[]{(byte)(i+'a')}, 0, 1); + System.err.println("WRITE>>"+(char)(i+'a')); + } + for(int i = 0; i < 26; i++) { + byte[] buffer = new byte[1]; + assertEquals(queue.read(buffer, 0, 1), 1); + System.err.println("READ>>"+((char)buffer[0])); + assertEquals(buffer[0], (byte)(i+'a')); + }*/ + } + +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueue.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueue.java new file mode 100644 index 0000000..893ae80 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueue.java @@ -0,0 +1,100 @@ +package org.simpleframework.common.buffer.queue; + +import java.io.IOException; + +import org.simpleframework.common.buffer.BufferException; + +public class ArrayByteQueue implements ByteQueue { + + private byte[] buffer; + private int limit; + private int count; + private int seek; + private boolean closed; + + public ArrayByteQueue(int limit) { + this.buffer = new byte[16]; + this.limit = limit; + } + + public synchronized void write(byte[] array) throws IOException { + write(array, 0, array.length); + } + + public synchronized void write(byte[] array, int off, int size) throws IOException { + if(closed) { + throw new BufferException("Queue has been closed"); + } + if (size + count > buffer.length) { + expand(count + size); + } + int fragment = buffer.length - seek; // from read pos to end + int space = fragment - count; // space at end + + if(space >= size) { + System.arraycopy(array, off, buffer, seek + count, size); + } else { + int chunk = Math.min(fragment, count); + + System.arraycopy(buffer, seek, buffer, 0, chunk); // adjust downward + System.arraycopy(array, off, buffer, chunk, size); + seek = 0; + } + notify(); + count += size; + } + + public synchronized int read(byte[] array) throws IOException { + return read(array, 0, array.length); + } + + public synchronized int read(byte[] array, int off, int size) throws IOException { + while(count == 0) { + try { + if(closed) { + return -1; + } + wait(); + } catch(Exception e) { + throw new BufferException("Thread interrupted", e); + } + } + int chunk = Math.min(size, count); + + if(chunk > 0) { + System.arraycopy(buffer, seek, array, off, chunk); + seek += chunk; + count -= chunk; + } + return chunk; + } + + private synchronized void expand(int capacity) throws IOException { + if (capacity > limit) { + throw new BufferException("Capacity limit %s exceeded", limit); + } + int resize = buffer.length * 2; + int size = Math.max(capacity, resize); + byte[] temp = new byte[size]; + + System.arraycopy(buffer, seek, temp, 0, count); + buffer = temp; + seek = 0; + } + + public synchronized void reset() throws IOException { + if(closed) { + throw new BufferException("Queue has been closed"); + } + seek = 0; + count = 0; + } + + public synchronized int available() { + return count; + } + + public synchronized void close() { + closed = true; + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueueTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueueTest.java new file mode 100644 index 0000000..7d361f3 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueueTest.java @@ -0,0 +1,119 @@ +package org.simpleframework.common.buffer.queue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import junit.framework.TestCase; + +public class ArrayByteQueueTest extends TestCase { + + public void testArrayByteQueue() throws Exception { + ArrayByteQueue queue = new ArrayByteQueue(10); + + for(int i = 0; i < 9; i++) { + queue.write(new byte[]{(byte)('A'+i)}); + } + for(int i = 0; i < 9; i++) { + byte[] b = new byte[1]; + queue.read(b); + System.err.write(b); + System.err.println(); + } + for(int i = 9; i < 19; i++) { + queue.write(new byte[]{(byte)('A'+i)}); + } + for(int i = 0; i < 9; i++) { + byte[] b = new byte[1]; + queue.read(b); + System.err.write(b); + System.err.println(); + } + } + + public void testRandomReadWrite() throws Exception { + ArrayByteQueue queue = new ArrayByteQueue(1024 * 10); + + for(int i = 0; i < 100; i++) { + String text = "Test: "+i; + queue.write(text.getBytes()); + } + for(int i = 0; i < 100; i++) { + String text = "Test: "+i; + byte[] buffer = new byte[256]; + int size = queue.read(buffer, 0, text.length()); + String result = new String(buffer, 0, size); + System.err.println(result); + assertEquals(result, text); + } + } + /* + public void testStream() throws Exception { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final ArrayByteQueue queue = new ArrayByteQueue(1024 * 10); + final Thread reader = new Thread(new Runnable() { + public void run() { + try { + for(int i = 0; i < 100; i++) { + byte[] chunk = new byte[(int)Math.round((Math.random() * 100))]; + int size = queue.read(chunk); + output.write(chunk, 0, size); + } + } catch(Exception e) { + e.printStackTrace(); + } + } + }); + final Thread writer = new Thread(new Runnable() { + public void run() { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutputStream objectOutput = new ObjectOutputStream(buffer); + + for(int i = 0; i < 100; i++) { + try { + TestMessage message = new TestMessage(i, "Test Message: " +i); + objectOutput.writeObject(message); + objectOutput.flush(); + byte[] messageBytes = buffer.toByteArray(); + queue.write(messageBytes); + buffer.reset(); // clear out the buffer so toByteArray picks up changes only + } catch(Exception e) { + e.printStackTrace(); + } + } + }catch(Exception e){ + e.printStackTrace(); + } + } + }); + writer.start(); + reader.start(); + writer.join(); + Thread.sleep(5000); + reader.interrupt(); + reader.join(); + + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + ObjectInputStream objectInput = new ObjectInputStream(input); + + for(int i = 0; i < 100; i++) { + TestMessage message = (TestMessage)objectInput.readObject(); + assertEquals(message.count, i); + assertEquals(message.text, "Test Message: "+i); + } + } +*/ + private static class TestMessage implements Serializable { + + public final int count; + public final String text; + + public TestMessage(int count, String text) { + this.count = count; + this.text = text; + } + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueue.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueue.java new file mode 100644 index 0000000..5f3e97f --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueue.java @@ -0,0 +1,67 @@ +package org.simpleframework.common.buffer.queue; + +import java.io.IOException; +import java.io.InputStream; + +import org.simpleframework.common.buffer.ArrayBuffer; +import org.simpleframework.common.buffer.Buffer; + +public class BufferQueue implements Buffer { + + private final ByteQueue queue; + private final Buffer buffer; + + public BufferQueue(ByteQueue queue) { + this.buffer = new ArrayBuffer(); + this.queue = queue; + } + + public InputStream open() throws IOException { + return new ByteQueueStream(queue); + } + + public Buffer allocate() throws IOException { + return new BufferQueue(queue); + } + + public String encode() throws IOException { + return encode("UTF-8"); + } + + public String encode(String charset) throws IOException { + InputStream source = open(); + byte[] chunk = new byte[512]; + int count = 0; + + while((count = source.read(chunk)) != -1) { + buffer.append(chunk, 0, count); + } + return buffer.encode(charset); + } + + public Buffer append(byte[] array) throws IOException { + if(array.length > 0) { + queue.write(array); + } + return this; + } + + public Buffer append(byte[] array, int off, int len) throws IOException { + if(len > 0) { + queue.write(array, off, len); + } + return this; + } + + public void clear() throws IOException { + queue.reset(); + } + + public void close() throws IOException { + queue.close(); + } + + public long length() { + return buffer.length(); + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueueTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueueTest.java new file mode 100644 index 0000000..22eaba7 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueueTest.java @@ -0,0 +1,44 @@ +package org.simpleframework.common.buffer.queue; + +import java.io.InputStream; + +import junit.framework.TestCase; + +public class BufferQueueTest extends TestCase { + + public void testBufferQueue() throws Exception { + final ByteQueue queue = new ArrayByteQueue(1024 * 1000); + final BufferQueue buffer = new BufferQueue(queue); + + Thread reader = new Thread(new Runnable() { + public void run() { + try { + InputStream source = buffer.open(); + for(int i = 0; i < 1000; i++) { + int octet = source.read(); + System.err.write(octet); + System.err.flush(); + } + }catch(Exception e) { + e.printStackTrace(); + } + } + }); + Thread writer = new Thread(new Runnable() { + public void run() { + try { + for(int i = 0; i < 1000; i++) { + buffer.append(("Test message: "+i+"\n").getBytes()); + } + }catch(Exception e) { + e.printStackTrace(); + } + } + }); + reader.start(); + writer.start(); + reader.join(); + writer.join(); + } + +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueue.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueue.java new file mode 100644 index 0000000..dc567e9 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueue.java @@ -0,0 +1,13 @@ +package org.simpleframework.common.buffer.queue; + +import java.io.IOException; + +public interface ByteQueue { + void write(byte[] array) throws IOException; + void write(byte[] array, int off, int size) throws IOException; + int read(byte[] array) throws IOException; + int read(byte[] array, int off, int size) throws IOException; + int available() throws IOException; + void reset() throws IOException; + void close() throws IOException; +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueueStream.java b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueueStream.java new file mode 100644 index 0000000..dbf73e1 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueueStream.java @@ -0,0 +1,40 @@ +package org.simpleframework.common.buffer.queue; + +import java.io.IOException; +import java.io.InputStream; + +public class ByteQueueStream extends InputStream { + + private final ByteQueue queue; + + public ByteQueueStream(ByteQueue queue) { + this.queue = queue; + } + + @Override + public int read() throws IOException { + byte[] array = new byte[1]; + int count = read(array) ; + + if(count != -1) { + return array[0] & 0xff; + } + return -1; + } + + public int read(byte[] buffer) throws IOException { + return queue.read(buffer, 0, buffer.length); + } + + public int read(byte[] buffer, int off, int size) throws IOException { + return queue.read(buffer, off, size); + } + + public int available() throws IOException { + return queue.available(); + } + + public void close() throws IOException { + queue.close(); + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractQueueTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractQueueTest.java new file mode 100644 index 0000000..6fa55c1 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractQueueTest.java @@ -0,0 +1,57 @@ +package org.simpleframework.common.lease; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.simpleframework.common.lease.Contract; +import org.simpleframework.common.lease.ContractQueue; +import org.simpleframework.common.lease.Expiration; + +public class ContractQueueTest extends TimeTestCase { + + public void testTimeUnits() throws Exception { + ContractQueue queue = new ContractQueue(); + List complete = new ArrayList(); + + for(long i = 0; i < 10000; i++) { + long random = (long)(Math.random() * 1000); + Contract contract = new Expiration(random, random, TimeUnit.NANOSECONDS); + + queue.offer(contract); + } + for(int i = 0; i < 10000; i++) { + Contract contract = queue.take(); + + assertGreaterThanOrEqual(contract.getDelay(TimeUnit.NANOSECONDS), contract.getDelay(TimeUnit.NANOSECONDS)); + assertGreaterThanOrEqual(contract.getDelay(TimeUnit.MILLISECONDS), contract.getDelay(TimeUnit.MILLISECONDS)); + assertGreaterThanOrEqual(contract.getDelay(TimeUnit.SECONDS), contract.getDelay(TimeUnit.SECONDS)); + + long nanoseconds = contract.getDelay(TimeUnit.NANOSECONDS); + long milliseconds = contract.getDelay(TimeUnit.MILLISECONDS); + + complete.add(String.format("index=[%s] nano=[%s] milli=[%s]", i, nanoseconds, milliseconds)); + } + for(int i = 0; i < 10000; i++) { + System.err.println("expiry=[" + complete.get(i)+ "]"); + } + } + + public void testAccuracy() throws Exception { + ContractQueue queue = new ContractQueue(); + + for(long i = 0; i < 10000; i++) { + long random = (long)(Math.random() * 1000); + Contract contract = new Expiration(random, random, TimeUnit.NANOSECONDS); + + queue.offer(contract); + } + for(int i = 0; i < 10000; i++) { + Contract contract = queue.take(); + + assertLessThanOrEqual(-2000, contract.getDelay(TimeUnit.MILLISECONDS)); + } + } + +} + diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractTest.java new file mode 100644 index 0000000..1cc40af --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractTest.java @@ -0,0 +1,40 @@ +package org.simpleframework.common.lease; + +import java.util.concurrent.TimeUnit; + +import org.simpleframework.common.lease.Contract; +import org.simpleframework.common.lease.Expiration; + +public class ContractTest extends TimeTestCase { + + public void testContract() throws Exception { + Contract ten = new Expiration(this, 10, TimeUnit.MILLISECONDS); + Contract twenty = new Expiration(this, 20, TimeUnit.MILLISECONDS); + Contract thirty= new Expiration(this, 30, TimeUnit.MILLISECONDS); + + assertGreaterThanOrEqual(twenty.getDelay(TimeUnit.NANOSECONDS), ten.getDelay(TimeUnit.NANOSECONDS)); + assertGreaterThanOrEqual(thirty.getDelay(TimeUnit.NANOSECONDS), twenty.getDelay(TimeUnit.NANOSECONDS)); + + assertGreaterThanOrEqual(twenty.getDelay(TimeUnit.MILLISECONDS), ten.getDelay(TimeUnit.MILLISECONDS)); + assertGreaterThanOrEqual(thirty.getDelay(TimeUnit.MILLISECONDS), twenty.getDelay(TimeUnit.MILLISECONDS)); + + ten.setDelay(0, TimeUnit.MILLISECONDS); + twenty.setDelay(0, TimeUnit.MILLISECONDS); + + assertLessThanOrEqual(ten.getDelay(TimeUnit.MILLISECONDS), 0); + assertLessThanOrEqual(twenty.getDelay(TimeUnit.MILLISECONDS), 0); + + ten.setDelay(10, TimeUnit.MILLISECONDS); + twenty.setDelay(20, TimeUnit.MILLISECONDS); + thirty.setDelay(30, TimeUnit.MILLISECONDS); + + assertGreaterThanOrEqual(twenty.getDelay(TimeUnit.NANOSECONDS), ten.getDelay(TimeUnit.NANOSECONDS)); + assertGreaterThanOrEqual(thirty.getDelay(TimeUnit.NANOSECONDS), twenty.getDelay(TimeUnit.NANOSECONDS)); + + assertGreaterThanOrEqual(twenty.getDelay(TimeUnit.MILLISECONDS), ten.getDelay(TimeUnit.MILLISECONDS)); + assertGreaterThanOrEqual(thirty.getDelay(TimeUnit.MILLISECONDS), twenty.getDelay(TimeUnit.MILLISECONDS)); + + ten.setDelay(0, TimeUnit.MILLISECONDS); + twenty.setDelay(0, TimeUnit.MILLISECONDS); + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseManagerTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseManagerTest.java new file mode 100644 index 0000000..9ce2b93 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseManagerTest.java @@ -0,0 +1,227 @@ +package org.simpleframework.common.lease; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.simpleframework.common.lease.Cleaner; +import org.simpleframework.common.lease.Lease; +import org.simpleframework.common.lease.LeaseManager; + +public class LeaseManagerTest extends TimeTestCase { + + private static int ITERATIONS = 1000; + private static int MAXIMUM = 20000; + + static { + String value = System.getProperty("iterations"); + + if (value != null) { + ITERATIONS = Integer.parseInt(value); + } + } + + public void testClock() { + List timeList = new ArrayList(); + + for(int i = 0; i < ITERATIONS; i++) { + long time = System.nanoTime(); + long milliseconds = TimeUnit.MILLISECONDS.convert(time, TimeUnit.MILLISECONDS); + + timeList.add(milliseconds); + } + for(int i = 1; i < ITERATIONS; i++) { + assertLessThanOrEqual(timeList.get(i - 1), timeList.get(i)); + } + } + + public void testRandom() { + for(int i = 0; i < ITERATIONS; i++) { + long randomTime = getRandomTime(MAXIMUM); + + assertGreaterThanOrEqual(MAXIMUM, randomTime); + assertGreaterThanOrEqual(randomTime, 0); + } + } + + public void testOrder() throws Exception { + final BlockingQueue clean = new LinkedBlockingQueue(); + final ConcurrentHashMap record = new ConcurrentHashMap(); + + Cleaner cleaner = new Cleaner() { + + long start = System.currentTimeMillis(); + + public void clean(Integer key) { + record.put(key, start - System.currentTimeMillis()); + clean.offer(key); + + } + }; + LeaseManager manager = new LeaseManager(cleaner); + List> list = new ArrayList>(); + + long start = System.currentTimeMillis(); + + for(int i = 0; i < ITERATIONS; i++) { + long randomTime = getRandomTime(MAXIMUM) + MAXIMUM + i * 50; + + System.err.printf("leasing [%s] for [%s] @ %s%n", i, randomTime, System.currentTimeMillis() - start); + + Lease lease = manager.lease(i, randomTime, TimeUnit.MILLISECONDS); + + list.add(lease); + } + start = System.currentTimeMillis(); + + for(int i = 0; i < ITERATIONS; i++) { + try { + System.err.printf("renewing [%s] for [%s] expires [%s] @ %s expired [%s] %n", i, i, list.get(i).getExpiry(TimeUnit.MILLISECONDS), System.currentTimeMillis() - start, record.get(i)); + list.get(i).renew(i, TimeUnit.MILLISECONDS); + }catch(Exception e) { + System.err.printf("Lease %s in error: ", i); + e.printStackTrace(System.err); + } + } + int variation = 20; + int cleaned = 0; + + for(int i = 0; i < ITERATIONS; i++) { + int value = clean.take(); + cleaned++; + + System.err.printf("index=[%s] clean=[%s] expiry[%s]=%s expiry[%s]=%s%n ", i, value, i, record.get(i), value, record.get(value)); + assertLessThanOrEqual(i - variation, value); + } + assertEquals(cleaned, ITERATIONS); + } + + public void testLease() throws Exception { + final BlockingQueue clean = new LinkedBlockingQueue(); + + Cleaner cleaner = new Cleaner() { + public void clean(Expectation key) { + clean.offer(key); + } + }; + final BlockingQueue> renewalQueue = new LinkedBlockingQueue>(); + final BlockingQueue> expiryQueue = new LinkedBlockingQueue>(); + final CountDownLatch ready = new CountDownLatch(21); + final AtomicInteger renewCount = new AtomicInteger(ITERATIONS); + + for(int i = 0; i < 20; i++) { + new Thread(new Runnable() { + public void run() { + while(renewCount.getAndDecrement() > 0) { + long randomTime = getRandomTime(MAXIMUM); + + try { + ready.countDown(); + ready.await(); + + Lease lease = renewalQueue.take(); + + try { + lease.renew(randomTime, TimeUnit.MILLISECONDS); + lease.getKey().setExpectation(randomTime, TimeUnit.MILLISECONDS); + + assertGreaterThanOrEqual(randomTime, 0); + assertGreaterThanOrEqual(randomTime, lease.getExpiry(TimeUnit.MILLISECONDS)); + } catch(Exception e) { + expiryQueue.offer(lease); + } + } catch(Exception e) { + e.printStackTrace(); + } + } + } + }).start(); + } + final LeaseManager manager = new LeaseManager(cleaner); + final CountDownLatch latch = new CountDownLatch(21); + final AtomicInteger leaseCount = new AtomicInteger(ITERATIONS); + + for(int i = 0; i < 20; i++) { + new Thread(new Runnable() { + public void run() { + while(leaseCount.getAndDecrement() > 0) { + long randomTime = getRandomTime(MAXIMUM); + Expectation expectation = new Expectation(randomTime, TimeUnit.MILLISECONDS); + + try { + latch.countDown(); + latch.await(); + } catch(InterruptedException e) { + e.printStackTrace(); + } + assertGreaterThanOrEqual(randomTime, 0); + + Lease lease = manager.lease(expectation, randomTime, TimeUnit.MILLISECONDS); + renewalQueue.offer(lease); + } + } + }).start(); + } + ready.countDown(); + latch.countDown(); + + for (int i = 0; i < ITERATIONS; i++) { + Expectation expectation = clean.poll(MAXIMUM, TimeUnit.MILLISECONDS); + + if(expectation != null) { + long accuracy = System.nanoTime() - expectation.getExpectation(TimeUnit.NANOSECONDS); + long milliseconds = TimeUnit.MILLISECONDS.convert(accuracy, TimeUnit.NANOSECONDS); + + System.err.printf("index=[%s] accuracy=[%s] queue=[%s]%n", i, milliseconds, clean.size()); + } else { + System.err.printf("index=[%s] queue=[%s]%n", i, clean.size()); + } + + } + System.err.printf("waiting=[%s]%n", clean.size()); + } + + + public static class Expectation { + + private long time; + + public Expectation(long duration, TimeUnit unit) { + setExpectation(duration, unit); + } + + public void setExpectation(long duration, TimeUnit unit) { + long nano = TimeUnit.NANOSECONDS.convert(duration, unit); + long expect = nano + System.nanoTime(); + + this.time = expect; + } + + public long getExpectation(TimeUnit unit) { + return unit.convert(time, TimeUnit.NANOSECONDS); + } + } + + + public static long getRandomTime(long maximum) { + long random = new Random().nextLong() % maximum; + + if(random < 0) { + random *= -1; + } + return random; + } + + public static void main(String[] list) throws Exception { + new LeaseManagerTest().testClock(); + new LeaseManagerTest().testRandom(); + new LeaseManagerTest().testOrder(); + new LeaseManagerTest().testLease(); + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseTest.java new file mode 100644 index 0000000..d4b73d7 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseTest.java @@ -0,0 +1,87 @@ +package org.simpleframework.common.lease; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.simpleframework.common.lease.Cleaner; +import org.simpleframework.common.lease.Contract; +import org.simpleframework.common.lease.ContractController; +import org.simpleframework.common.lease.ContractLease; +import org.simpleframework.common.lease.ContractMaintainer; +import org.simpleframework.common.lease.Expiration; +import org.simpleframework.common.lease.Lease; + +public class LeaseTest extends TimeTestCase { + + private static int ITERATIONS = 10000; + + static { + String value = System.getProperty("iterations"); + + if (value != null) { + ITERATIONS = Integer.parseInt(value); + } + } + + public void testLease() throws Exception { + final BlockingQueue clean = new LinkedBlockingQueue(); + + Cleaner cleaner = new Cleaner() { + public void clean(Integer key) { + clean.offer(key); + } + }; + Map table = new ConcurrentHashMap(); + List list = new ArrayList(); + ContractController controller = new ContractMaintainer(cleaner); + + for (int i = 0; i < ITERATIONS; i++) { + long random = (long) (Math.random() * 1000) + 1000L; + Contract contract = new Expiration(i, random, TimeUnit.MILLISECONDS); + Lease lease = new ContractLease(controller, contract); + + table.put(i, contract); + list.add(lease); + controller.issue(contract); + } + for (int i = 0; i < ITERATIONS; i++) { + long random = (long) (Math.random() * 1000); + + try { + list.get(i).renew(random, TimeUnit.MILLISECONDS); + } catch (Exception e) { + continue; + // e.printStackTrace(); + } + } + for (int i = 0; i < ITERATIONS; i++) { + try { + System.err.println("delay: " + + list.get(i).getExpiry(TimeUnit.MILLISECONDS)); + } catch (Exception e) { + continue; + // e.printStackTrace(); + } + } + System.err.println("clean: " + clean.size()); + + for (int i = 0; i < ITERATIONS; i++) { + Integer index = clean.take(); + Contract contract = table.get(index); + + // assertLessThanOrEqual(-4000, + // contract.getDelay(TimeUnit.MILLISECONDS)); + System.err.println(String.format("index=[%s] delay=[%s]", index, + contract.getDelay(TimeUnit.MILLISECONDS))); + } + } + + public static void main(String[] list) throws Exception { + new LeaseTest().testLease(); + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/lease/TimeTestCase.java b/simple/simple-common/src/test/java/org/simpleframework/common/lease/TimeTestCase.java new file mode 100644 index 0000000..294cc9b --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/lease/TimeTestCase.java @@ -0,0 +1,25 @@ +package org.simpleframework.common.lease; + +import junit.framework.TestCase; + +public class TimeTestCase extends TestCase { + + public void testTime() { + } + + public static void assertLessThan(long a, long b) { + assertTrue(String.format("Value %s is not less than %s", a, b), a < b); + } + + public static void assertLessThanOrEqual(long a, long b) { + assertTrue(String.format("Value %s is not less than or equal to %s", a, b), a <= b); + } + + public static void assertGreaterThan(long a, long b) { + assertTrue(String.format("Value %s is not greater than %s", a, b), a > b); + } + + public static void assertGreaterThanOrEqual(long a, long b) { + assertTrue(String.format("Value %s is not greater than or equal to %s", a, b), a >= b); + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/thread/SchedulerTest.java b/simple/simple-common/src/test/java/org/simpleframework/common/thread/SchedulerTest.java new file mode 100644 index 0000000..78fe802 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/thread/SchedulerTest.java @@ -0,0 +1,65 @@ +package org.simpleframework.common.thread; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.simpleframework.common.thread.ConcurrentScheduler; + +import junit.framework.TestCase; + +public class SchedulerTest extends TestCase { + + private static final int ITERATIONS = 10000; + + public void testScheduler() throws Exception { + ConcurrentScheduler queue = new ConcurrentScheduler(Runnable.class, 10); + LinkedBlockingQueue list = new LinkedBlockingQueue(); + + for(int i = 0; i < ITERATIONS; i++) { + queue.execute(new Task(list, new Timer(i)), i, TimeUnit.MILLISECONDS); + } + for(Timer timer = list.take(); timer.getValue() < ITERATIONS - 10; timer = list.take()) { + System.err.println("value=["+timer.getValue()+"] delay=["+timer.getDelay()+"] expect=["+timer.getExpectation()+"]"); + } + } + + public class Timer { + + private Integer value; + + private long time; + + public Timer(Integer value) { + this.time = System.currentTimeMillis(); + this.value = value; + } + + public Integer getValue() { + return value; + } + + public long getDelay() { + return System.currentTimeMillis() - time; + } + + public int getExpectation() { + return value.intValue(); + } + } + + public class Task implements Runnable { + + private LinkedBlockingQueue queue; + + private Timer timer; + + public Task(LinkedBlockingQueue queue, Timer timer) { + this.queue = queue; + this.timer = timer; + } + + public void run() { + queue.offer(timer); + } + } +} diff --git a/simple/simple-common/src/test/java/org/simpleframework/common/thread/TransientApplication.java b/simple/simple-common/src/test/java/org/simpleframework/common/thread/TransientApplication.java new file mode 100644 index 0000000..69941b2 --- /dev/null +++ b/simple/simple-common/src/test/java/org/simpleframework/common/thread/TransientApplication.java @@ -0,0 +1,54 @@ +package org.simpleframework.common.thread; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.simpleframework.common.thread.ConcurrentExecutor; + +public class TransientApplication { + + public static void main(String[] list) throws Exception { + BlockingQueue queue = new LinkedBlockingQueue(); + ConcurrentExecutor pool = new ConcurrentExecutor(TerminateTask.class, 10); + + for(int i = 0; i < 50; i++) { + pool.execute(new LongTask(queue, String.valueOf(i))); + } + pool.execute(new TerminateTask(pool)); + } + + private static class TerminateTask implements Runnable { + + private ConcurrentExecutor pool; + + public TerminateTask(ConcurrentExecutor pool) { + this.pool = pool; + } + + public void run() { + pool.stop(); + } + } + + private static class LongTask implements Runnable { + + private BlockingQueue queue; + + private String name; + + public LongTask(BlockingQueue queue, String name) { + this.queue = queue; + this.name = name; + } + + public void run() { + try { + Thread.sleep(1000); + } catch(Exception e) { + e.printStackTrace(); + } + System.err.println(name); + queue.offer(name); + } + } +} diff --git a/simple/simple-http/pom.xml b/simple/simple-http/pom.xml new file mode 100644 index 0000000..2a079fc --- /dev/null +++ b/simple/simple-http/pom.xml @@ -0,0 +1,142 @@ + + + org.sonatype.oss + oss-parent + 7 + + 4.0.0 + org.simpleframework + simple-http + jar + 6.0.1 + Simple HTTP + http://www.simpleframework.org + Simple is a high performance asynchronous HTTP framework for Java + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + http://simpleweb.svn.sourceforge.net/viewvc/simpleweb.tags/simple-http-6.0.1 + scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-http-6.0.1 + scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-http-6.0.1 + + + + niallg + Niall Gallagher + niallg@users.sf.net + + + + UTF-8 + UTF-8 + UTF-8 + + + + org.simpleframework + simple-common + 6.0.1 + + + org.simpleframework + simple-transport + 6.0.1 + + + junit + junit + 3.8.1 + test + + + + + + org.codehaus.mojo + cobertura-maven-plugin + + + + + + + org.apache.maven.wagon + wagon-ssh-external + 1.0-alpha-5 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + org.apache.maven.plugins + maven-source-plugin + + UTF-8 + UTF-8 + + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + + release-sign-artifacts + + + performRelease + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.1 + + + sign-artifacts + verify + + sign + + + + + + + + + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Address.java b/simple/simple-http/src/main/java/org/simpleframework/http/Address.java new file mode 100644 index 0000000..cd05280 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Address.java @@ -0,0 +1,157 @@ +/* + * Address.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import org.simpleframework.common.KeyMap; + +/** + * The Address interface is used to represent a generic + * uniform resource identifier. This interface allows each section + * of the uniform resource identifier to be represented. A generic + * uniform resource identifier syntax is represented in RFC 2616 + * section 3.2.2 for the HTTP protocol, this allows similar URI's + * for example ftp, http, https, tftp. The syntax is + *

+ *
+ *    URI = [scheme "://"] host [ ":" port ] [ path [ "?" query ]]
+ *
+ * 
+ * This interface represents the host, port, path and query part + * of the uniform resource identifier. The parameters are also + * represented by the URI. The parameters in a URI consist of name + * and value pairs in the path segment of the URI. + *

+ * This will normalize the path part of the uniform resource + * identifier. A normalized path is one that contains no back + * references like "./" and "../". The normalized path will not + * contain the path parameters. + * + * @author Niall Gallagher + */ +public interface Address { + + /** + * This allows the scheme of the URL given to be returned. + * If the URI does not contain a scheme then this will + * return null. The scheme of the URI is the part that + * specifies the type of protocol that the URI is used + * for, an example http://domain/path is + * a URI that is intended for the http protocol. The + * scheme is the string http. + * + * @return the scheme tag for the address if available + */ + String getScheme(); + + /** + * This is used to retrieve the domain of this URI. The + * domain part in the URI is an optional part, an example + * http://domain/path?querypart. This will + * return the value of the domain part. If there is no + * domain part then this will return null otherwise the + * domain value found in the uniform resource identifier. + * + * @return the domain part of the address if available + */ + String getDomain(); + + /** + * This is used to retrieve the port of the uniform resource + * identifier. The port part in this is an optional part, an + * example http://host:port/path?querypart. This + * will return the value of the port. If there is no port then + * this will return -1 because this represents + * an impossible uniform resource identifier port. The port + * is an optional part. + * + * @return this returns the port part if it is available + */ + int getPort(); + + /** + * This is used to retrieve the path of this URI. The path part + * is the most fundamental part of the URI. This will return + * the value of the path. If there is no path part then this + * will return a Path implementation that represents the root + * path represented by /. + * + * @return the path part of the uniform resource identifier + */ + Path getPath(); + + /** + * This is used to retrieve the query of this URI. The query part + * in the URI is an optional part. This will return the value + * of the query part. If there is no query part then this will + * return an empty Query object. The query is + * an optional member of a URI and comes after the path part, it + * is preceded by a question mark, ? character. + * For example the following URI contains query for + * its query part, http://host:port/path?query. + *

+ * This returns a org.simpleframework.http.Query + * object that can be used to interact directly with the query + * values. The Query object is a read-only interface + * to the query parameters, and so will not affect the URI. + * + * @return a Query object for the query part + */ + Query getQuery(); + + /** + * This extracts the parameter values from the uniform resource + * identifier represented by this object. The parameters that a + * uniform resource identifier contains are embedded in the path + * part of the URI. If the path contains no parameters then this + * will return an empty Map instance. + *

+ * This will produce unique name and value parameters. Thus if the + * URI contains several path segments with similar parameter names + * this will return the deepest parameter. For example if the URI + * represented was http://domain/path1;x=y/path2;x=z + * the value for the parameter named x would be + * z. + * + * @return this will return the parameter names found in the URI + */ + KeyMap getParameters(); + + /** + * This is used to convert this URI object into a String + * object. This will only convert the parts of the URI that exist, so + * the URI may not contain the domain or the query part and it will + * not contain the path parameters. If the URI contains all these + * parts then it will return something like + *

+    * scheme://host:port/path/path?querypart
+    * 
+ *

+ * It can return /path/path?querypart style relative + * URI's. If any of the parts are set to null then that part will be + * missing, for example if only the path is available then this will + * omit the domain, port and scheme. Showing a relative address. + *

+    * scheme://host:port/?querypart
+    * 
+ * + * @return the URI address with the optional parts if available + */ + String toString(); +} + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/ContentDisposition.java b/simple/simple-http/src/main/java/org/simpleframework/http/ContentDisposition.java new file mode 100644 index 0000000..579cb91 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/ContentDisposition.java @@ -0,0 +1,59 @@ +/* + * ContentDisposition.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * The ContentDisposition object represents the HTTP + * Content-Disposition header of a request. A content disposition + * contains the name of the part and whether that part contains + * the contents of a file. If the part represents a parameter then + * the getName can be used to determine the name, if + * it represents a file then getFileName is preferred. + * + * @author Niall Gallagher + */ +public interface ContentDisposition { + + /** + * This method is used to acquire the name of the part. Typically + * this is used when the part represents a text parameter rather + * than a file. However, this can also be used with a file part. + * + * @return this returns the name of the associated part + */ + String getName(); + + /** + * This method is used to acquire the file name of the part. This + * is used when the part represents a text parameter rather than + * a file. However, this can also be used with a file part. + * + * @return this returns the file name of the associated part + */ + String getFileName(); + + /** + * This method is used to determine the type of a part. Typically + * a part is either a text parameter or a file. If this is true + * then the content represented by the associated part is a file. + * + * @return this returns true if the associated part is a file + */ + boolean isFile(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/ContentType.java b/simple/simple-http/src/main/java/org/simpleframework/http/ContentType.java new file mode 100644 index 0000000..c62687d --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/ContentType.java @@ -0,0 +1,142 @@ +/* + * ContentType.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * This provides access to the MIME type parts, that is the primary + * type, the secondary type and an optional character set parameter. + * The charset parameter is one of many parameters that + * can be associated with a MIME type. This however this exposes this + * parameter with a typed method. + *

+ * The getCharset will return the character encoding the + * content type is encoded within. This allows the user of the content + * to decode it correctly. Other parameters can be acquired from this + * by simply providing the name of the parameter. + * + * @author Niall Gallagher + */ +public interface ContentType { + + /** + * This method is used to get the primary and secondary parts + * joined together with a "/". This is typically how a content + * type is examined. Here convenience is most important, we can + * easily compare content types without any parameters. + * + * @return this returns the primary and secondary types + */ + String getType(); + + /** + * This sets the primary type to whatever value is in the string + * provided is. If the string is null then this will contain a + * null string for the primary type of the parameter, which is + * likely invalid in most cases. + * + * @param type the type to set for the primary type of this + */ + void setPrimary(String type); + + /** + * This is used to retrieve the primary type of this MIME type. The + * primary type part within the MIME type defines the generic type. + * For example text/plain; charset=UTF-8. This will + * return the text value. If there is no primary type then this + * will return null otherwise the string value. + * + * @return the primary type part of this MIME type + */ + String getPrimary(); + + /** + * This sets the secondary type to whatever value is in the string + * provided is. If the string is null then this will contain a + * null string for the secondary type of the parameter, which is + * likely invalid in most cases. + * + * @param type the type to set for the primary type of this + */ + void setSecondary(String type); + + /** + * This is used to retrieve the secondary type of this MIME type. + * The secondary type part within the MIME type defines the generic + * type. For example text/html; charset=UTF-8. This + * will return the HTML value. If there is no secondary type then + * this will return null otherwise the string value. + * + * @return the primary type part of this MIME type + */ + String getSecondary(); + + /** + * This will set the charset to whatever value the + * string contains. If the string is null then this will not set + * the parameter to any value and the toString method + * will not contain any details of the parameter. + * + * @param charset parameter value to add to the MIME type + */ + void setCharset(String charset); + + /** + * This is used to retrieve the charset of this MIME + * type. This is a special parameter associated with the type, if + * the parameter is not contained within the type then this will + * return null, which typically means the default of ISO-8859-1. + * + * @return the value that this parameter contains + */ + String getCharset(); + + /** + * This is used to retrieve an arbitrary parameter from the MIME + * type header. This ensures that values for boundary + * or other such parameters are not lost when the header is parsed. + * This will return the value, unquoted if required, as a string. + * + * @param name this is the name of the parameter to be retrieved + * + * @return this is the value for the parameter, or null if empty + */ + String getParameter(String name); + + /** + * This will add a named parameter to the content type header. If + * a parameter of the specified name has already been added to the + * header then that value will be replaced by the new value given. + * Parameters such as the boundary as well as other + * common parameters can be set with this method. + * + * @param name this is the name of the parameter to be added + * @param value this is the value to associate with the name + */ + void setParameter(String name, String value); + + /** + * This will return the value of the MIME type as a string. This + * will concatenate the primary and secondary type values and + * add the charset parameter to the type which will + * recreate the content type. + * + * @return this returns the string representation of the type + */ + String toString(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Cookie.java b/simple/simple-http/src/main/java/org/simpleframework/http/Cookie.java new file mode 100644 index 0000000..a9ab422 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Cookie.java @@ -0,0 +1,527 @@ +/* + * Cookie.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.SimpleTimeZone; +import java.util.TimeZone; + +/** + * This class is used to represent a generic cookie. This exposes + * the fields that a cookie can have. By default the version of the + * Cookie is set to 1. The version can be configured + * using the setVersion method. The domain, path, + * security, and expiry of the cookie can also be set using their + * respective set methods. + *

+ * The toString method allows the Cookie + * to be converted back into text form. This text form converts the + * cookie according to the Set-Cookie header form. This is done so + * that a created Cookie instance can be converted + * to a string which can be used as a a HTTP header. + * + * @author Niall Gallagher + */ +public class Cookie { + + /** + * This is used to set the expiry date for the cookie. + */ + private CookieDate date; + + /** + * The name attribute of this cookie instance. + */ + private String name; + + /** + * The value attribute of this cookie instance. + */ + private String value; + + /** + * Represents the value of the path for this cookie. + */ + private String path; + + /** + * Represents the value of the domain attribute. + */ + private String domain; + + /** + * Determines whether the cookie should be secure. + */ + private boolean secure; + + /** + * Determines whether the cookie should be protected. + */ + private boolean protect; + + /** + * This is used to determine the the cookie is new. + */ + private boolean created; + + /** + * Represents the value of the version attribute. + */ + private int version; + + /** + * Represents the duration in seconds of the cookie. + */ + private int expiry; + + /** + * Constructor of the Cookie that uses a default + * version of 1, which is used by RFC 2109. This contains none + * of the optional attributes, such as domain and path. These + * optional attributes can be set using the set methods. + *

+ * The name must conform to RFC 2109, which means that it can + * contain only ASCII alphanumeric characters and cannot have + * commas, white space, or semicolon characters. + * + * @param name this is the name of this cookie instance + * @param value this is the value of this cookie instance + */ + public Cookie(String name, String value) { + this(name, value, "/"); + } + + /** + * Constructor of the Cookie that uses a default + * version of 1, which is used by RFC 2109. This contains none + * of the optional attributes, such as domain and path. These + * optional attributes can be set using the set methods. + *

+ * The name must conform to RFC 2109, which means that it can + * contain only ASCII alphanumeric characters and cannot have + * commas, white space, or semicolon characters. + * + * @param name this is the name of this cookie instance + * @param value this is the value of this cookie instance + * @param created this determines if the cookie is new + */ + public Cookie(String name, String value, boolean created) { + this(name, value, "/", created); + } + + /** + * Constructor of the Cookie that uses a default + * version of 1, which is used by RFC 2109. This allows the + * path attribute to be specified for on construction. Other + * attributes can be set using the set methods provided. + *

+ * The name must conform to RFC 2109, which means that it can + * contain only ASCII alphanumeric characters and cannot have + * commas, white space, or semicolon characters. + * + * @param name this is the name of this cookie instance + * @param value this is the value of this cookie instance + * @param path the path attribute of this cookie instance + */ + public Cookie(String name, String value, String path) { + this(name, value, path, false); + } + + /** + * Constructor of the Cookie that uses a default + * version of 1, which is used by RFC 2109. This allows the + * path attribute to be specified for on construction. Other + * attributes can be set using the set methods provided. + *

+ * The name must conform to RFC 2109, which means that it can + * contain only ASCII alphanumeric characters and cannot have + * commas, white space, or semicolon characters. + * + * @param name this is the name of this cookie instance + * @param value this is the value of this cookie instance + * @param path the path attribute of this cookie instance + * @param created this determines if the cookie is new + */ + public Cookie(String name, String value, String path, boolean created) { + this.date = new CookieDate(); + this.created = created; + this.value = value; + this.name = name; + this.path = path; + this.version = 1; + this.expiry = -1; + } + + /** + * This is used to determine if the cookie is new. A cookie is + * considered new if it has just been created on the server. A + * cookie is considered not new if it has been received by the + * client in a request. This allows the server to determine if + * the cookie needs to be delivered to the client. + * + * @return this returns true if the cookie was just created + */ + public boolean isNew() { + return created; + } + + /** + * This returns the version for this cookie. The version is + * not optional and so will always return the version this + * cookie uses. If no version number is specified this will + * return a version of 1, to comply with RFC 2109. + * + * @return the version value from this cookie instance + */ + public int getVersion() { + return version; + } + + /** + * This enables the version of the Cookie to be + * set. By default the version of the Cookie is + * set to 1. It is not advisable to set the version higher + * than 1, unless it is known that the client will accept it. + *

+ * Some old browsers can only handle cookie version 0. This + * can be used to comply with the original Netscape cookie + * specification. Version 1 complies with RFC 2109. + * + * @param version this is the version number for the cookie + */ + public void setVersion(int version) { + this.version = version; + } + + /** + * This returns the name for this cookie. The name and value + * attributes of a cookie define what the Cookie + * is for, these values will always be present. These are + * mandatory for both the Cookie and Set-Cookie headers. + *

+ * Because the cookie may be stored by name, the cookie name + * cannot be modified after the creation of the cookie object. + * + * @return the name from this cookie instance object + */ + public String getName() { + return name; + } + + /** + * This returns the value for this cookie. The name and value + * attributes of a cookie define what the Cookie + * is for, these values will always be present. These are + * mandatory for both the Cookie and Set-Cookie headers. + * + * @return the value from this cookie instance object + */ + public String getValue() { + return value; + } + + /** + * This enables the value of the cookie to be changed. This + * can be set to any value the server wishes to send. Cookie + * values can contain space characters as they are transmitted + * in quotes. For example a value of some value + * is perfectly legal. However for maximum compatibility + * across the different plaforms such as PHP, JavaScript and + * others, quotations should be avoided. If quotations are + * required they must be added to the string. For example a + * quoted value could be created as "some value". + * + * @param value this is the new value of this cookie object + */ + public void setValue(String value) { + this.value = value; + } + + /** + * This determines whether the cookie is secure. The cookie + * is secure if it has the "secure" token set, as defined + * by RFC 2109. If this token is set then the cookie is only + * sent over secure channels such as SSL and TLS and ensures + * that a third party cannot intercept and spoof the cookie. + * + * @return this returns true if the "secure" token is set + */ + public boolean isSecure() { + return secure; + } + + /** + * This is used to determine if the client browser should send + * this cookie over a secure protocol. If this is true then + * the client browser should only send the cookie over secure + * channels such as SSL and TLS. This ensures that the value + * of the cookie cannot be intercepted by a third party. + * + * @param secure if true then the cookie should be secure + */ + public void setSecure(boolean secure) { + this.secure = secure; + } + + /** + * This is used to determine if the cookie is protected against + * cross site scripting. It sets the HttpOnly value + * for the cookie. Setting this value ensures that the cookie + * is not available to some scripting attacks. + * + * @return this returns true if the cookie is protected + */ + public boolean isProtected() { + return protect; + } + + /** + * This is used to protect the cookie from cross site scripting + * vulnerabilities. If this is set to true the cookie will be + * protected by setting the HttpOnly value for the + * cookie. See RFC 6265 for more details on this value. + * + * @param protect this determines if the cookie is protected + */ + public void setProtected(boolean protect) { + this.protect = protect; + } + + /** + * This returns the number of seconds a cookie lives for. This + * determines how long the cookie will live on the client side. + * If the expiry is less than zero the cookie lifetime is the + * duration of the client browser session, if it is zero then + * the cookie will be deleted from the client browser. + * + * @return returns the duration in seconds the cookie lives + */ + public int getExpiry() { + return expiry; + } + + /** + * This allows a lifetime to be specified for the cookie. This + * will make use of the "max-age" token specified by RFC 2109 + * the specifies the number of seconds a browser should keep + * a cookie for. This is useful if the cookie is to be kept + * beyond the lifetime of the client session. If the value of + * this is zero then this will remove the client cookie, if + * it is less than zero then the "max-age" field is ignored. + * + * @param expiry the duration in seconds the cookie lives + */ + public void setExpiry(int expiry){ + this.expiry = expiry; + } + + /** + * This returns the path for this cookie. The path is in both + * the Cookie and Set-Cookie headers and so may return null + * if there is no domain value. If the toString + * or toClientString is invoked the path will + * not be present if the path attribute is null. + * + * @return this returns the path value from this cookie + */ + public String getPath() { + return path; + } + + /** + * This is used to set the cookie path for this cookie. This + * is set so that the cookie can specify the directories that + * the cookie is sent with. For example if the path attribute + * is set to /pub/bin, then requests for the + * resource http://hostname:port/pub/bin/README + * will be issued with this cookie. The cookie is issued for + * all resources in the path and all subdirectories. + * + * @param path this is the path value for this cookie object + */ + public void setPath(String path) { + this.path = path; + } + + /** + * This returns the domain for this cookie. The domain is in + * both the Cookie and Set-Cookie headers and so may return + * null if there is no domain value. If either the + * toString or toClientString is + * invoked the domain will not be present if this is null. + * + * @return this returns the domain value from this cookie + */ + public String getDomain() { + return domain; + } + + /** + * This enables the domain for this Cookie to be + * set. The form of the domain is specified by RFC 2109. The + * value can begin with a dot, like .host.com. + * This means that the cookie is visible within a specific + * DNS zone like www.host.com. By default this + * value is null which means it is sent back to its origin. + * + * @param domain this is the domain value for this cookie + */ + public void setDomain(String domain) { + this.domain = domain; + } + + /** + * This will give the correct string value of this cookie. This + * will generate the cookie text with only the values that were + * given with this cookie. If there are no optional attributes + * like $Path or $Domain these are left blank. This returns the + * encoding as it would be for the HTTP Cookie header. + * + * @return this returns the Cookie header encoding of this + */ + public String toClientString(){ + return "$Version="+version+"; "+name+"="+ + value+ (path==null?"":"; $Path="+ + path)+ (domain==null? "":"; $Domain="+ + domain); + } + + /** + * The toString method converts the cookie to the + * Set-Cookie value. This can be used to send the HTTP header + * to a client browser. This uses a format that has been tested + * with various browsers. This is required as some browsers + * do not perform flexible parsing of the Set-Cookie value. + *

+ * Netscape and IE-5.0 can't or wont handle Path + * it must be path also Netscape can not handle + * the path in quotations such as "/path" it must + * be /path. This value is never in quotations. + *

+ * For maximum compatibility cookie values are not transmitted + * in quotations. This is done to ensure that platforms like + * PHP, JavaScript and various others that don't comply with + * RFC 2109 can transparently access the sent cookies. + *

+ * When setting the expiry time for the cookie it is important + * to set the max-age and expires + * attributes so that IE-5.0 and up can understand them. Old + * versions of IE do not understand max-age. + * + * @return this returns a Set-Cookie encoding of the cookie + */ + public String toString(){ + return name+"="+value+"; version="+ + version +(path ==null ?"":"; path="+path)+ + (domain ==null ?"": "; domain="+domain)+ + (expiry< 0?"":"; expires="+date.format(expiry))+ + (expiry < 0 ? "" : "; max-age="+expiry)+ + (secure ? "; secure" : "") + + (protect ? "; httponly" : ""); + + } + + /** + * The CookieDate complies with the date format + * used by older browsers such as Internet Explorer and + * Netscape Navigator. The format of the date is not the same + * as other HTTP date headers. It takes the form. + *

+    * 
+    *    DAY, DD-MMM-YYYY HH:MM:SS GMT
+    * 
+    * 
+ * Support for this format is required as many browsers do + * not support max-age and so cookies will not + * expire for these browsers. + */ + private static class CookieDate { + + /** + * This is the format that is required for the date. + */ + private static final String FORMAT = "EEE, dd-MMM-yyyy HH:mm:ss z"; + + /** + * The cookie date must be returned in the GMT zone. + */ + private static final String ZONE = "GMT"; + + /** + * This is the date formatter used to build the string. + */ + private final DateFormat format; + + /** + * This is the GMT time zone which must be used. + */ + private final TimeZone zone; + + /** + * Constructor for the CookieDate formatter. + * This creates the time zone and date formatting tools + * that are need to convert the expiry in seconds to the + * correct text format for older browsers to understand. + */ + public CookieDate() { + this.format = new SimpleDateFormat(FORMAT); + this.zone = new SimpleTimeZone(0, ZONE); + } + + /** + * This takes the number of seconds the cookie will live + * for. In order for this to be respected by older browsers + * such as IE-5.0 to IE-9.0 this must return a string in + * the original cookie specification by Netscape. + * + * @param seconds the number of seconds from now + * + * @return a date formatted for used with old browsers + */ + public String format(int seconds) { + Calendar calendar = Calendar.getInstance(zone); + Date date = convert(seconds); + + calendar.setTime(date); + format.setCalendar(calendar); + + return format.format(date); + } + + /** + * This method is used to convert the provided time to + * a date that can be formatted. The time returned is the + * current time plus the number of seconds provided. + * + * @param seconds the number of seconds from now + * + * @return a date representing some time in the future + */ + private Date convert(int seconds) { + long now = System.currentTimeMillis(); + long duration = seconds * 1000L; + long time = now + duration; + + return new Date(time); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Method.java b/simple/simple-http/src/main/java/org/simpleframework/http/Method.java new file mode 100644 index 0000000..5bb5027 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Method.java @@ -0,0 +1,70 @@ +/* + * Method.java May 2012 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * The Method interface contains the common HTTP methods + * that are sent with a request. This only contains those methods + * that have been defined within the RFC 2616 specification. These + * are defined here for convenience and informational purposes. + * + * @author Niall Gallagher + */ +public interface Method { + + /** + * For use with a proxy that can dynamically switch to being a tunnel. + */ + String CONNECT = "CONNECT"; + + /** + * Requests that the origin server delete the resource identified. + */ + String DELETE = "DELETE"; + + /** + * Retrieve whatever information is identified by the request. + */ + String GET = "GET"; + + /** + * Retrieve only the headers for the resource that is requested. + */ + String HEAD = "HEAD"; + + /** + * Represents a request for the communication options available. + */ + String OPTIONS = "OPTIONS"; + + /** + * Request that the origin server accept the entity in the request. + */ + String POST = "POST"; + + /** + * Requests that the entity be stored as the resource specified + */ + String PUT = "PUT"; + + /** + * Invoke a remote application layer loop back of the request. + */ + String TRACE = "TRACE"; +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Part.java b/simple/simple-http/src/main/java/org/simpleframework/http/Part.java new file mode 100644 index 0000000..75660ea --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Part.java @@ -0,0 +1,107 @@ +/* + * Part.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.io.IOException; +import java.io.InputStream; + +/** + * The Part object is used to represent a part within + * a request message. Typically a part represents either a text + * parameter or a file, with associated headers. The contents of + * the part can be acquire as an InputStream or as a + * string encoded in the default HTTP encoding ISO-8859-1 or in + * the encoding specified with the Content-Type header. + * + * @author Niall Gallagher + */ +public interface Part { + + /** + * This method is used to determine the type of a part. Typically + * a part is either a text parameter or a file. If this is true + * then the content represented by the associated part is a file. + * + * @return this returns true if the associated part is a file + */ + boolean isFile(); + + /** + * This method is used to acquire the name of the part. Typically + * this is used when the part represents a text parameter rather + * than a file. However, this can also be used with a file part. + * + * @return this returns the name of the associated part + */ + String getName(); + + /** + * This method is used to acquire the file name of the part. This + * is used when the part represents a text parameter rather than + * a file. However, this can also be used with a file part. + * + * @return this returns the file name of the associated part + */ + String getFileName(); + + /** + * This is used to acquire the header value for the specified + * header name. Providing the header values through this method + * ensures any special processing for a know content type can be + * handled by an application. + * + * @param name the name of the header to get the value for + * + * @return value of the header mapped to the specified name + */ + String getHeader(String name); + + /** + * This is used to acquire the content of the part as a string. + * The encoding of the string is taken from the content type. + * If no content type is sent the content is decoded in the + * standard default of ISO-8859-1. + * + * @return this returns a string representing the content + * + * @throws IOException thrown if the content can not be created + */ + String getContent() throws IOException; + + /** + * This is used to acquire an InputStream for the + * part. Acquiring the stream allows the content of the part to + * be consumed by reading the stream. Each invocation of this + * method will produce a new stream starting from the first byte. + * + * @return this returns the stream for this part object + * + * @throws IOException thrown if the stream can not be created + */ + InputStream getInputStream() throws IOException; + + /** + * This is used to acquire the content type for this part. This + * is typically the type of content for a file part, as provided + * by a MIME type from the HTTP "Content-Type" header. + * + * @return this returns the content type for the part object + */ + ContentType getContentType(); +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Path.java b/simple/simple-http/src/main/java/org/simpleframework/http/Path.java new file mode 100644 index 0000000..fb07ef0 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Path.java @@ -0,0 +1,166 @@ +/* + * Path.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * The Path represents the path part of a URI. This provides + * the various components of the URI path to the user. The normalization + * of the path is the conversion of the path given into it's actual path by + * removing the references to the parent directories and to the current dir. + *

+ * If the path that this represents is /usr/bin/../etc/./README + * then the actual path, normalized, is /usr/etc/README. Once + * the path has been normalized it is possible to acquire the segments as + * an array of strings, which allows simple manipulation of the path. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.parse.PathParser + */ +public interface Path { + + /** + * This will return the extension that the file name contains. + * For example a file name file.en_US.extension + * will produce an extension of extension. This + * will return null if the path contains no file extension. + * + * @return this will return the extension this path contains + */ + String getExtension(); + + /** + * This will return the full name of the file without the path. + * As regargs the definition of the path in RFC 2396 the name + * would be considered the last path segment. So if the path + * was /usr/README the name is README. + * Also for directorys the name of the directory in the last + * path segment is returned. This returns the name without any + * of the path parameters. As RFC 2396 defines the path to have + * path parameters after the path segments. + * + * @return this will return the name of the file in the path + */ + String getName(); + + /** + * This will return the normalized path. The normalized path is + * the path without any references to its parent or itself. So + * if the path to be parsed is /usr/../etc/./ the + * path is /etc/. If the path that this represents + * is a path with an immediate back reference then this will + * return null. This is the path with all its information even + * the parameter information if it was defined in the path. + * + * @return this returns the normalize path without + * ../ or ./ + */ + String getPath(); + + /** + * This will return the normalized path from the specified path + * segment. This allows various path parts to be acquired in an + * efficient means what does not require copy operations of the + * use of substring invocations. Of particular + * interest is the extraction of context based paths. This is + * the path with all its information even the parameter + * information if it was defined in the path. + * + * @param from this is the segment offset to get the path for + * + * @return this returns the normalize path without + * ../ or ./ + */ + String getPath(int from); + + /** + * This will return the normalized path from the specified path + * segment. This allows various path parts to be acquired in an + * efficient means what does not require copy operations of the + * use of substring invocations. Of particular + * interest is the extraction of context based paths. This is + * the path with all its information even the parameter + * information if it was defined in the path. + * + * @param from this is the segment offset to get the path for + * @param count this is the number of path segments to include + * + * @return this returns the normalize path without + * ../ or ./ + */ + String getPath(int from, int count); + + /** + * This method is used to break the path into individual parts + * called segments, see RFC 2396. This can be used as an easy + * way to compare paths and to examine the directory tree that + * the path points to. For example, if an path was broken from + * the string /usr/bin/../etc then the segments + * returned would be usr and etc as + * the path is normalized before the segments are extracted. + * + * @return return all the path segments within the directory + */ + String[] getSegments(); + + /** + * This will return the highest directory that exists within + * the path. This is used to that files within the same path + * can be acquired. An example of that this would do given + * the path /pub/./bin/README would be to return + * the highest directory path /pub/bin/. The "/" + * character will allways be the last character in the path. + * + * @return this method will return the highest directory + */ + String getDirectory(); + + /** + * This will return the path as it is relative to the issued + * path. This in effect will chop the start of this path if + * it's start matches the highest directory of the given path + * as of getDirectory. This is useful if paths + * that are relative to a specific location are required. To + * illustrate what this method will do the following example + * is provided. If this object represented the path string + * /usr/share/rfc/rfc2396.txt and the issued + * path was /usr/share/text.txt then this will + * return the path string /rfc/rfc2396.txt. + * + * @param path the path prefix to acquire a relative path + * + * @return returns a path relative to the one it is given + * otherwize this method will return null + */ + String getRelative(String path); + + /** + * This will return the normalized path. The normalized path is + * the path without any references to its parent or itself. So + * if the path to be parsed is /usr/../etc/./ the + * path is /etc/. If the path that this represents + * is a path with an immediate back reference then this will + * return null. This is the path with all its information even + * the parameter information if it was defined in the path. + * + * @return this returns the normalize path without + * ../ or ./ + */ + String toString(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Principal.java b/simple/simple-http/src/main/java/org/simpleframework/http/Principal.java new file mode 100644 index 0000000..361e4c1 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Principal.java @@ -0,0 +1,48 @@ +/* + * Principal.java November 2002 + * + * Copyright (C) 2002, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * The Principal interface is used to describe a + * user that has a name and password. This should not be + * confused with the java.security.Principal + * interface which does not provide getPassword. + * + * @author Niall Gallagher + */ +public interface Principal { + + /** + * The getPassword method is used to retrieve + * the password of the principal. This is the password + * tag in the RFC 2616 Authorization credentials expression. + * + * @return this returns the password for this principal + */ + String getPassword(); + + /** + * The getName method is used to retreive + * the name of the principal. This is the name tag in + * the RFC 2616 Authorization credentials expression. + * + * @return this returns the name of this principal + */ + String getName(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Protocol.java b/simple/simple-http/src/main/java/org/simpleframework/http/Protocol.java new file mode 100644 index 0000000..295b6c6 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Protocol.java @@ -0,0 +1,370 @@ +/* + * Protocol.java May 2012 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * This represents the HTTP header names defined in RFC 2616. It can be + * used to set and get headers safely from the Request and + * Response objects. This is used internally by the HTTP + * server to parse the incoming requests and also to submit response + * values for each conversation. + *

+ * In addition to the header names this also contains some common + * HTTP header value tokens. These are provided for convenience and + * can be used to ensure that response values comply with RFC 2616. + * + * @author Niall Gallagher + */ +public interface Protocol { + + /** + * Specifies media types which are acceptable for the response. + */ + String ACCEPT = "Accept"; + + /** + * Indicates what character sets are acceptable for the response. + */ + String ACCEPT_CHARSET = "Accept-Charset"; + + /** + * Restricts the content codings that are acceptable in the response. + */ + String ACCEPT_ENCODING = "Accept-Encoding"; + + /** + * Restricts the set of languages that are preferred as a response. + */ + String ACCEPT_LANGUAGE = "Accept-Language"; + + /** + * Indicates a servers acceptance of range requests for a resource. + */ + String ACCEPT_RANGES = "Accept-Ranges"; + + /** + * Estimates the amount of time since the response was generated. + */ + String AGE = "Age"; + + /** + * Lists the set of methods supported by the resource identified. + */ + String ALLOW = "Allow"; + + /** + * Sent by a client that wishes to authenticate itself with a server. + */ + String AUTHORIZATION = "Authorization"; + + /** + * Specifies directives that must be obeyed by all caching mechanisms. + */ + String CACHE_CONTROL = "Cache-Control"; + + /** + * Specifies options that are desired for that particular connection. + */ + String CONNECTION = "Connection"; + + /** + * Specifies a tag indicating of its desired presentation semantics. + */ + String CONTENT_DISPOSITION = "Content-Disposition"; + + /** + * Indicates additional content codings have been applied to the body. + */ + String CONTENT_ENCODING = "Content-Encoding"; + + /** + * Describes the languages of the intended audience for the body. + */ + String CONTENT_LANGUAGE = "Content-Language"; + + /** + * Indicates the size of the entity body in decimal number of octets. + */ + String CONTENT_LENGTH = "Content-Length"; + + /** + * Used to supply the resource location for the entity enclosed. + */ + String CONTENT_LOCATION = "Content-Location"; + + /** + * An MD5 digest of the body for the purpose of checking integrity. + */ + String CONTENT_MD5 = "Content-MD5"; + + /** + * Specifies where in the full body a partial body should be applied. + */ + String CONTENT_RANGE = "Content-Range"; + + /** + * Indicates the media type of the body sent to the recipient. + */ + String CONTENT_TYPE = "Content-Type"; + + /** + * Represents a cookie that contains some information from the client. + */ + String COOKIE = "Cookie"; + + /** + * Represents the date and time at which the message was originated. + */ + String DATE = "Date"; + + /** + * Provides the value of the entity tag for the requested variant. + */ + String ETAG = "ETag"; + + /** + * Indicate that particular server behaviors are required by the client. + */ + String EXPECT = "Expect"; + + /** + * Gives the time after which the response is considered stale. + */ + String EXPIRES = "Expires"; + + /** + * Address for the human user who controls the requesting user agent. + */ + String FROM = "From"; + + /** + * Specifies the host and port number of the resource being requested. + */ + String HOST = "Host"; + + /** + * Specifies the entity tag for a request to make it conditional. + */ + String IF_MATCH = "If-Match"; + + /** + * If variant has not been modified since the time specified. + */ + String IF_MODIFIED_SINCE = "If-Modified-Since"; + + /** + * Verify that none of those entities is current by including a list. + */ + String IF_NONE_MATCH = "If-None-Match"; + + /** + * If the entity is unchanged send me the part that I am missing. + */ + String IF_RANGE = "If-Range"; + + /** + * If the requested resource has not been modified since this time. + */ + String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + /** + * Indicates the date and time at which the variant was last modified. + */ + String LAST_MODIFIED = "Last-Modified"; + + /** + * Used to redirect the recipient to a location other than the URI. + */ + String LOCATION = "Location"; + + /** + * Limit the number of proxies or gateways that can forward the request. + */ + String MAX_FORWARDS = "Max-Forwards"; + + /** + * Include implementation specific directives that might apply. + */ + String PRAGMA = "Pragma"; + + /** + * Challenge indicating the authentication applicable to the proxy. + */ + String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + + /** + * Allows client identification for a proxy requiring authentication. + */ + String PROXY_AUTHORIZATION = "Proxy-Authorization"; + + /** + * Specifies a range of bytes within a resource to be sent by a server. + */ + String RANGE = "Range"; + + /** + * Allows the client to specify the source address to the server. + */ + String REFERER = "Referer"; + + /** + * Response to indicate how long the service will be unavailable. + */ + String RETRY_AFTER = "Retry-After"; + + /** + * Represents the globally unique identifier sent by the client. + */ + String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + + /** + * Represents the SHA-1 digest of the clients globally unique identifier. + */ + String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; + + /** + * Specifies the protocol that should be used by the connected parties. + */ + String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + + /** + * Represents the version of the protocol that should be used. + */ + String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; + + /** + * Contains information about the software used by the origin server. + */ + String SERVER = "Server"; + + /** + * Represents some value from the server that the client should keep. + */ + String SET_COOKIE = "Set-Cookie"; + + /** + * Indicates what extension transfer codings it is willing to accept. + */ + String TE = "TE"; + + /** + * Indicates that these header fields is present in the trailer. + */ + String TRAILER = "Trailer"; + + /** + * Indicates the transformation has been applied to the message body. + */ + String TRANSFER_ENCODING = "Transfer-Encoding"; + + /** + * Specifies additional communication protocols the client supports. + */ + String UPGRADE = "Upgrade"; + + /** + * Contains information about the user agent originating the request. + */ + String USER_AGENT = "User-Agent"; + + /** + * Indicates the headers that can make a cached resource stale. + */ + String VARY = "Vary"; + + /** + * Used by gateways and proxies to indicate the intermediate protocols. + */ + String VIA = "Via"; + + /** + * Used to carry additional information about the status or body. + */ + String WARNING = "Warning"; + + /** + * Uses to challenge a client for authentication for a resource. + */ + String WWW_AUTHENTICATE = "WWW-Authenticate"; + + /** + * Represents a class of data representing an executable application. + */ + String APPLICATION = "application"; + + /** + * Represents the token used to identify a multipart boundary. + */ + String BOUNDARY = "boundary"; + + /** + * Represents the token used to identify the encoding of a message. + */ + String CHARSET = "charset"; + + /** + * Represents the name of a self delimiting transfer encoding. + */ + String CHUNKED = "chunked"; + + /** + * Specifies that the server will terminate the connection. + */ + String CLOSE = "close"; + + /** + * Represents a message type for an image such as a PNG or JPEG. + */ + String IMAGE = "image"; + + /** + * Specifies that the server wishes to keep the connection open. + */ + String KEEP_ALIVE = "keep-alive"; + + /** + * Represents a message type that contains multiple parts. + */ + String MULTIPART = "multipart"; + + /** + * Specifies that the message should not be cached by anything. + */ + String NO_CACHE = "no-cache"; + + /** + * Represents the default content type if none is specified. + */ + String OCTET_STREAM = "octet-stream"; + + /** + * Represents a message type containing human readable text. + */ + String TEXT = "text"; + + /** + * Represents a message type that contains HTML form posted data. + */ + String URL_ENCODED = "x-www-form-urlencoded"; + + /** + * This is the protocol token that is used when upgrading. + */ + String WEBSOCKET = "websocket"; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Query.java b/simple/simple-http/src/main/java/org/simpleframework/http/Query.java new file mode 100644 index 0000000..5ab8afa --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Query.java @@ -0,0 +1,99 @@ +/* + * Query.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.util.List; +import java.util.Map; + +/** + * The Query object is used to represent HTTP query + * parameters. Parameters are acquired by name and can be either a + * string, float, int, or boolean value. This ensures that data can + * be conveniently extracted in the correct type. This stores the + * parameters in a map of key value pairs. Each parameter can be + * acquired using the name of the parameter, if the parameter is + * named twice then all values can be acquired. + * + * @author Niall Gallagher + */ +public interface Query extends Map { + + /** + * This method is used to acquire a List for all of + * the parameter values associated with the specified name. Using + * this method allows the query to expose many values taken from + * the query or HTTP form posting. Typically the first value in + * the list is the value from the get(String) method + * as this is the primary value from the ordered list of values. + * + * @param name this is the name used to search for the value + * + * @return this is the list of values associated with the key + */ + List getAll(Object name); + + /** + * This extracts an integer parameter for the named value. If the + * named parameter does not exist this will return a zero value. + * If however the parameter exists but is not in the format of a + * decimal integer value then this will throw an exception. + * + * @param name the name of the parameter value to retrieve + * + * @return this returns the named parameter value as an integer + */ + int getInteger(Object name); + + /** + * This extracts a float parameter for the named value. If the + * named parameter does not exist this will return a zero value. + * If however the parameter exists but is not in the format of a + * floating point number then this will throw an exception. + * + * @param name the name of the parameter value to retrieve + * + * @return this returns the named parameter value as a float + */ + float getFloat(Object name); + + /** + * This extracts a boolean parameter for the named value. If the + * named parameter does not exist this will return false otherwise + * the value is evaluated. If it is either true or + * false then those boolean values are returned. + * + * @param name the name of the parameter value to retrieve + * + * @return this returns the named parameter value as an float + */ + boolean getBoolean(Object name); + + /** + * This will return all parameters represented using the HTTP + * URL query format. The x-www-form-urlencoded + * format is used to encode the attributes, see RFC 2616. + *

+ * This will also encode any special characters that appear + * within the name and value pairs as an escaped sequence. + * If there are no parameters an empty string is returned. + * + * @return returns an empty string if the is no parameters + */ + String toString(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Request.java b/simple/simple-http/src/main/java/org/simpleframework/http/Request.java new file mode 100644 index 0000000..2c83c28 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Request.java @@ -0,0 +1,210 @@ +/* + * Request.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.channels.ReadableByteChannel; +import java.util.List; +import java.util.Map; + +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; + +/** + * The Request is used to provide an interface to the + * HTTP entity body and message header. This provides methods that + * allow the entity body to be acquired as a stream, string, or if + * the message is a multipart encoded body, then the individual + * parts of the request body can be acquired. + *

+ * This can also maintain data during the request lifecycle as well + * as the session lifecycle. A Session is made available + * for convenience. It provides a means for the services to associate + * data with a given client session, which can be retrieved when + * there are subsequent requests sent to the server. + *

+ * It is important to note that the entity body can be read multiple + * times from the request. Calling getInputStream will + * start reading from the first byte in the body regardless of the + * number of times it is called. This allows POST parameters as well + * as multipart bodies to be read from the stream if desired. + * + * @author Niall Gallagher + */ +public interface Request extends RequestHeader { + + /** + * This is used to determine if the request has been transferred + * over a secure connection. If the protocol is HTTPS and the + * content is delivered over SSL then the request is considered + * to be secure. Also the associated response will be secure. + * + * @return true if the request is transferred securely + */ + boolean isSecure(); + + /** + * This is a convenience method that is used to determine whether + * or not this message has the Connection: close + * header. If the close token is present then this stream is not + * a keep-alive connection. If this has no Connection + * header then the keep-alive status is determined by the HTTP + * version, that is, HTTP/1.1 is keep-alive by default, HTTP/1.0 + * is not keep-alive by default. + * + * @return returns true if this has a keep-alive stream + */ + boolean isKeepAlive(); + + /** + * This is the time in milliseconds when the request was first + * read from the underlying socket. The time represented here + * represents the time collection of this request began. This + * does not necessarily represent the time the bytes arrived as + * as some data may have been buffered before it was parsed. + * + * @return this represents the time the request arrived at + */ + long getRequestTime(); + + /** + * This provides the underlying channel for the request. It + * contains the TCP socket channel and various other low level + * components. Typically this will only ever be needed when + * there is a need to switch protocols. + * + * @return the underlying channel for this request + */ + Channel getChannel(); + + /** + * This is used to acquire the SSL certificate used when the + * server is using a HTTPS connection. For plain text connections + * or connections that use a security mechanism other than SSL + * this will be null. This is only available when the connection + * makes specific use of an SSL engine to secure the connection. + * + * @return this returns the associated SSL certificate if any + */ + Certificate getClientCertificate(); + + /** + * This is used to acquire the remote client address. This can + * be used to acquire both the port and the I.P address for the + * client. It allows the connected clients to be logged and if + * require it can be used to perform course grained security. + * + * @return this returns the client address for this request + */ + InetSocketAddress getClientAddress(); + + /** + * This can be used to retrieve the response attributes. These can + * be used to keep state with the response when it is passed to + * other systems for processing. Attributes act as a convenient + * model for storing objects associated with the response. This + * also inherits attributes associated with the client connection. + * + * @return the attributes of that have been set on the request + */ + Map getAttributes(); + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the attribute Map + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param key this is the key of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + Object getAttribute(Object key); + + /** + * This is used to provide quick access to the parameters. This + * avoids having to acquire the request Form object. + * This basically acquires the parameters object and invokes + * the getParameters method with the given name. + * + * @param name this is the name of the parameter value + */ + String getParameter(String name); + + /** + * This method is used to acquire a Part from the + * HTTP request using a known name for the part. This is typically + * used when there is a file upload with a multipart POST request. + * All parts that are not files can be acquired as string values + * from the attachment object. + * + * @param name this is the name of the part object to acquire + * + * @return the named part or null if the part does not exist + */ + Part getPart(String name); + + /** + * This method is used to get all Part objects that + * are associated with the request. Each attachment contains the + * body and headers associated with it. If the request is not a + * multipart POST request then this will return an empty list. + * + * @return the list of parts associated with this request + */ + List getParts(); + + /** + * This is used to get the content body. This will essentially get + * the content from the body and present it as a single string. + * The encoding of the string is determined from the content type + * charset value. If the charset is not supported this will throw + * an exception. Typically only text values should be extracted + * using this method if there is a need to parse that content. + * + * @return this returns the message bytes as an encoded string + */ + String getContent() throws IOException; + + /** + * This is used to read the content body. The specifics of the data + * that is read from this InputStream can be determined + * by the getContentLength method. If the data sent by + * the client is chunked then it is decoded, see RFC 2616 section + * 3.6. Also multipart data is available as Part objects + * however the raw content of the multipart body is still available. + * + * @return this returns an input stream containing the message body + */ + InputStream getInputStream() throws IOException; + + /** + * This is used to read the content body. The specifics of the data + * that is read from this ReadableByteChannel can be + * determined by the getContentLength method. If the + * data sent by the client is chunked then it is decoded, see RFC + * 2616 section 3.6. This stream will never provide empty reads as + * the content is internally buffered, so this can do a full read. + * + * @return this returns the byte channel used to read the content + */ + ReadableByteChannel getByteChannel() throws IOException; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/RequestHeader.java b/simple/simple-http/src/main/java/org/simpleframework/http/RequestHeader.java new file mode 100644 index 0000000..d1ca7d0 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/RequestHeader.java @@ -0,0 +1,201 @@ +/* + * RequestHeader.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.util.List; +import java.util.Locale; + +/** + * This is a Header object that is used to represent a + * basic form for the HTTP request message. This is used to extract + * values such as the request line and header values from the request + * message. Access to header values is done case insensitively. + *

+ * As well as providing the header values and request line values + * this will also provide convenience methods which enable the user + * to determine the length of the body this message header prefixes. + * + * @author Niall Gallagher + */ +public interface RequestHeader extends RequestLine { + + /** + * This method is used to get a List of the names + * for the headers. This will provide the original names for the + * HTTP headers for the message. Modifications to the provided + * list will not affect the header, the list is a simple copy. + * + * @return this returns a list of the names within the header + */ + List getNames(); + + /** + * This can be used to get the integer of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + int getInteger(String name); + + /** + * This can be used to get the date of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + long getDate(String name); + + /** + * This is used to acquire a cookie using the name of that cookie. + * If the cookie exists within the HTTP header then it is returned + * as a Cookie object. Otherwise this method will + * return null. Each cookie object will contain the name, value + * and path of the cookie as well as the optional domain part. + * + * @param name this is the name of the cookie object to acquire + * + * @return this returns a cookie object from the header or null + */ + Cookie getCookie(String name); + + /** + * This is used to acquire all cookies that were sent in the header. + * If any cookies exists within the HTTP header they are returned + * as Cookie objects. Otherwise this method will an + * empty list. Each cookie object will contain the name, value and + * path of the cookie as well as the optional domain part. + * + * @return this returns all cookie objects from the HTTP header + */ + List getCookies(); + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if theres no HTTP message header. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name); + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if theres no HTTP message header. + * + * @param name the HTTP message header to get the value from + * @param index if there are multiple values this selects one + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name, int index); + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered array of tokens extracted from the header(s) + */ + List getValues(String name); + + /** + * This is used to acquire the locales from the request header. The + * locales are provided in the Accept-Language header. + * This provides an indication as to the languages that the client + * accepts. It provides the locales in preference order. + * + * @return this returns the locales preferred by the client + */ + List getLocales(); + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + ContentType getContentType(); + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return the content length, or -1 if it cannot be determined + */ + long getContentLength(); + + /** + * This method returns a CharSequence holding the header + * consumed for the request. A character sequence is returned as it + * can provide a much more efficient means of representing the header + * data by just wrapping the consumed byte array. + * + * @return this returns the characters consumed for the header + */ + CharSequence getHeader(); + + /** + * This method returns a string representing the header that was + * consumed for this request. For performance reasons it is better + * to acquire the character sequence representing the header as it + * does not require the allocation on new memory. + * + * @return this returns a string representation of this request + */ + String toString(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/RequestLine.java b/simple/simple-http/src/main/java/org/simpleframework/http/RequestLine.java new file mode 100644 index 0000000..b5b1abc --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/RequestLine.java @@ -0,0 +1,98 @@ +/* + * RequestLine.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * The RequestLine is used to represent a HTTP request + * line. The methods provided for this can be used to provide easy + * access to the components of a HTTP request line. For the syntax + * of a HTTP request line see RFC 2616. + * + * @author Niall Gallagher + */ +public interface RequestLine { + + /** + * This can be used to get the HTTP method for this request. The + * HTTP specification RFC 2616 specifies the HTTP request methods + * in section 9, Method Definitions. Typically this will be a + * GET, POST or a HEAD method, although any string is possible. + * + * @return the request method for this request message + */ + String getMethod(); + + /** + * This can be used to get the URI specified for this HTTP + * request. This corresponds to the /index part of a + * http://www.domain.com/index URL but may contain the full + * URL. This is a read only value for the request. + * + * @return the URI that this HTTP request is targeting + */ + String getTarget(); + + /** + * This is used to acquire the address from the request line. + * An address is the full URI including the scheme, domain, port + * and the query parts. This allows various parameters to be + * acquired without having to parse the raw request target URI. + * + * @return this returns the address of the request line + */ + Address getAddress(); + + /** + * This is used to acquire the path as extracted from the HTTP + * request URI. The Path object that is provided by + * this method is immutable, it represents the normalized path + * only part from the request uniform resource identifier. + * + * @return this returns the normalized path for the request + */ + Path getPath(); + + /** + * This method is used to acquire the query part from the + * HTTP request URI target. This will return only the values + * that have been extracted from the request URI target. + * + * @return the query associated with the HTTP target URI + */ + Query getQuery(); + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 1 + * of a HTTP/1.0 version string. + * + * @return the major version number for the request message + */ + int getMajor(); + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 0 + * of a HTTP/1.0 version string. This is used to determine if + * the request message has keep alive semantics. + * + * @return the major version number for the request message + */ + int getMinor(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/RequestWrapper.java b/simple/simple-http/src/main/java/org/simpleframework/http/RequestWrapper.java new file mode 100644 index 0000000..be81f5e --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/RequestWrapper.java @@ -0,0 +1,520 @@ +/* + * RequestWrapper.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.channels.ReadableByteChannel; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; + +/** + * The RequestWrapper object is used so that the original + * Request object can be wrapped in a filtering proxy + * object. This allows a Container that interacts with + * a modified request object. To add functionality to the request it + * can be wrapped in a subclass of this and the overridden methods + * can provide modified functionality to the standard request. + * + * @author Niall Gallagher + */ +public class RequestWrapper implements Request { + + /** + * This is the request instance that is being wrapped. + */ + protected Request request; + + /** + * Constructor for RequestWrapper object. This allows + * the original Request object to be wrapped so that + * adjustments to the behaviour of a request object handed to the + * container can be provided by a subclass implementation. + * + * @param request the request object that is being wrapped + */ + public RequestWrapper(Request request){ + this.request = request; + } + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 1 + * of a HTTP/1.0 version string. + * + * @return the major version number for the request message + */ + public int getMajor() { + return request.getMajor(); + } + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 0 + * of a HTTP/1.0 version string. This is used to determine if + * the request message has keep alive semantics. + * + * @return the major version number for the request message + */ + public int getMinor() { + return request.getMinor(); + } + + /** + * This can be used to get the HTTP method for this request. The + * HTTP specification RFC 2616 specifies the HTTP request methods + * in section 9, Method Definitions. Typically this will be a + * GET, POST or a HEAD method, although any string is possible. + * + * @return the request method for this request message + */ + public String getMethod() { + return request.getMethod(); + } + + /** + * This can be used to get the URI specified for this HTTP request. + * This corresponds to the either the full HTTP URI or the path + * part of the URI depending on how the client sends the request. + * + * @return the URI address that this HTTP request is targeting + */ + public String getTarget() { + return request.getTarget(); + } + + /** + * This is used to acquire the address from the request line. + * An address is the full URI including the scheme, domain, port + * and the query parts. This allows various parameters to be + * acquired without having to parse the raw request target URI. + * + * @return this returns the address of the request line + */ + public Address getAddress() { + return request.getAddress(); + } + + /** + * This is used to acquire the path as extracted from the HTTP + * request URI. The Path object that is provided by + * this method is immutable, it represents the normalized path + * only part from the request uniform resource identifier. + * + * @return this returns the normalized path for the request + */ + public Path getPath() { + return request.getPath(); + } + + /** + * This method is used to acquire the query part from the HTTP + * request URI target and a form post if it exists. Both the + * query and the form post are merge together in a single query. + * + * @return the query associated with the HTTP target URI + */ + public Query getQuery() { + return request.getQuery(); + } + + /** + * This method is used to get a List of the names + * for the headers. This will provide the original names for the + * HTTP headers for the message. Modifications to the provided + * list will not affect the header, the list is a simple copy. + * + * @return this returns a list of the names within the header + */ + public List getNames() { + return request.getNames(); + } + + /** + * This can be used to get the integer of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public int getInteger(String name) { + return request.getInteger(name); + } + + /** + * This can be used to get the date of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public long getDate(String name) { + return request.getDate(name); + } + + /** + * This is used to acquire a cookie usiing the name of that cookie. + * If the cookie exists within the HTTP header then it is returned + * as a Cookie object. Otherwise this method will + * return null. Each cookie object will contain the name, value + * and path of the cookie as well as the optional domain part. + * + * @param name this is the name of the cookie object to acquire + * + * @return this returns a cookie object from the header or null + */ + public Cookie getCookie(String name) { + return request.getCookie(name); + } + + /** + * This is used to acquire all cookies that were sent in the header. + * If any cookies exists within the HTTP header they are returned + * as Cookie objects. Otherwise this method will an + * empty list. Each cookie object will contain the name, value and + * path of the cookie as well as the optional domain part. + * + * @return this returns all cookie objects from the HTTP header + */ + public List getCookies() { + return request.getCookies(); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma seperated list of + * values the value returned is the first value in that list. + * This returns null if theres no HTTP message header. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name) { + return request.getValue(name); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if theres no HTTP message header. + * + * @param name the HTTP message header to get the value from + * @param index if there are multiple values this selects one + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name, int index) { + return request.getValue(name, index); + } + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benifits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearence. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has higest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered array of tokens extracted from the header(s) + */ + public List getValues(String name) { + return request.getValues(name); + } + + /** + * This is used to acquire the locales from the request header. The + * locales are provided in the Accept-Language header. + * This provides an indication as to the languages that the client + * accepts. It provides the locales in preference order. + * + * @return this returns the locales preferred by the client + */ + public List getLocales() { + return request.getLocales(); + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + public ContentType getContentType() { + return request.getContentType(); + } + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return the content length, or -1 if it cannot be determined + */ + public long getContentLength() { + return request.getContentLength(); + } + + /** + * This is used to determine if the request has been transferred + * over a secure connection. If the protocol is HTTPS and the + * content is delivered over SSL then the request is considered + * to be secure. Also the associated response will be secure. + * + * @return true if the request is transferred securely + */ + public boolean isSecure() { + return request.isSecure(); + } + + /** + * This is a convenience method that is used to determine whether + * or not this message has the Connection: close + * header. If the close token is present then this stream is not + * a keep-alive connection. If this has no Connection + * header then the keep-alive status is determined by the HTTP + * version, that is, HTTP/1.1 is keep-alive by default, HTTP/1.0 + * is not keep-alive by default. + * + * @return returns true if this has a keep-alive stream + */ + public boolean isKeepAlive() { + return request.isKeepAlive(); + } + + /** + * This is the time in milliseconds when the request was first + * read from the underlying socket. The time represented here + * represents the time collection of this request began. This + * does not necessarily represent the time the bytes arrived as + * as some data may have been buffered before it was parsed. + * + * @return this represents the time the request arrived at + */ + public long getRequestTime() { + return request.getRequestTime(); + } + + /** + * This provides the underlying channel for the request. It + * contains the TCP socket channel and various other low level + * components. Typically this will only ever be needed when + * there is a need to switch protocols. + * + * @return the underlying channel for this request + */ + public Channel getChannel() { + return request.getChannel(); + } + + /** + * This is used to acquire the SSL certificate used when the + * server is using a HTTPS connection. For plain text connections + * or connections that use a security mechanism other than SSL + * this will be null. This is only available when the connection + * makes specific use of an SSL engine to secure the connection. + * + * @return this returns the associated SSL certificate if any + */ + public Certificate getClientCertificate() { + return request.getClientCertificate(); + } + + /** + * This can be used to retrieve the response attributes. These can + * be used to keep state with the response when it is passed to + * other systems for processing. Attributes act as a convenient + * model for storing objects associated with the response. This + * also inherits attributes associated with the client connection. + * + * @return the attributes that have been set on this response + */ + public Map getAttributes() { + return request.getAttributes(); + } + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the attribute Map + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param key this is the key of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + public Object getAttribute(Object key) { + return request.getAttribute(key); + } + + /** + * This is used to acquire the remote client address. This can + * be used to acquire both the port and the I.P address for the + * client. It allows the connected clients to be logged and if + * require it can be used to perform course grained security. + * + * @return this returns the client address for this request + */ + public InetSocketAddress getClientAddress() { + return request.getClientAddress(); + } + + /** + * This method returns a CharSequence holding the header + * consumed for the request. A character sequence is returned as it + * can provide a much more efficient means of representing the header + * data by just wrapping the consumed byte array. + * + * @return this returns the characters consumed for the header + */ + public CharSequence getHeader() { + return request.getHeader(); + } + + /** + * This is used to get the content body. This will essentially get + * the content from the body and present it as a single string. + * The encoding of the string is determined from the content type + * charset value. If the charset is not supported this will throw + * an exception. Typically only text values should be extracted + * using this method if there is a need to parse that content. + * + * @exception IOException signifies that there is an I/O problem + * + * @return the body content as an encoded string value + */ + public String getContent() throws IOException { + return request.getContent(); + } + + /** + * This is used to read the content body. The specifics of the data + * that is read from this InputStream can be determined + * by the getContentLength method. If the data sent by + * the client is chunked then it is decoded, see RFC 2616 section + * 3.6. Also multipart data is available as Part objects + * however the raw content of the multipart body is still available. + * + * @exception Exception signifies that there is an I/O problem + * + * @return returns the input stream containing the message body + */ + public InputStream getInputStream() throws IOException { + return request.getInputStream(); + } + + /** + * This is used to read the content body. The specifics of the data + * that is read from this ReadableByteChannel can be + * determined by the getContentLength method. If the + * data sent by the client is chunked then it is decoded, see RFC + * 2616 section 3.6. This stream will never provide empty reads as + * the content is internally buffered, so this can do a full read. + * + * @return this returns the byte channel used to read the content + */ + public ReadableByteChannel getByteChannel() throws IOException { + return request.getByteChannel(); + } + + /** + * This is used to provide quick access to the parameters. This + * avoids having to acquire the request Form object. + * This basically acquires the parameters object and invokes + * the getParameters method with the given name. + * + * @param name this is the name of the parameter value + */ + public String getParameter(String name) { + return request.getParameter(name); + } + + /** + * This method is used to acquire a Part from the + * HTTP request using a known name for the part. This is typically + * used when there is a file upload with a multipart POST request. + * All parts that are not files can be acquired as string values + * from the attachment object. + * + * @param name this is the name of the part object to acquire + * + * @return the named part or null if the part does not exist + */ + public Part getPart(String name) { + return request.getPart(name); + } + + /** + * This method is used to get all Part objects that + * are associated with the request. Each attachment contains the + * body and headers associated with it. If the request is not a + * multipart POST request then this will return an empty list. + * + * @return the list of parts associated with this request + */ + public List getParts() { + return request.getParts(); + } + + /** + * This method returns a string representing the header that was + * consumed for this request. For performance reasons it is better + * to acquire the character sequence representing the header as it + * does not require the allocation on new memory. + * + * @return this returns a string representation of this request + */ + public String toString() { + return request.toString(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Response.java b/simple/simple-http/src/main/java/org/simpleframework/http/Response.java new file mode 100644 index 0000000..e9e54da --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Response.java @@ -0,0 +1,262 @@ +/* + * Response.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.channels.WritableByteChannel; + +/** + * This is used to represent the HTTP response. This provides methods + * that can be used to set various characteristics of the response. + * An OutputStream can be acquired via this interface + * which can be used to write the response body. A buffer size can be + * specified when acquiring the output stream which allows data to + * be buffered until it over flows or is flushed explicitly. This + * buffering allows a partially written response body to be reset. + *

+ * This should never allow the message body be sent if it should not + * be sent with the headers as of RFC 2616 rules for the presence of + * a message body. A message body must not be included with a HEAD + * request or with a 304 or a 204 response. A proper implementation + * of this will prevent a message body being sent if the response + * is to a HEAD request of if there is a 304 or 204 response code. + *

+ * It is important to note that the Response controls + * the processing of the HTTP pipeline. The next HTTP request is + * not processed until the response has been sent. To ensure that + * the response is sent the close method of the response + * or the output stream should be used. This will notify the server + * to dispatch the next request in the pipeline for processing. + * + * @author Niall Gallagher + */ +public interface Response extends ResponseHeader { + + /** + * This should be used when the size of the message body is known. + * This ensures that Persistent HTTP (PHTTP) connections can be + * maintained for both HTTP/1.0 and HTTP/1.1 clients. If the length + * of the output is not known HTTP/1.0 clients will require a + * connection close, which reduces performance (see RFC 2616). + *

+ * This removes any previous Content-Length headers from the message + * header. This will then set the appropriate Content-Length header + * with the correct length. If a the Connection header is set with the + * close token then the semantics of the connection are such that the + * server will close it once the output stream or request is closed. + * + * @param length this is the length of the HTTP message body + */ + void setContentLength(long length); + + /** + * This is used to set the content type for the response. Typically + * a response will contain a message body of some sort. This is used + * to conveniently set the type for that response. Setting the + * content type can also be done explicitly if desired. + * + * @param type this is the type that is to be set in the response + */ + void setContentType(String type); + + /** + * Used to write a message body with the Response. The + * semantics of this OutputStream will be determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + * + * @return an output stream object with the specified semantics + */ + OutputStream getOutputStream() throws IOException; + + /** + * Used to write a message body with the Response. The + * semantics of this OutputStream will be determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + *

+ * This will ensure that there is buffering done so that the output + * can be reset using the reset method. This will + * enable the specified number of bytes to be written without + * committing the response. This specified size is the minimum size + * that the response buffer must be. + * + * @return an output stream object with the specified semantics + */ + OutputStream getOutputStream(int size) throws IOException; + + /** + * This method is provided for convenience so that the HTTP content + * can be written using the print methods provided by + * the PrintStream. This will basically wrap the + * getOutputStream with a buffer size of zero. + *

+ * The retrieved PrintStream uses the charset used to + * describe the content, with the Content-Type header. This will + * check the charset parameter of the contents MIME type. So if + * the Content-Type was text/plain; charset=UTF-8 the + * resulting PrintStream would encode the written data + * using the UTF-8 encoding scheme. Care must be taken to ensure + * that bytes written to the stream are correctly encoded. + *

+ * Implementations of the Response must guarantee + * that this can be invoked repeatedly without effecting any issued + * OutputStream or PrintStream object. + * + * @return a print stream that provides convenience writing + */ + PrintStream getPrintStream() throws IOException; + + /** + * This method is provided for convenience so that the HTTP content + * can be written using the print methods provided by + * the PrintStream. This will basically wrap the + * getOutputStream with a specified buffer size. + *

+ * The retrieved PrintStream uses the charset used to + * describe the content, with the Content-Type header. This will + * check the charset parameter of the contents MIME type. So if + * the Content-Type was text/plain; charset=UTF-8 the + * resulting PrintStream would encode the written data + * using the UTF-8 encoding scheme. Care must be taken to ensure + * that bytes written to the stream are correctly encoded. + *

+ * Implementations of the Response must guarantee + * that this can be invoked repeatedly without effecting any issued + * OutputStream or PrintStream object. + * + * @param size the minimum size that the response buffer must be + * + * @return a print stream that provides convenience writing + */ + PrintStream getPrintStream(int size) throws IOException; + + /** + * Used to write a message body with the Response. The + * semantics of this WritableByteChannel are determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + * + * @return a writable byte channel used to write the message body + */ + WritableByteChannel getByteChannel() throws IOException; + + /** + * Used to write a message body with the Response. The + * semantics of this WritableByteChannel are determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + *

+ * This will ensure that there is buffering done so that the output + * can be reset using the reset method. This will + * enable the specified number of bytes to be written without + * committing the response. This specified size is the minimum size + * that the response buffer must be. + * + * @param size the minimum size that the response buffer must be + * + * @return a writable byte channel used to write the message body + */ + WritableByteChannel getByteChannel(int size) throws IOException; + + /** + * This represents the time at which the response has fully written. + * Because the response is delivered asynchronously to the client + * this response time does not represent the time to last byte. + * It simply represents the time at which the response has been + * fully generated and written to the output buffer or queue. This + * returns zero if the response has not finished. + * + * @return this is the time taken to complete the response + */ + long getResponseTime(); + + /** + * This is used to determine if the HTTP response message is a + * keep alive message or if the underlying socket was closed. Even + * if the client requests a connection keep alive and supports + * persistent connections, the response can still be closed by + * the server. This can be explicitly indicated by the presence + * of the Connection HTTP header, it can also be + * implicitly indicated by using version HTTP/1.0. + * + * @return this returns true if the connection was closed + */ + boolean isKeepAlive(); + + /** + * This can be used to determine whether the Response + * has been committed. This is true if the Response + * was committed, either due to an explicit invocation of the + * commit method or due to the writing of content. If + * the Response has committed the reset + * method will not work in resetting content already written. + * + * @return true if the response headers have been committed + */ + boolean isCommitted(); + + /** + * This is used to write the headers that where given to the + * Response. Any further attempts to give headers + * to the Response will be futile as only the headers + * that were given at the time of the first commit will be used + * in the message header. + *

+ * This also performs some final checks on the headers submitted. + * This is done to determine the optimal performance of the + * output. If no specific Connection header has been specified + * this will set the connection so that HTTP/1.0 closes by default. + * + * @exception IOException thrown if there was a problem writing + */ + void commit() throws IOException; + + /** + * This can be used to determine whether the Response + * has been committed. This is true if the Response + * was committed, either due to an explicit invocation of the + * commit method or due to the writing of content. If + * the Response has committed the reset + * method will not work in resetting content already written. + * + * @throws IOException thrown if there is a problem resetting + */ + void reset() throws IOException; + + /** + * This is used to close the connection and commit the request. + * This provides the same semantics as closing the output stream + * and ensures that the HTTP response is committed. This will + * throw an exception if the response can not be committed. + * + * @throws IOException thrown if there is a problem writing + */ + void close() throws IOException; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/ResponseHeader.java b/simple/simple-http/src/main/java/org/simpleframework/http/ResponseHeader.java new file mode 100644 index 0000000..5b36994 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/ResponseHeader.java @@ -0,0 +1,304 @@ +/* + * Response.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.util.List; + +/** + * The ResponseHeader object is used to manipulate the + * header information for a given response. Headers are stored and + * retrieved from this object in a case insensitive manner. This + * implements the StatusLine object, which exposes the + * protocol version and response status code. + *

+ * All cookies set on the response header will be delivered as a + * Set-Cookie header in the response message. The Content-Length and + * Transfer-Encoding headers can be set to configure how the message + * body is delivered to the connected client. + * + * @author Niall Gallagher + */ +public interface ResponseHeader extends StatusLine { + + /** + * This is used to acquire the names of the of the headers that + * have been set in the response. This can be used to acquire all + * header values by name that have been set within the response. + * If no headers have been set this will return an empty list. + * + * @return a list of strings representing the set header names + */ + List getNames(); + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void addValue(String name, String value); + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getInteger in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void addInteger(String name, int value); + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTPdate string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + void addDate(String name, long date); + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void setValue(String name, String value); + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void setInteger(String name, int value); + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void setLong(String name, long value); + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTP date string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + void setDate(String name, long date); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * @param index used if there are multiple headers present + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name, int index); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the integer + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + int getInteger(String name); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the long value + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + long getDate(String name); + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered list of tokens extracted from the header(s) + */ + List getValues(String name); + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * + * @param cookie this is the cookie to be added to the response + * + * @return returns the cookie that has been set in the response + */ + Cookie setCookie(Cookie cookie); + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * This is a convenience method that avoids cookie creation. + * + * @param name this is the cookie to be added to the response + * @param value this is the cookie value that is to be used + * + * @return returns the cookie that has been set in the response + */ + Cookie setCookie(String name, String value); + + /** + * This returns the Cookie object stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If the cookie does + * not exist under the specified name this will return null. + * + * @param name this is the name of the cookie to be retrieved + * + * @return returns the Cookie by the given name + */ + Cookie getCookie(String name); + + /** + * This returns all Cookie objects stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If there are no + * cookies then this will return an empty list. + * + * @return returns all the Cookie in the response + */ + List getCookies(); + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + ContentType getContentType(); + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Transfer-Encoding header, if there is + * then this will parse that header and return the first token in + * the comma separated list of values, which is the primary value. + * + * @return this returns the transfer encoding value if it exists + */ + String getTransferEncoding(); + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return content length, or -1 if it cannot be determined + */ + long getContentLength(); + + /** + * This method returns a CharSequence holding the header + * created for the request. A character sequence is returned as it + * can provide a much more efficient means of representing the header + * data by just wrapping the the data generated. + * + * @return this returns the characters generated for the header + */ + CharSequence getHeader(); + + /** + * This method returns a string representing the header that was + * generated for this header. For performance reasons it is better + * to acquire the character sequence representing the header as it + * does not require the allocation on new memory. + * + * @return this returns a string representation of this response + */ + String toString(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/ResponseWrapper.java b/simple/simple-http/src/main/java/org/simpleframework/http/ResponseWrapper.java new file mode 100644 index 0000000..240384c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/ResponseWrapper.java @@ -0,0 +1,747 @@ +/* + * ResponseWrapper.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.channels.WritableByteChannel; +import java.util.List; + +/** + * The ResponseWrapper object is used so that the original + * Response object can be wrapped in a filtering proxy + * object. This allows a container to interact with an implementation + * of this with overridden methods providing specific functionality. + * the Response object in a concurrent environment. + *

+ *
+ *    public void handle(Request req, Response resp) {
+ *       handler.handle(req, new ZipResponse(resp));
+ *    }
+ *
+ * 
+ * The above is an example of how the ResponseWrapper can + * be used to provide extra functionality to a Response + * in a transparent manner. Such an implementation could apply a + * Content-Encoding header and compress the response for performance + * over a slow network. Filtering can be applied with the use of + * layered Container objects. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.Container + */ +public class ResponseWrapper implements Response { + + /** + * This is the response instance that is being wrapped. + */ + protected Response response; + + /** + * Constructor for ResponseWrapper object. This allows + * the original Response object to be wrapped so that + * adjustments to the behavior of a request object handed to the + * container can be provided by a subclass implementation. + * + * @param response the response object that is being wrapped + */ + public ResponseWrapper(Response response){ + this.response = response; + } + + /** + * This represents the status code of the HTTP response. + * The response code represents the type of message that is + * being sent to the client. For a description of the codes + * see RFC 2616 section 10, Status Code Definitions. + * + * @return the status code that this HTTP response has + */ + public int getCode() { + return response.getCode(); + } + + /** + * This method allows the status for the response to be + * changed. This MUST be reflected the the response content + * given to the client. For a description of the codes see + * RFC 2616 section 10, Status Code Definitions. + * + * @param code the new status code for the HTTP response + */ + public void setCode(int code) { + response.setCode(code); + } + + /** + * This can be used to retrieve the text of a HTTP status + * line. This is the text description for the status code. + * This should match the status code specified by the RFC. + * + * @return the message description of the response + */ + public String getDescription() { + return response.getDescription(); + } + + /** + * This is used to set the text of the HTTP status line. + * This should match the status code specified by the RFC. + * + * @param text the descriptive text message of the status + */ + public void setDescription(String text) { + response.setDescription(text); + } + + /** + * This is used to acquire the status from the response. + * The Status object returns represents the + * code that has been set on the response, it does not + * necessarily represent the description in the response. + * + * @return this is the response for this status line + */ + public Status getStatus() { + return response.getStatus(); + } + + /** + * This is used to set the status code and description + * for this response. Setting the code and description in + * this manner provides a much more convenient way to set + * the response status line details. + * + * @param status this is the status to set on the response + */ + public void setStatus(Status status) { + response.setStatus(status); + } + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 1 + * of a HTTP/1.0 version string. + * + * @return the major version number for the request message + */ + public int getMajor() { + return response.getMajor(); + } + + /** + * This can be used to set the major number from a HTTP version. + * The major version corresponds to the major type that is the 1 + * of a HTTP/1.0 version string. + * + * @param major the major version number for the request message + */ + public void setMajor(int major) { + response.setMajor(major); + } + + /** + * This can be used to get the minor number from a HTTP version. + * The minor version corresponds to the major type that is the 0 + * of a HTTP/1.0 version string. This is used to determine if + * the request message has keep alive semantics. + * + * @return the minor version number for the request message + */ + public int getMinor() { + return response.getMinor(); + } + + /** + * This can be used to get the minor number from a HTTP version. + * The minor version corresponds to the major type that is the 0 + * of a HTTP/1.0 version string. This is used to determine if + * the request message has keep alive semantics. + * + * @param minor the minor version number for the request message + */ + public void setMinor(int minor) { + response.setMinor(minor); + } + + /** + * This represents the time at which the response has fully written. + * Because the response is delivered asynchronously to the client + * this response time does not represent the time to last byte. + * It simply represents the time at which the response has been + * fully generated and written to the output buffer or queue. This + * returns zero if the response has not finished. + * + * @return this is the time taken to complete the response + */ + public long getResponseTime() { + return response.getResponseTime(); + } + + /** + * This is used to acquire the names of the of the headers that + * have been set in the response. This can be used to acquire all + * header values by name that have been set within the response. + * If no headers have been set this will return an empty list. + * + * @return a list of strings representing the set header names + */ + public List getNames() { + return response.getNames(); + } + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void addValue(String name, String value) { + response.addValue(name, value); + } + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getInteger in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void addInteger(String name, int value) { + response.addInteger(name, value); + } + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTPdate string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + public void addDate(String name, long date) { + response.addDate(name, date); + } + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void setValue(String name, String value) { + response.setValue(name, value); + } + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void setInteger(String name, int value) { + response.setInteger(name, value); + } + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void setLong(String name, long value) { + response.setLong(name, value); + } + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTP date string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + public void setDate(String name, long date) { + response.setDate(name, date); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name) { + return response.getValue(name); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * @param index used if there are multiple headers present + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name, int index) { + return response.getValue(name, index); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the integer + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public int getInteger(String name) { + return response.getInteger(name); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the long value + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public long getDate(String name) { + return response.getDate(name); + } + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered list of tokens extracted from the header(s) + */ + public List getValues(String name) { + return response.getValues(name); + } + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * + * @param cookie this is the cookie to be added to the response + * + * @return returns the cookie that has been set in the response + */ + public Cookie setCookie(Cookie cookie) { + return response.setCookie(cookie); + } + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * This is a convenience method that avoids cookie creation. + * + * @param name this is the cookie to be added to the response + * @param value this is the cookie value that is to be used + * + * @return returns the cookie that has been set in the response + */ + public Cookie setCookie(String name, String value) { + return response.setCookie(name, value); + } + + /** + * This returns the Cookie object stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If the cookie does + * not exist under the specified name this will return null. + * + * @param name this is the name of the cookie to be retrieved + * + * @return returns the cookie object send with the request + */ + public Cookie getCookie(String name) { + return response.getCookie(name); + } + + /** + * This returns all Cookie objects stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If there are no + * cookies then this will return an empty list. + * + * @return returns all the cookie objects for this response + */ + public List getCookies() { + return response.getCookies(); + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + public ContentType getContentType() { + return response.getContentType(); + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Transfer-Encoding header, if there is + * then this will parse that header and return the first token in + * the comma separated list of values, which is the primary value. + * + * @return this returns the transfer encoding value if it exists + */ + public String getTransferEncoding() { + return response.getTransferEncoding(); + } + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return content length, or -1 if it cannot be determined + */ + public long getContentLength() { + return response.getContentLength(); + } + + /** + * This should be used when the size of the message body is known. For + * performance reasons this should be used so the length of the output + * is known. This ensures that Persistent HTTP (PHTTP) connections + * can be maintained for both HTTP/1.0 and HTTP/1.1 clients. If the + * length of the output is not known HTTP/1.0 clients will require a + * connection close, which reduces performance (see RFC 2616). + *

+ * This removes any previous Content-Length headers from the message + * header. This will then set the appropriate Content-Length header with + * the correct length. If a the Connection header is set with the close + * token then the semantics of the connection are such that the server + * will close it once the OutputStream.close is used. + * + * @param length this is the length of the HTTP message body + */ + public void setContentLength(long length) { + response.setContentLength(length); + } + + /** + * This is used to set the content type for the response. Typically + * a response will contain a message body of some sort. This is used + * to conveniently set the type for that response. Setting the + * content type can also be done explicitly if desired. + * + * @param type this is the type that is to be set in the response + */ + public void setContentType(String type) { + response.setContentType(type); + } + + /** + * This method returns a CharSequence holding the header + * created for the request. A character sequence is returned as it + * can provide a much more efficient means of representing the header + * data by just wrapping the the data generated. + * + * @return this returns the characters generated for the header + */ + public CharSequence getHeader() { + return response.getHeader(); + } + + /** + * Used to write a message body with the Response. The + * semantics of this OutputStream will be determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + * The OutputStream issued must be thread safe so that + * it can be used in a concurrent environment. + * + * @exception IOException this is thrown if there was an I/O error + * + * @return an output stream used to write the response body + */ + public OutputStream getOutputStream() throws IOException { + return response.getOutputStream(); + } + + /** + * Used to write a message body with the Response. The + * semantics of this OutputStream will be determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + * The OutputStream issued must be thread safe so that + * it can be used in a concurrent environment. + *

+ * This will ensure that there is buffering done so that the output + * can be reset using the reset method. This will + * enable the specified number of bytes to be written without + * committing the response. This specified size is the minimum size + * that the response buffer must be. + * + * @param size the minimum size that the response buffer must be + * + * @return an output stream used to write the response body + * + * @exception IOException this is thrown if there was an I/O error + */ + public OutputStream getOutputStream(int size) throws IOException { + return response.getOutputStream(size); + } + + /** + * This method is provided for convenience so that the HTTP content + * can be written using the print methods provided by + * the PrintStream. This will basically wrap the + * getOutputStream with a buffer size of zero. + *

+ * The retrieved PrintStream uses the charset used to + * describe the content, with the Content-Type header. This will + * check the charset parameter of the contents MIME type. So if + * the Content-Type was text/plain; charset=UTF-8 the + * resulting PrintStream would encode the written data + * using the UTF-8 encoding scheme. Care must be taken to ensure + * that bytes written to the stream are correctly encoded. + *

+ * Implementations of the Response must guarantee + * that this can be invoked repeatedly without effecting any issued + * OutputStream or PrintStream object. + * + * @return a print stream used for writing the response body + * + * @exception IOException this is thrown if there was an I/O error + */ + public PrintStream getPrintStream() throws IOException { + return response.getPrintStream(); + } + + /** + * This method is provided for convenience so that the HTTP content + * can be written using the print methods provided by + * the PrintStream. This will basically wrap the + * getOutputStream with a specified buffer size. + *

+ * The retrieved PrintStream uses the charset used to + * describe the content, with the Content-Type header. This will + * check the charset parameter of the contents MIME type. So if + * the Content-Type was text/plain; charset=UTF-8 the + * resulting PrintStream would encode the written data + * using the UTF-8 encoding scheme. Care must be taken to ensure + * that bytes written to the stream are correctly encoded. + *

+ * Implementations of the Response must guarantee + * that this can be invoked repeatedly without effecting any issued + * OutputStream or PrintStream object. + * + * @param size the minimum size that the response buffer must be + * + * @return a print stream used for writing the response body + * + * @exception IOException this is thrown if there was an I/O error + */ + public PrintStream getPrintStream(int size) throws IOException { + return response.getPrintStream(size); + } + + /** + * Used to write a message body with the Response. The + * semantics of this WritableByteChannel are determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + * + * @return a writable byte channel used to write the message body + */ + public WritableByteChannel getByteChannel() throws IOException { + return response.getByteChannel(); + } + + /** + * Used to write a message body with the Response. The + * semantics of this WritableByteChannel are determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + *

+ * This will ensure that there is buffering done so that the output + * can be reset using the reset method. This will + * enable the specified number of bytes to be written without + * committing the response. This specified size is the minimum size + * that the response buffer must be. + * + * @param size the minimum size that the response buffer must be + * + * @return a writable byte channel used to write the message body + */ + public WritableByteChannel getByteChannel(int size) throws IOException { + return response.getByteChannel(size); + } + + /** + * This is used to determine if the HTTP response message is a + * keep alive message or if the underlying socket was closed. Even + * if the client requests a connection keep alive and supports + * persistent connections, the response can still be closed by + * the server. This can be explicitly indicated by the presence + * of the Connection HTTP header, it can also be + * implicitly indicated by using version HTTP/1.0. + * + * @return this returns true if the connection was closed + */ + public boolean isKeepAlive() { + return response.isKeepAlive(); + } + + /** + * This can be used to determine whether the Response + * has been committed. This is true if the Response + * was committed, either due to an explicit invocation of the + * commit method or due to the writing of content. If + * the Response has committed the reset + * method will not work in resetting content already written. + * + * @return true if the response has been fully committed + */ + public boolean isCommitted() { + return response.isCommitted(); + } + + /** + * This is used to write the headers that where given to the + * Response. Any further attempts to give headers + * to the Response will be futile as only the headers + * that were given at the time of the first commit will be used + * in the message header. + *

+ * This also performs some final checks on the headers submitted. + * This is done to determine the optimal performance of the + * output. If no specific Connection header has been specified + * this will set the connection so that HTTP/1.0 closes by default. + * + * @exception IOException thrown if there was a problem writing + */ + public void commit() throws IOException { + response.commit(); + } + + /** + * This can be used to determine whether the Response + * has been committed. This is true if the Response + * was committed, either due to an explicit invocation of the + * commit method or due to the writing of content. If + * the Response has committed the reset + * method will not work in resetting content already written. + * + * @throws IOException thrown if there is a problem resetting + */ + public void reset() throws IOException { + response.reset(); + } + + /** + * This is used to close the connection and commit the request. + * This provides the same semantics as closing the output stream + * and ensures that the HTTP response is committed. This will + * throw an exception if the response can not be committed. + * + * @throws IOException thrown if there is a problem writing + */ + public void close() throws IOException { + response.close(); + } + + /** + * This method returns a string representing the header that was + * generated for this header. For performance reasons it is better + * to acquire the character sequence representing the header as it + * does not require the allocation on new memory. + * + * @return this returns a string representation of this response + */ + public String toString() { + return response.toString(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Scheme.java b/simple/simple-http/src/main/java/org/simpleframework/http/Scheme.java new file mode 100644 index 0000000..b6df799 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Scheme.java @@ -0,0 +1,136 @@ +/* +* Scheme.java February 2014 +* +* Copyright (C) 2014, Niall Gallagher +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. +*/ + +package org.simpleframework.http; + +import java.net.URI; + +/** +* The Scheme represents a scheme used for a URI. Here + * only schemes that directly relate to HTTP are provided, which + * includes HTTP/1.1 schemes and WebSocket 1.0 schemes. + * + * @author Niall Gallagher +*/ +public enum Scheme { + + /** + * This represents the scheme for a plaintext HTTP connection. + */ + HTTP("http", false), + + /** + * This represents the scheme for a HTTP over TLS connection. + */ + HTTPS("https", true), + + /** + * This represents the scheme for a plaintext WebSocket connection. + */ + WS("ws", false), + + /** + * This represents the scheme for WebSocket over TLS connection. + */ + WSS("wss", true); + + /** + * This is the actual scheme token that is to be used in the URI. + */ + public final String scheme; + + /** + * This is used to determine if the connection is secure or not. + */ + public final boolean secure; + + /** + * Constructor for the Scheme object. This is used + * create an entry using the specific scheme token and a boolean + * indicating if the scheme is secure or not. + * + * @param scheme this is the scheme token to be used + * @param secure this determines if the scheme is secure or not + */ + private Scheme(String scheme, boolean secure) { + this.scheme = scheme; + this.secure = secure; + } + + /** + * This is used to determine if the scheme is secure or not. In + * general a secure scheme is one sent over a SSL/TLS connection. + * + * @return this returns true if the scheme is a secure one + */ + public boolean isSecure() { + return secure; + } + + /** + * This is used to acquire the scheme token for this. The scheme + * token can be used to prefix a absolute fully qualified URI. + * + * @return the scheme token representing this scheme + */ + public String getScheme() { + return scheme; + } + + /** + * This is used to resolve the scheme given a token. If there is + * no matching scheme for the provided token a default of HTTP + * is provided. + * + * @param token this is the token used to determine the scheme + * + * @return this returns the match or HTTP if none matched + */ + public static Scheme resolveScheme(String token) { + if(token != null) { + for(Scheme scheme : values()) { + if(token.equalsIgnoreCase(scheme.scheme)) { + return scheme; + } + } + } + return HTTP; + } + + /** + * This is used to resolve the scheme given a URI. If + * there is no matching scheme for the provided instance then this + * will return null. + * + * @param token this is the object to resolve a scheme for + * + * @return this returns the match or null if none matched + */ + public static Scheme resolveScheme(URI target) { + if(target != null) { + String scheme = target.getScheme(); + + for(Scheme option : values()) { + if(option.scheme.equalsIgnoreCase(scheme)) { + return option; + } + } + } + return null; + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/Status.java b/simple/simple-http/src/main/java/org/simpleframework/http/Status.java new file mode 100644 index 0000000..7fa3b6f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/Status.java @@ -0,0 +1,320 @@ +/* + * Status.java February 2008 + * + * Copyright (C) 2008, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * The Status enumeration is used to specify status codes + * and the descriptions of those status codes. This is a convenience + * enumeration that allows users to acquire the descriptions of codes + * by simply providing the code. Also if the response state is known + * the code and description can be provided to the client. + *

+ * The official HTTP status codes are defined in RFC 2616 section 10. + * Each set of status codes belongs to a specific family. Each family + * describes a specific scenario. Although it is possible to use other + * status codes it is recommended that servers restrict their status + * code responses to those specified in this enumeration. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.StatusLine + */ +public enum Status { + + /** + * This is used as an intermediate response to a request. + */ + CONTINUE(100, "Continue"), + + /** + * This represents a change in the protocol the client is using. + */ + SWITCHING_PROTOCOLS(101, "Switching Protocols"), + + /** + * This represents a successful response of a targeted request. + */ + OK(200, "OK"), + + /** + * This is used to signify that a resource was created successfully. + */ + CREATED(201, "Created"), + + /** + * This is used to signify that the request has been accepted. + */ + ACCEPTED(202, "Accepted"), + + /** + * This represents a response that contains no response content. + */ + NO_CONTENT(204, "No Content"), + + /** + * This is used to represent a response that resets the content. + */ + RESET_CONTENT(205, "Reset Content"), + + /** + * This is used to represent a response that has partial content. + */ + PARTIAL_CONTENT(206, "Partial Content"), + + /** + * This is used to represent a response where there are choices. + */ + MULTIPLE_CHOICES(300, "Multiple Choices"), + + /** + * This is used to represent a target resource that has moved. + */ + MOVED_PERMANENTLY(301, "Moved Permanently"), + + /** + * This is used to represent a resource that has been found. + */ + FOUND(302, "Found"), + + /** + * This is used to tell the client to see another HTTP resource. + */ + SEE_OTHER(303, "See Other"), + + /** + * This is used in response to a target that has not been modified. + */ + NOT_MODIFIED(304, "Not Modified"), + + /** + * This is used to tell the client that it should use a proxy. + */ + USE_PROXY(305, "Use Proxy"), + + /** + * This is used to redirect the client to a resource that has moved. + */ + TEMPORARY_REDIRECT(307, "Temporary Redirect"), + + /** + * This is used to tell the client they have send an invalid request. + */ + BAD_REQUEST(400, "Bad Request"), + + /** + * This is used to tell the client that authorization is required. + */ + UNAUTHORIZED(401, "Unauthorized"), + + /** + * This is used to tell the client that payment is required. + */ + PAYMENT_REQUIRED(402, "Payment Required"), + + /** + * This is used to tell the client that the resource is forbidden. + */ + FORBIDDEN(403, "Forbidden"), + + /** + * This is used to tell the client that the resource is not found. + */ + NOT_FOUND(404, "Not Found"), + + /** + * This is used to tell the client that the method is not allowed. + */ + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + + /** + * This is used to tell the client the request is not acceptable. + */ + NOT_ACCEPTABLE(406, "Not Acceptable"), + + /** + * This is used to tell the client that authentication is required. + */ + PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"), + + /** + * This is used to tell the client that the request has timed out. + */ + REQUEST_TIMEOUT(408, "Request Timeout"), + + /** + * This is used to tell the client that there has been a conflict. + */ + CONFLICT(409, "Conflict"), + + /** + * This is used to tell the client that the resource has gone. + */ + GONE(410, "Gone"), + + /** + * This is used to tell the client that a request length is needed. + */ + LENGTH_REQUIRED(411, "Length Required"), + + /** + * This is used to tell the client that a precondition has failed. + */ + PRECONDITION_FAILED(412, "Precondition Failed"), + + /** + * This is used to tell the client that the request body is too big. + */ + REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"), + + /** + * This is used to tell the client that the request URI is too long. + */ + REQUEST_URI_TOO_LONG(414, "Request-URI Too Long"), + + /** + * This is used to tell the client that the content type is invalid. + */ + UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), + + /** + * This is used to tell the client that the range is invalid. + */ + REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), + + /** + * This is used to tell the client that the expectation has failed. + */ + EXPECTATION_FAILED(417, "Expectation Failed"), + + /** + * This is sent when the request has caused an internal server error. + */ + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + + /** + * This is used to tell the client the resource is not implemented. + */ + NOT_IMPLEMENTED(501, "Not Implemented"), + + /** + * This is used to tell the client that the gateway is invalid. + */ + BAD_GATEWAY(502, "Bad Gateway"), + + /** + * This is used to tell the client the resource is unavailable. + */ + SERVICE_UNAVAILABLE(503, "Service Unavailable"), + + /** + * This is used to tell the client there was a gateway timeout. + */ + GATEWAY_TIMEOUT(504, "Gateway Timeout"), + + /** + * This is used to tell the client the request version is invalid. + */ + VERSION_NOT_SUPPORTED(505, "Version Not Supported"); + + /** + * This is the description of the status this instance represents. + */ + public final String description; + + /** + * This is the code for the status that this instance represents. + */ + public final int code; + + /** + * Constructor for the Status object. This will create + * a status object that is used to represent a response state. It + * contains a status code and a description of that code. + * + * @param code this is the code that is used for this status + * @param description this is the description used for the status + */ + private Status(int code, String description) { + this.description = description; + this.code = code; + } + + /** + * This is used to acquire the code of the status object. This is + * used in the HTTP response message to tell the client what kind + * of response this represents. Typically this is used to get a + * code for a known response state for convenience. + * + * @return the code associated by this status instance + */ + public int getCode() { + return code; + } + + /** + * This is used to provide the status description. The description + * is the textual description of the response state. It is used + * so that the response can be interpreted and is a required part + * of the HTTP response combined with the status code. + * + * @return the description associated by this status instance + */ + public String getDescription() { + return description; + } + + /** + * This is used to provide the status description. The description + * is the textual description of the response state. It is used + * so that the response can be interpreted and is a required part + * of the HTTP response combined with the status code. + * + * @param code this is the code to resolve the description for + * + * @return the description associated by this status code + */ + public static String getDescription(int code) { + Status[] list = values(); + + for(Status status : list) { + if(status.code == code) + return status.description; + } + return "Unknown"; + } + + /** + * This is used to provide the status value. If the specified + * code can not be matched this will return the default HTTP/1.1 + * status code of OK, which may not match the intended status. + * + * @param code this is the code to resolve the status for + * + * @return the status value associated by this status code + */ + public static Status getStatus(int code) { + Status[] list = values(); + + for(Status status : list) { + if(status.code == code) + return status; + } + return OK; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/StatusLine.java b/simple/simple-http/src/main/java/org/simpleframework/http/StatusLine.java new file mode 100644 index 0000000..3ea0f16 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/StatusLine.java @@ -0,0 +1,122 @@ +/* + * StatusLine.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http; + +/** + * The StatusLine is used to represent a HTTP status + * line. This provides several convenience methods that can be used + * to manipulate a HTTP status line. see the RFC (RFC 2616) for the + * syntax of a status line. + * + * @author Niall Gallagher + */ +public interface StatusLine { + + /** + * This represents the status code of the HTTP response. + * The response code represents the type of message that is + * being sent to the client. For a description of the codes + * see RFC 2616 section 10, Status Code Definitions. + * + * @return the status code that this HTTP response has + */ + int getCode(); + + /** + * This method allows the status for the response to be + * changed. This MUST be reflected the the response content + * given to the client. For a description of the codes see + * RFC 2616 section 10, Status Code Definitions. + * + * @param code the new status code for the HTTP response + */ + void setCode(int code); + + /** + * This can be used to retrieve the text of a HTTP status + * line. This is the text description for the status code. + * This should match the status code specified by the RFC. + * + * @return the message description of the response + */ + String getDescription(); + + /** + * This is used to set the text of the HTTP status line. + * This should match the status code specified by the RFC. + * + * @param text the descriptive text message of the status + */ + void setDescription(String text); + + /** + * This is used to acquire the status from the response. + * The Status object returns represents the + * code that has been set on the response, it does not + * necessarily represent the description in the response. + * + * @return this is the response for this status line + */ + Status getStatus(); + + /** + * This is used to set the status code and description + * for this response. Setting the code and description in + * this manner provides a much more convenient way to set + * the response status line details. + * + * @param status this is the status to set on the response + */ + void setStatus(Status status); + + /** + * This can be used to get the major number from a HTTP + * version. The major version corresponds to the major + * type that is the 1 of a HTTP/1.0 version string. + * + * @return the major version number for the response + */ + int getMajor(); + + /** + * This can be used to specify the major version. This + * should be the major version of the HTTP request. + * + * @param major this is the major number desired + */ + void setMajor(int major); + + /** + * This can be used to get the minor number from a HTTP + * version. The major version corresponds to the minor + * type that is the 0 of a HTTP/1.0 version string. + * + * @return the major version number for the response + */ + int getMinor(); + + /** + * This can be used to specify the minor version. This + * should not be set to zero if the HTTP request was + * for HTTP/1.1. The response must be equal or higher. + * + * @param minor this is the minor number desired + */ + void setMinor(int minor); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoder.java new file mode 100644 index 0000000..a2cd6dd --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoder.java @@ -0,0 +1,108 @@ +/* + * BodyEncoder.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * The BodyEncoder object is used to encode content from + * the HTTP response. This acts in much the same way as an output + * stream would. As a requirement of RFC 2616 any HTTP/1.1 compliant + * server must support a set of transfer types. These are fixed size, + * chunked encoded, and connection close. A producer implementation + * is required to implement one of this formats for delivery of the + * response message. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.BodyObserver + */ +interface BodyEncoder { + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + */ + void encode(byte[] array) throws IOException; + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + * @param off this is the offset within the array to send from + * @param size this is the number of bytes that are to be sent + */ + void encode(byte[] array, int off, int size) throws IOException; + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + */ + void encode(ByteBuffer buffer) throws IOException; + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + * @param off this is the offset within the buffer to send from + * @param size this is the number of bytes that are to be sent + */ + void encode(ByteBuffer buffer, int off, int size) throws IOException; + + /** + * This method is used to flush the contents of the buffer to + * the client. This method will block until such time as all of + * the data has been sent to the client. If at any point there + * is an error sending the content an exception is thrown. + */ + void flush() throws IOException; + + /** + * This is used to signal to the producer that all content has + * been written and the user no longer needs to write. This will + * either close the underlying transport or it will notify the + * monitor that the response has completed and the next request + * can begin. This ensures the content is flushed to the client. + */ + void close() throws IOException; +} + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderException.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderException.java new file mode 100644 index 0000000..7a0a86a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderException.java @@ -0,0 +1,58 @@ +/* + * BodyEncoderException.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; + +/** + * The BodyEncoderException object is used to represent + * an exception that is thrown when there is a problem producing the + * response body. This can be used to wrap IOException + * objects that are thrown from the underlying transport. + * + * @author Niall Gallagher + */ +class BodyEncoderException extends IOException { + + /** + * Constructor for the BodyEncoderException object. This + * is used to represent an exception that is thrown when producing + * the response body. The encoder exception is an I/O exception + * and thus exceptions can propagate out of stream methods. + * + * @param message this is the message describing the exception + */ + public BodyEncoderException(String message) { + super(message); + } + + /** + * Constructor for the BodyEncoderException object. This + * is used to represent an exception that is thrown when producing + * the response body. The encoder exception is an I/O exception + * and thus exceptions can propagate out of stream methods. + * + * @param message this is the message describing the exception + * @param cause this is the cause of the encoder exception + */ + public BodyEncoderException(String message, Throwable cause) { + super(message); + initCause(cause); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderFactory.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderFactory.java new file mode 100644 index 0000000..93aab52 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderFactory.java @@ -0,0 +1,118 @@ +/* + * BodyEncoderFactory.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; + +/** + * The BodyEncoderFactory is used to create a producer to + * match the HTTP header sent with the response. This interprets the + * headers within the response and composes a producer that will + * match those. Producers can be created to send in chunked encoding + * format, as well as fixed length and connection close for HTTP/1.0. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.ResponseEncoder + */ +class BodyEncoderFactory { + + /** + * This is used to determine the semantics of the HTTP pipeline. + */ + private final Conversation support; + + /** + * This is the monitor used to notify the initiator of events. + */ + private final BodyObserver observer; + + /** + * This is the underlying sender used to deliver the raw data. + */ + private final ByteWriter writer; + + /** + * Constructor for the BodyEncoderFactory object. + * This is used to create producers that can encode data in a HTTP + * compliant format. Each producer created will produce its data + * and deliver it to the specified sender, should an I/O events + * occur such as an error, or completion of the response then + * the monitor is notified and the server kernel takes action. + * + * @param observer this is used to deliver signals to the kernel + * @param support this contains details regarding the semantics + * @param writer this is used to send to the underlying transport + */ + public BodyEncoderFactory(BodyObserver observer, Conversation support, Channel channel) { + this.writer = channel.getWriter(); + this.observer = observer; + this.support = support; + } + + /** + * This is used to create an a BodyEncoder object + * that can be used to encode content according to the HTTP header. + * If the request was from a HTTP/1.0 client that did not ask + * for keep alive connection semantics a simple close producer + * is created. Otherwise the content is chunked encoded or sent + * according the the Content-Length. + * + * @return this returns the producer used to send the response + */ + public BodyEncoder getInstance() { + boolean keepAlive = support.isKeepAlive(); + boolean chunkable = support.isChunkedEncoded(); + boolean tunnel = support.isTunnel(); + + if(!keepAlive || tunnel) { + return new CloseEncoder(observer, writer); + } + return getInstance(chunkable); + } + + /** + * This is used to create an a BodyEncoder object + * that can be used to encode content according to the HTTP header. + * If the request was from a HTTP/1.0 client that did not ask + * for keep alive connection semantics a simple close producer + * is created. Otherwise the content is chunked encoded or sent + * according the the Content-Length. + * + * @param chunkable does the connected client support chunked + * + * @return this returns the producer used to send the response + */ + private BodyEncoder getInstance(boolean chunkable) { + long length = support.getContentLength(); + + if(!support.isHead()) { + if(length > 0) { + return new FixedLengthEncoder(observer, writer, length); + } + if(chunkable) { + return new ChunkedEncoder(observer, writer); + } + } + return new EmptyEncoder(observer, writer); + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyObserver.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyObserver.java new file mode 100644 index 0000000..eebefc7 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/BodyObserver.java @@ -0,0 +1,121 @@ +/* + * BodyObserver.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import org.simpleframework.transport.ByteWriter; + +/** + * The BodyObserver object is core to how the requests + * are processed from a pipeline. This observes the progress of the + * response streams as they are written to the underlying transport + * which is typically TCP. If at any point there is an error in + * the delivery of the response the observer is notified. It can + * then shutdown the connection, as RFC 2616 suggests on errors. + *

+ * If however the response is delivered successfully the monitor is + * notified of this event. On successful delivery the monitor will + * hand the Channel back to the server kernel so that + * the next request can be processed. This ensures ordering of the + * responses matches ordering of the requests. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.Controller + */ +interface BodyObserver { + + /** + * This is used to close the underlying transport. A closure is + * typically done when the response is to a HTTP/1.0 client + * that does not require a keep alive connection. Also, if the + * container requests an explicit closure this is used when all + * of the content for the response has been sent. + * + * @param writer this is the writer used to send the response + */ + void close(ByteWriter writer); + + /** + * This is used when there is an error sending the response. On + * error RFC 2616 suggests a connection closure is the best + * means to handle the condition, and the one clients should be + * expecting and support. All errors result in closure of the + * underlying transport and no more requests are processed. + * + * @param writer this is the writer used to send the response + */ + void error(ByteWriter writer); + + /** + * This is used when the response has been sent correctly and + * the connection supports persisted HTTP. When ready the channel + * is handed back in to the server kernel where the next request + * on the pipeline is read and used to compose the next entity. + * + * @param writer this is the writer used to send the response + */ + void ready(ByteWriter writer); + + /** + * This is used to notify the monitor that the HTTP response is + * committed and that the header can no longer be changed. It + * is also used to indicate whether the response can be reset. + * + * @param writer this is the writer used to send the response + */ + void commit(ByteWriter writer); + + /** + * This can be used to determine whether the response has been + * committed. If the response is committed then the header can + * no longer be manipulated and the response has been partially + * send to the client. + * + * @return true if the response headers have been committed + */ + boolean isCommitted(); + + /** + * This is used to determine if the response has completed or + * if there has been an error. This basically allows the writer + * of the response to take action on certain I/O events. + * + * @return this returns true if there was an error or close + */ + boolean isClosed(); + + /** + * This is used to determine if the response was in error. If + * the response was in error this allows the writer to throw an + * exception indicating that there was a problem responding. + * + * @return this returns true if there was a response error + */ + boolean isError(); + + /** + * This represents the time at which the response was either + * ready, closed or in error. Providing a time here is useful + * as it allows the time taken to generate a response to be + * determined even if the response is written asynchronously. + * + * @return the time when the response completed or failed + */ + long getTime(); +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ChunkedEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ChunkedEncoder.java new file mode 100644 index 0000000..5663d4b --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ChunkedEncoder.java @@ -0,0 +1,221 @@ +/* + * ChunkedEncoder.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.simpleframework.transport.ByteWriter; + +/** + * The ChunkedEncoder object is used to encode data in + * the chunked encoding format. A chunked producer is required when + * the length of the emitted content is unknown. It enables the HTTP + * pipeline to remain open as it is a self delimiting format. This + * is preferred over the CloseEncoder for HTTP/1.1 as + * it maintains the pipeline and thus the cost of creating it. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.message.ChunkedConsumer + */ +class ChunkedEncoder implements BodyEncoder { + + /** + * This is the size line which is used to generate the size. + */ + private byte[] size = { '0', '0', '0', '0', '0', '0', '0', '0', '\r', '\n' }; + + /** + * This is the hexadecimal alphabet used to translate the size. + */ + private byte[] index = { '0', '1', '2', '3', '4', '5','6', '7', '8', '9', 'a', 'b', 'c', 'd','e', 'f' }; + + /** + * This is the zero length chunk sent when this is completed. + */ + private byte[] zero = { '0', '\r', '\n', '\r', '\n' }; + + /** + * This is the observer used to notify the selector of events. + */ + private BodyObserver observer; + + /** + * This is the underlying writer used to deliver the encoded data. + */ + private ByteWriter writer; + + /** + * Constructor for the ChunkedEncoder object. This + * is used to create a producer that can sent data in the chunked + * encoding format. Once the data is encoded in the format it is + * handed to the provided ByteWriter object which will + * then deliver it to the client using the underlying transport. + * + * @param observer this is the observer used to signal I/O events + * @param writer this is the writer used to deliver the content + */ + public ChunkedEncoder(BodyObserver observer, ByteWriter writer) { + this.observer = observer; + this.writer = writer; + } + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 complaint format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + */ + public void encode(byte[] array) throws IOException { + encode(array, 0, array.length); + } + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 complaint format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + * @param off this is the offset within the array to send from + * @param len this is the number of bytes that are to be sent + */ + public void encode(byte[] array, int off, int len) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(array, off, len); + + if(len > 0) { + encode(buffer); + } + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + */ + public void encode(ByteBuffer buffer) throws IOException { + int mark = buffer.position(); + int size = buffer.limit(); + + if(mark > size) { + throw new BodyEncoderException("Buffer position greater than limit"); + } + encode(buffer, 0, size - mark); + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + * @param off this is the offset within the buffer to send from + * @param len this is the number of bytes that are to be sent + */ + public void encode(ByteBuffer buffer, int off, int len) throws IOException { + int pos = 7; + + if(observer.isClosed()) { + throw new BodyEncoderException("Stream has been closed"); + } + if(len > 0) { + for(int num = len; num > 0; num >>>= 4){ + size[pos--] = index[num & 0xf]; + } + try { + writer.write(size, pos + 1, 9 - pos); + writer.write(buffer, off, len); + writer.write(size, 8, 2); + } catch(Exception cause) { + if(writer != null) { + observer.error(writer); + } + throw new BodyEncoderException("Error sending response", cause); + } + } + } + + /** + * This method is used to flush the contents of the buffer to + * the client. This method will block until such time as all of + * the data has been sent to the client. If at any point there + * is an error sending the content an exception is thrown. + */ + public void flush() throws IOException { + try { + if(!observer.isClosed()) { + writer.flush(); + } + } catch(Exception cause) { + if(writer != null) { + observer.close(writer); + } + throw new BodyEncoderException("Error sending response", cause); + } + } + + /** + * This method is used to write the zero length chunk. Writing + * the zero length chunk tells the client that the response has + * been fully sent, and the next sequence of bytes from the HTTP + * pipeline is the start of the next response. This will signal + * to the server kernel that the next request is read to read. + */ + private void finish() throws IOException { + try { + writer.write(zero); + observer.ready(writer); + } catch(Exception cause) { + if(writer != null) { + observer.close(writer); + } + throw new BodyEncoderException("Error flushing response", cause); + } + } + + /** + * This is used to signal to the producer that all content has + * been written and the user no longer needs to write. This will + * either close the underlying transport or it will notify the + * monitor that the response has completed and the next request + * can begin. This ensures the content is flushed to the client. + */ + public void close() throws IOException { + if(!observer.isClosed()) { + finish(); + } + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/CloseEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/CloseEncoder.java new file mode 100644 index 0000000..8abd726 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/CloseEncoder.java @@ -0,0 +1,179 @@ +/* + * CloseEncoder.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.simpleframework.transport.ByteWriter; + +/** + * The CloseEncoder is used to close a connection once + * all of the content has been produced. This is typically used if + * the connected client supports the HTTP/1.0 protocol and there is + * no Connection header with the keep-alive token. For reasons of + * performance this should not be used for HTTP/1.1 clients. + * + * @author Niall Gallagher + */ +class CloseEncoder implements BodyEncoder { + + /** + * This is the observer used to notify the selector of events. + */ + private final BodyObserver observer; + + /** + * This is the underlying writer used to deliver the raw data. + */ + private final ByteWriter writer; + + /** + * Constructor for the CloseEncoder object. This is + * used to create a producer that will close the underlying socket + * as a means to signal that the response is fully sent. This is + * typically used with HTTP/1.0 connections. + * + * @param writer this is used to send to the underlying transport + * @param observer this is used to deliver signals to the kernel + */ + public CloseEncoder(BodyObserver observer, ByteWriter writer) { + this.observer = observer; + this.writer = writer; + } + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 complaint format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + */ + public void encode(byte[] array) throws IOException { + encode(array, 0, array.length); + } + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 complaint format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + * @param off this is the offset within the array to send from + * @param len this is the number of bytes that are to be sent + */ + public void encode(byte[] array, int off, int len) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(array, off, len); + + if(len > 0) { + encode(buffer); + } + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + */ + public void encode(ByteBuffer buffer) throws IOException { + int mark = buffer.position(); + int size = buffer.limit(); + + if(mark > size) { + throw new BodyEncoderException("Buffer position greater than limit"); + } + encode(buffer, 0, size - mark); + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + * @param off this is the offset within the buffer to send from + * @param len this is the number of bytes that are to be sent + */ + public void encode(ByteBuffer buffer, int off, int len) throws IOException { + if(observer.isClosed()) { + throw new BodyEncoderException("Stream has been closed"); + } + try { + writer.write(buffer, off, len); + } catch(Exception cause) { + if(writer != null) { + observer.error(writer); + } + throw new BodyEncoderException("Error sending response", cause); + } + } + + /** + * This method is used to flush the contents of the buffer to + * the client. This method will block until such time as all of + * the data has been sent to the client. If at any point there + * is an error sending the content an exception is thrown. + */ + public void flush() throws IOException { + try { + if(!observer.isClosed()) { + writer.flush(); + } + } catch(Exception cause) { + if(writer != null) { + observer.error(writer); + } + throw new BodyEncoderException("Error sending response", cause); + } + } + + /** + * This is used to signal to the producer that all content has + * been written and the user no longer needs to write. This will + * close the underlying transport which tells the client that + * all of the content has been sent over the connection. + */ + public void close() throws IOException { + try { + if(!observer.isClosed()) { + observer.close(writer); + writer.close(); + } + } catch(Exception cause) { + if(writer != null) { + observer.error(writer); + } + throw new BodyEncoderException("Error sending response", cause); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/Collector.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/Collector.java new file mode 100644 index 0000000..df5b4ca --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/Collector.java @@ -0,0 +1,50 @@ +/* + * Collector.java October 2002 + * + * Copyright (C) 2002, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.http.message.Entity; + +/** + * The Collector object is used to collect all of the + * data used to form a request entity. This will collect the data + * fragment by fragment from the underlying transport. When all + * of the data is consumed and the entity is created and then it + * is sent to the Controller object for processing. + * If the request has completed the next request can be collected + * from the underlying transport using a new collector object. + * + * @author Niall Gallagher + */ +interface Collector extends Entity { + + /** + * This is used to collect the data from a Channel + * which is used to compose the entity. If at any stage there + * are no ready bytes on the socket the controller provided can be + * used to queue the collector until such time as the socket is + * ready to read. Also, should the entity have completed reading + * all required content it is handed to the controller as ready, + * which processes the entity as a new client HTTP request. + * + * @param controller this is the controller used to queue this + */ + void collect(Controller controller) throws IOException; +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/Container.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/Container.java new file mode 100644 index 0000000..91a034c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/Container.java @@ -0,0 +1,62 @@ +/* + * Container.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The Container object is used to process HTTP requests + * and compose HTTP responses. The Request objects that + * are handed to this container contain all information relating to + * the received message. The responsibility of the container is to + * interpret the request and compose a suitable response. + *

+ * All implementations must ensure that the container is thread safe + * as it will receive multiple HTTP transactions concurrently. Also + * it should be known that the Response object used to + * deliver the HTTP response will only commit and send once it has + * its OutputStream closed. + *

+ * The Container is entirely responsible for the HTTP + * message headers and body. It is up to the implementation to ensure + * that it complies to RFC 2616 or any previous specification. All + * headers and the status line can be modified by this object. + * + * @author Niall Gallagher + */ +public interface Container { + + /** + * Used to pass the Request and Response + * to the container for processing. Any implementation of this + * must ensure that this is thread safe, as it will receive many + * concurrent invocations each with a unique HTTP request. + *

+ * The request and response objects are used to interact with the + * connected pipeline, in such a way that requests and response + * objects can be delivered in sequence and without interference. + * The next request from a HTTP pipeline is only processed once + * the Response object has been closed and committed. + * + * @param req the request that contains the client HTTP message + * @param resp the response used to deliver the server response + */ + void handle(Request req, Response resp); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerController.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerController.java new file mode 100644 index 0000000..16a6374 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerController.java @@ -0,0 +1,161 @@ +/* + * ContainerController.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static java.nio.channels.SelectionKey.OP_READ; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.thread.ConcurrentExecutor; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.TransportException; +import org.simpleframework.transport.reactor.ExecutorReactor; +import org.simpleframework.transport.reactor.Reactor; + +/** + * The ContainerController object is essentially the core + * processing engine for the server. This is used to collect requests + * from the connected channels and dispatch those requests to the + * provided Container object. This contains two thread + * pools. The first is used to collect data from the channels and + * create request entities. The second is used to take the created + * entities and service them with the provided container. + * + * @author Niall Gallagher + */ +class ContainerController implements Controller { + + /** + * This is the thread pool used for servicing the requests. + */ + private final ConcurrentExecutor executor; + + /** + * This is the thread pool used for collecting the requests. + */ + private final ConcurrentExecutor collect; + + /** + * This is the allocator used to create the buffers needed. + */ + private final Allocator allocator; + + /** + * This is the container used to service the requests. + */ + private final Container container; + + /** + * This is the reactor used to schedule the collectors. + */ + private final Reactor reactor; + + /** + * Constructor for the ContainerController object. This + * is used to create a controller which will collect and dispatch + * requests using two thread pools. The first is used to collect + * the requests, the second is used to service those requests. + * + * @param container this is the container used to service requests + * @param allocator this is used to allocate any buffers needed + * @param count this is the number of threads per thread pool + * @param select this is the number of controller threads to use + */ + public ContainerController(Container container, Allocator allocator, int count, int select) throws IOException { + this.executor = new ConcurrentExecutor(RequestDispatcher.class, count); + this.collect = new ConcurrentExecutor(RequestReader.class, count); + this.reactor = new ExecutorReactor(collect, select); + this.allocator = allocator; + this.container = container; + } + + /** + * This is used to initiate the processing of the channel. Once + * the channel is passed in to the initiator any bytes ready on + * the HTTP pipeline will be processed and parsed in to a HTTP + * request. When the request has been built a callback is made + * to the Container to process the request. Also + * when the request is completed the channel is passed back in + * to the initiator so that the next request can be dealt with. + * + * @param channel the channel to process the request from + */ + public void start(Channel channel) throws IOException { + start(new RequestCollector(allocator, channel)); + } + + /** + * The start event is used to immediately consume bytes form the + * underlying transport, it does not require a select to check + * if the socket is read ready which improves performance. Also, + * when a response has been delivered the next request from the + * pipeline is consumed immediately. + * + * @param collector this is the collector used to collect data + */ + public void start(Collector collector) throws IOException { + reactor.process(new RequestReader(this, collector)); + } + + /** + * The select event is used to register the connected socket with + * a Java NIO selector which can efficiently determine when there + * are bytes ready to read from the socket. + * + * @param collector this is the collector used to collect data + */ + public void select(Collector collector) throws IOException { + reactor.process(new RequestReader(this, collector), OP_READ); + } + + /** + * The ready event is used when a full HTTP entity has been + * collected from the underlying transport. On such an event the + * request and response can be handled by a container. + * + * @param collector this is the collector used to collect data + */ + public void ready(Collector collector) throws IOException { + executor.execute(new RequestDispatcher(container, this, collector)); + } + + /** + * This method is used to stop the Selector so that + * all resources are released. As well as freeing occupied memory + * this will also stop all threads, which means that is can no + * longer be used to collect data from the pipelines. + *

+ * Here we stop the Reactor first, this ensures + * that there are no further selects performed if a given socket + * does not have enough data to fulfil a request. From there we + * stop the main dispatch Executor so that all of + * the currently executing tasks complete. The final stage of + * termination requires the collector thread pool to be stopped. + */ + public void stop() throws IOException { + try { + reactor.stop(); + executor.stop(); + collect.stop(); + } catch(Exception cause) { + throw new TransportException("Error stopping", cause); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerEvent.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerEvent.java new file mode 100644 index 0000000..ecd96a3 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerEvent.java @@ -0,0 +1,93 @@ +/* + * ContainerEvent.java October 2012 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +/** + * The ContainerEvent enum represents events that occur when + * processing a HTTP transaction. Here each phase of processing has a + * single event to represent it. If a Trace object has been + * associated with the connection then the server will notify the trace + * when the connection enters a specific phase of processing. + * + * @author Niall Gallagher + * + * @see org.simpleframework.transport.trace.Trace + */ +public enum ContainerEvent { + + /** + * This event indicates that the server is reading the request header. + */ + READ_HEADER, + + /** + * This event indicates that the server is reading the request body. + */ + READ_BODY, + + /** + * This event indicates that the server is writing the response header. + */ + WRITE_HEADER, + + /** + * This event indicates that the server is writing the response body. + */ + WRITE_BODY, + + /** + * This indicates that the server has fully read the request header. + */ + HEADER_FINISHED, + + /** + * This indicates that the server has fully read the request body. + */ + BODY_FINISHED, + + /** + * This event indicates that the server sent a HTTP continue reply. + */ + DISPATCH_CONTINUE, + + /** + * This event indicates that the request is ready for processing. + */ + REQUEST_READY, + + /** + * This indicates that the request has been dispatched for processing. + */ + DISPATCH_REQUEST, + + /** + * This indicates that the dispatch thread has completed the dispatch. + */ + DISPATCH_FINISHED, + + /** + * This indicates that all the bytes within the response are sent. + */ + RESPONSE_FINISHED, + + /** + * This indicates that there was some error event with the request. + */ + ERROR; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerSocketProcessor.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerSocketProcessor.java new file mode 100644 index 0000000..0bf1a44 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerSocketProcessor.java @@ -0,0 +1,155 @@ +/* + * ContainerSocketProcessor.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.FileAllocator; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.TransportSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.Socket; + +/** + * The ContainerSocketProcessor object is a connector + * that dispatch requests from a connected pipeline. SSL connections + * and plain connections can be processed by this implementation. It + * collects data from the connected pipelines and constructs the + * requests and responses used to dispatch to the container. + *

+ * In order to process the requests this uses two thread pools. One + * is used to collect data from the pipelines and create the requests. + * The other is used to service those requests. Such an architecture + * ensures that the serving thread does not have to deal with I/O + * operations. All data is consumed before it is serviced. + * + * @author Niall Gallagher + */ +public class ContainerSocketProcessor implements SocketProcessor { + + /** + * This is the transporter used to process the connections. + */ + private final TransportProcessor processor; + + /** + * This is used to deliver pipelines to the container. + */ + private final SocketProcessor adapter; + + /** + * Constructor for the ContainerSocketProcessor object. + * The connector created will collect HTTP requests from the pipelines + * provided and dispatch those requests to the provided container. + * + * @param container this is the container used to service requests + */ + public ContainerSocketProcessor(Container container) throws IOException { + this(container, 8); + } + + /** + * Constructor for the ContainerSocketProcessor object. + * The connector created will collect HTTP requests from the pipelines + * provided and dispatch those requests to the provided container. + * + * @param container this is the container used to service requests + * @param count this is the number of threads used for each pool + */ + public ContainerSocketProcessor(Container container, int count) throws IOException { + this(container, count, 1); + } + + /** + * Constructor for the ContainerSocketProcessor object. The + * connector created will collect HTTP requests from the pipelines + * provided and dispatch those requests to the provided container. + * + * @param container this is the container used to service requests + * @param count this is the number of threads used for each pool + * @param select this is the number of selector threads to use + */ + public ContainerSocketProcessor(Container container, int count, int select) throws IOException { + this(container, new FileAllocator(), count, select); + } + + /** + * Constructor for the ContainerSocketProcessor object. + * The connector created will collect HTTP requests from the pipelines + * provided and dispatch those requests to the provided container. + * + * @param container this is the container used to service requests + * @param allocator this is the allocator used to create buffers + */ + public ContainerSocketProcessor(Container container, Allocator allocator) throws IOException { + this(container, allocator, 8); + } + + /** + * Constructor for the ContainerSocketProcessor object. + * The connector created will collect HTTP requests from the pipelines + * provided and dispatch those requests to the provided container. + * + * @param container this is the container used to service requests + * @param allocator this is the allocator used to create buffers + * @param count this is the number of threads used for each pool + */ + public ContainerSocketProcessor(Container container, Allocator allocator, int count) throws IOException { + this(container, allocator, count, 1); + } + + /** + * Constructor for the ContainerSocketProcessor object. + * The connector created will collect HTTP requests from the pipelines + * provided and dispatch those requests to the provided container. + * + * @param container this is the container used to service requests + * @param allocator this is the allocator used to create buffers + * @param count this is the number of threads used for each pool + * @param select this is the number of selector threads to use + */ + public ContainerSocketProcessor(Container container, Allocator allocator, int count, int select) throws IOException { + this.processor = new ContainerTransportProcessor(container, allocator, count, select); + this.adapter = new TransportSocketProcessor(processor, count); + } + + /** + * This is used to consume HTTP messages that arrive on the socket + * and dispatch them to the internal container. Depending on whether + * the socket contains an SSLEngine an SSL handshake may + * be performed before any HTTP messages are consumed. This can be + * called from multiple threads and does not block. + * + * @param socket this is the connected HTTP pipeline to process + */ + public void process(Socket socket) throws IOException { + adapter.process(socket); + } + + /** + * This method is used to stop the connector in such a way that it + * will not accept and process any further messages. If there are + * resources to clean up they may be cleaned up asynchronously + * so that this method can return without blocking. + */ + public void stop() throws IOException { + adapter.stop(); + } + } diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerTransportProcessor.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerTransportProcessor.java new file mode 100644 index 0000000..dd6df1a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerTransportProcessor.java @@ -0,0 +1,96 @@ +/* + * ContainerProcessor.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.Transport; +import org.simpleframework.transport.TransportChannel; + +/** + * The ContainerProcessor object is used to create + * channels which can be used to consume and process requests. This + * is basically an adapter to the Selector which will + * convert the provided transport to a usable channel. Each of the + * connected pipelines will end up at this object, regardless of + * whether those connections are SSL or plain data. + * + * @author Niall Gallagher + */ +public class ContainerTransportProcessor implements TransportProcessor { + + /** + * This is the controller used to process the created channels. + */ + private final Controller controller; + + /** + * Constructor for the ContainerProcessor object. + * This is used to create a processor which will convert the + * provided transport objects to channels, which can then be + * processed by the controller and dispatched to the container. + * + * @param container the container to dispatch requests to + * @param allocator this is the allocator used to buffer data + * @param count this is the number of threads to be used + */ + public ContainerTransportProcessor(Container container, Allocator allocator, int count) throws IOException { + this(container, allocator, count, 1); + } + + /** + * Constructor for the ContainerProcessor object. + * This is used to create a processor which will convert the + * provided transport objects to channels, which can then be + * processed by the controller and dispatched to the container. + * + * @param container the container to dispatch requests to + * @param allocator this is the allocator used to buffer data + * @param count this is the number of threads to be used + * @param select this is the number of controller threads to use + */ + public ContainerTransportProcessor(Container container, Allocator allocator, int count, int select) throws IOException { + this.controller = new ContainerController(container, allocator, count, select); + } + + /** + * This is used to consume HTTP messages that arrive on the given + * transport. All messages consumed from the transport are then + * handed to the Container for processing. The response + * will also be delivered over the provided transport. At this point + * the SSL handshake will have fully completed. + * + * @param transport the transport to process requests from + */ + public void process(Transport transport) throws IOException { + controller.start(new TransportChannel(transport)); + } + + /** + * This method is used to stop the connector in such a way that it + * will not accept and process any further messages. If there are + * resources to clean up they may be cleaned up asynchronously + * so that this method can return without blocking. + */ + public void stop() throws IOException { + controller.stop(); + } + } \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/Controller.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/Controller.java new file mode 100644 index 0000000..3e152bd --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/Controller.java @@ -0,0 +1,100 @@ +/* + * Controller.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.transport.Channel; + +/** + * The Controller interface represents an object which + * is used to process collection events. The sequence of events that + * typically take place is for the collection to start, if not all + * of the bytes can be consumed it selects, and finally when all of + * the bytes within the entity have been consumed it is ready. + *

+ * The start event is used to immediately consume bytes form the + * underlying transport, it does not require a select to determine + * if the socket is read ready which provides an initial performance + * enhancement. Also when a response has been delivered the next + * request from the pipeline is consumed immediately. + *

+ * The select event is used to register the connected socket with a + * Java NIO selector which can efficiently determine when there are + * bytes ready to read from the socket. Finally, the ready event + * is used when a full HTTP entity has been collected from the + * underlying transport. On such an event the request and response + * can be handled by a container. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.Collector + */ +interface Controller { + + /** + * This is used to initiate the processing of the channel. Once + * the channel is passed in to the initiator any bytes ready on + * the HTTP pipeline will be processed and parsed in to a HTTP + * request. When the request has been built a callback is made + * to the Container to process the request. Also + * when the request is completed the channel is passed back in + * to the initiator so that the next request can be dealt with. + * + * @param channel the channel to process the request from + */ + void start(Channel channel) throws IOException; + + /** + * The start event is used to immediately consume bytes form the + * underlying transport, it does not require a select to check + * if the socket is read ready which improves performance. Also, + * when a response has been delivered the next request from the + * pipeline is consumed immediately. + * + * @param collector this is the collector used to collect data + */ + void start(Collector collector) throws IOException; + + /** + * The select event is used to register the connected socket with + * a Java NIO selector which can efficiently determine when there + * are bytes ready to read from the socket. + * + * @param collector this is the collector used to collect data + */ + void select(Collector collector) throws IOException; + + /** + * The ready event is used when a full HTTP entity has been + * collected from the underlying transport. On such an event the + * request and response can be handled by a container. + * + * @param collector this is the collector used to collect data + */ + void ready(Collector collector) throws IOException; + + /** + * This method is used to stop the Selector so that + * all resources are released. As well as freeing occupied memory + * this will also stop all threads, which means that is can no + * longer be used to collect data from the pipelines. + */ + void stop() throws IOException; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/Conversation.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/Conversation.java new file mode 100644 index 0000000..07318ce --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/Conversation.java @@ -0,0 +1,358 @@ +/* + * Conversation.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.Method.CONNECT; +import static org.simpleframework.http.Method.HEAD; +import static org.simpleframework.http.Protocol.CHUNKED; +import static org.simpleframework.http.Protocol.CLOSE; +import static org.simpleframework.http.Protocol.CONNECTION; +import static org.simpleframework.http.Protocol.CONTENT_LENGTH; +import static org.simpleframework.http.Protocol.KEEP_ALIVE; +import static org.simpleframework.http.Protocol.TRANSFER_ENCODING; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import org.simpleframework.http.RequestHeader; +import org.simpleframework.http.ResponseHeader; + +/** + * The Conversation object is used to set and interpret + * the semantics of the HTTP headers with regard to the encoding + * used for the response. This will ensure the the correct headers + * are used so that if chunked encoding or a connection close is + * needed that the headers are set accordingly. This allows both the + * server and client to agree on the best semantics to use. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.ResponseBuffer + * @see org.simpleframework.http.core.ResponseEncoder + */ +public class Conversation { + + /** + * This is the response object that requires HTTP headers set. + */ + private final ResponseHeader response; + + /** + * This contains the request headers and protocol version. + */ + private final RequestHeader request; + + /** + * Constructor for the Conversation object. This is + * used to create an object that makes use of both the request + * and response HTTP headers to determine how best to deliver + * the response body. Depending on the protocol version and the + * existing response headers suitable semantics are determined. + * + * @param request this is the request from the client + * @param response this is the response that is to be sent + */ + public Conversation(RequestHeader request, ResponseHeader response) { + this.response = response; + this.request = request; + } + + /** + * This provides the Request object. This can be + * used to acquire the request HTTP headers and protocl version + * used by the client. Typically the conversation provides all + * the data needed to determine the type of response required. + * + * @return this returns the request object for the conversation + */ + public RequestHeader getRequest() { + return request; + } + + /** + * This provides the Response object. This is used + * when the commit is required on the response. By committing + * the response the HTTP header is generated and delivered to + * the underlying transport. + * + * @return this returns the response for the conversation + */ + public ResponseHeader getResponse() { + return response; + } + + /** + * This is used to acquire the content length for the response. + * The content length is acquired fromt he Content-Length header + * if it has been set. If not then this will return a -1 value. + * + * @return this returns the value for the content length header + */ + public long getContentLength() { + return response.getContentLength(); + } + + /** + * This is used to determine if the Response has a + * message body. If this does not have a message body then true + * is returned. This is determined as of RFC 2616 rules for the + * presence of a message body. A message body must not be + * included with a HEAD request or with a 304 or a 204 response. + * If when this is called there is no message length delimiter + * as specified by section RFC 2616 4.4, then there is no body. + * + * @return true if there is no response body, false otherwise + */ + public boolean isEmpty() { + int code = response.getCode(); + + if(code == 204){ + return true; + } + if(code == 304){ + return true; + } + return false; + } + + /** + * This is used to determine if the request method was HEAD. This + * is of particular interest in a HTTP conversation as it tells + * the response whether a response body is to be sent or not. + * If the method is head the delimeters for the response should + * be as they would be for a similar GET, however no body is sent. + * + * @return true if the request method was a HEAD method + */ + public boolean isHead() { + String method = request.getMethod(); + + if(method != null) { + return method.equalsIgnoreCase(HEAD); + } + return false; + } + + /** + * This is used to determine if the method was a CONNECT. The + * connect method is typically used when a client wishes to + * establish a connection directly with an origin server. Such a + * direct connection is useful when using TLS as it ensures there + * is not man in the middle with respect to key exchanges. + * + * @return this returns true if the method was a CONNECT method + */ + public boolean isConnect() { + String method = request.getMethod(); + + if(method != null) { + return method.equalsIgnoreCase(CONNECT); + } + return false; + } + + /** + * This is used to set the content length for the response. If + * the HTTP version is HTTP/1.1 then the Content-Length header is + * used, if an earlier protocol version is used then connection + * close semantics are also used to ensure client compatibility. + * + * @param length this is the length to set HTTP header to + */ + public void setContentLength(long length) { + boolean keepAlive = isKeepAlive(); + + if(keepAlive) { + response.setValue(CONNECTION, KEEP_ALIVE); + } else { + response.setValue(CONNECTION, CLOSE); + } + response.setLong(CONTENT_LENGTH, length); + } + + /** + * This checks the protocol version used in the request to check + * whether it supports persistent HTTP connections. By default the + * HTTP/1.1 protocol supports persistent connnections, this can + * onlyy be overridden with a Connection header with the close + * token. Earlier protocol versions are connection close. + * + * @return this returns true if the protocol is HTTP/1.1 or above + */ + public boolean isPersistent() { + String token = request.getValue(CONNECTION); + + if(token != null) { + return token.equalsIgnoreCase(KEEP_ALIVE); + } + int major = request.getMajor(); + int minor = request.getMinor(); + + if(major > 1) { + return true; + } + if(major == 1) { + return minor > 0; + } + return false; + } + + /** + * The isKeepAlive method is used to determine if + * the connection semantics are set to maintain the connection. + * This checks to see if there is a Connection header with the + * keep-alive token, if so then the connection is keep alive, if + * however there is no connection header the version is used. + * + * @return true if the response connection is to be maintained + */ + public boolean isKeepAlive() { + String token = response.getValue(CONNECTION); + + if(token != null) { + return !token.equalsIgnoreCase(CLOSE); + } + return isPersistent(); + } + + /** + * The isChunkable method is used to determine if + * the client supports chunked encoding. If the client does not + * support chunked encoding then a connection close should be used + * instead, this allows HTTP/1.0 clients to be supported properly. + * + * @return true if the client supports chunked transfer encoding + */ + public boolean isChunkable() { + int major = request.getMajor(); + int minor = request.getMinor(); + + if(major >= 1) { + return minor >= 1; + } + return false; + } + + /** + * This is used when the output is encoded in the chunked encoding. + * This should only be used if the protocol version is HTTP/1.1 or + * above. If the protocol version supports chunked encoding then it + * will encode the data as specified in RFC 2616 section 3.6.1. + */ + public void setChunkedEncoded() { + boolean keepAlive = isKeepAlive(); + boolean chunkable = isChunkable(); + + if(keepAlive && chunkable) { + response.setValue(TRANSFER_ENCODING, CHUNKED); + response.setValue(CONNECTION, KEEP_ALIVE); + } else { + response.setValue(CONNECTION, CLOSE); + } + } + + /** + * This is used to set the response to a connection upgrade. The + * response for an upgrade contains no payload delimeter such as + * content length or transfer encoding. It is typically used when + * establishing a web socket connection or a HTTP tunnel. + */ + public void setConnectionUpgrade() { + response.setValue(TRANSFER_ENCODING, null); + response.setValue(CONTENT_LENGTH, null); + response.setValue(CONNECTION, UPGRADE); + } + + /** + * This will remove all explicit transfer encoding headers from + * the response header. By default the identity encoding is used + * for all connections, it basically means no encoding. So if the + * response uses a Content-Length it implicitly assumes tha the + * encoding of the response is identity encoding. + */ + public void setIdentityEncoded() { + response.setValue(TRANSFER_ENCODING, null); + } + + /** + * The isChunkedEncoded is used to determine whether + * the chunked encoding scheme is desired. This is enables data to + * be encoded in such a way that a connection can be maintained + * without a Content-Length header. If the output is chunked then + * the connection is keep alive. + * + * @return true if the response output is chunked encoded + */ + public boolean isChunkedEncoded() { + String token = response.getValue(TRANSFER_ENCODING); + + if(token != null) { + return token.equalsIgnoreCase(CHUNKED); + } + return false; + } + + /** + * This is used to determine if a WebSocket upgrade was requested + * and established. An upgrade to use a WebSocket is done when the + * client requests the upgrade and the server responds with an + * upgrade confirmation, this is the basic handshake required. + * + * @return this returns true if a WebSocket handshake succeeded + */ + public boolean isWebSocket() { + String token = request.getValue(UPGRADE); + int code = response.getCode(); + + if(token != null && code == 101) { + String reply = response.getValue(UPGRADE); + + if(token.equalsIgnoreCase(WEBSOCKET)) { + return token.equalsIgnoreCase(reply); + } + } + return false; + } + + /** + * This is used to determine if a tunnel should be established. + * A tunnel is where the the HTTP server no longer processes any + * HTTP requests, it simply forms a byte stream with the client. + * Scenarios where tunnels are useful are when WebSockets are + * required or when a client wants a TLS connection to an origin + * server through a proxy server. + * + * @return this returns true if a tunnel has been established + */ + public boolean isTunnel() { + boolean socket = isWebSocket(); + + if(!socket) { + int code = response.getCode(); + + if(code < 200) { + return false; + } + if(code >= 300) { + return false; + } + return isConnect(); + } + return true; + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/EmptyEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/EmptyEncoder.java new file mode 100644 index 0000000..7ec78fb --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/EmptyEncoder.java @@ -0,0 +1,132 @@ +/* + * EmptyEncoder.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.simpleframework.transport.ByteWriter; + +/** + * The EmptyEncoder object is a producer used if there + * is not response body to be delivered. Typically this is used when + * the HTTP request method is HEAD or if there is some status code + * sent to the client that does not require a response body. + * + * @author Niall Gallagher + */ +class EmptyEncoder implements BodyEncoder { + + /** + * This is the observer that is used to process the pipeline. + */ + private final BodyObserver observer; + + /** + * This is the writer that is passed to the monitor when ready. + */ + private final ByteWriter writer; + + /** + * Constructor for the EmptyEncoder object. Once + * created this producer will signal the kernel the the next + * request is ready to read from the HTTP pipeline as there is + * no content to be delivered with this producer object. + * + * @param writer this is used to send to the underlying transport + * @param observer this is used to deliver signals to the kernel + */ + public EmptyEncoder(BodyObserver observer, ByteWriter writer) { + this.observer = observer; + this.writer = writer; + } + + /** + * This method performs no operation. Because this producer is + * not required to generate a response body this will ignore all + * data that is provided to sent to the underlying transport. + * + * @param array this is the array of bytes to send to the client + */ + public void encode(byte[] array) throws IOException { + return; + } + + /** + * This method performs no operation. Because this producer is + * not required to generate a response body this will ignore all + * data that is provided to sent to the underlying transport. + * + * @param array this is the array of bytes to send to the client + * @param off this is the offset within the array to send from + * @param size this is the number of bytes that are to be sent + */ + public void encode(byte[] array, int off, int size) throws IOException { + return; + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + */ + public void encode(ByteBuffer buffer) throws IOException { + return; + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + * @param off this is the offset within the buffer to send from + * @param size this is the number of bytes that are to be sent + */ + public void encode(ByteBuffer buffer, int off, int size) throws IOException { + return; + } + + /** + * This method performs no operation. Because this producer is + * not required to generate a response body this will ignore all + * data that is provided to sent to the underlying transport. + */ + public void flush() throws IOException { + return; + } + + /** + * This method performs no operation. Because this producer is + * not required to generate a response body this will ignore all + * data that is provided to sent to the underlying transport. + */ + public void close() throws IOException { + observer.ready(writer); + } +} + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/FixedLengthEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/FixedLengthEncoder.java new file mode 100644 index 0000000..86148eb --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/FixedLengthEncoder.java @@ -0,0 +1,198 @@ +/* + * FixedLengthEncoder.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.simpleframework.transport.ByteWriter; + +/** + * The FixedLengthEncoder object produces content without + * any encoding, but limited to a fixed number of bytes. This is used if + * the length of the content being delivered is know beforehand. It + * will simply count the number of bytes being send and signal the + * server kernel that the next request is ready to read once all of + * the bytes have been sent to the client. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.message.FixedLengthConsumer + */ +class FixedLengthEncoder implements BodyEncoder{ + + /** + * This is the observer used to notify the initiator of events. + */ + private BodyObserver observer; + + /** + * This is the underlying writer used to deliver the raw data. + */ + private ByteWriter writer; + + /** + * This is the number of bytes that have been sent so far. + */ + private long count; + + /** + * This is the number of bytes this producer is limited to. + */ + private long limit; + + /** + * Constructor for the FixedLengthEncoder object. This + * is used to create an encoder that will count the number of bytes + * that are sent over the pipeline, once all bytes have been sent + * this will signal that the next request is ready to read. + * + * @param observer this is used to deliver signals to the kernel + * @param writer this is used to send to the underlying transport + * @param limit this is used to limit the number of bytes sent + */ + public FixedLengthEncoder(BodyObserver observer, ByteWriter writer, long limit) { + this.observer = observer; + this.writer = writer; + this.limit = limit; + } + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 complaint format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + */ + public void encode(byte[] array) throws IOException { + encode(array, 0, array.length); + } + + /** + * This method is used to encode the provided array of bytes in + * a HTTP/1.1 complaint format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param array this is the array of bytes to send to the client + * @param off this is the offset within the array to send from + * @param len this is the number of bytes that are to be sent + */ + public void encode(byte[] array, int off, int len) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(array, off, len); + + if(len > 0) { + encode(buffer); + } + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + */ + public void encode(ByteBuffer buffer) throws IOException { + int mark = buffer.position(); + int size = buffer.limit(); + + if(mark > size) { + throw new BodyEncoderException("Buffer position greater than limit"); + } + encode(buffer, 0, size - mark); + } + + /** + * This method is used to encode the provided buffer of bytes in + * a HTTP/1.1 compliant format and sent it to the client. Once + * the data has been encoded it is handed to the transport layer + * within the server, which may choose to buffer the data if the + * content is too small to send efficiently or if the socket is + * not write ready. + * + * @param buffer this is the buffer of bytes to send to the client + * @param off this is the offset within the buffer to send from + * @param len this is the number of bytes that are to be sent + */ + public void encode(ByteBuffer buffer, int off, int len) throws IOException { + long size = Math.min(len, limit - count); + + try { + if(observer.isClosed()) { + throw new BodyEncoderException("Response content complete"); + } + writer.write(buffer, off, (int)size); + + if(count + size == limit) { + observer.ready(writer); + } + } catch(Exception cause) { + if(writer != null) { + observer.error(writer); + } + throw new BodyEncoderException("Error sending response", cause); + } + count += size; + } + + /** + * This method is used to flush the contents of the buffer to + * the client. This method will block until such time as all of + * the data has been sent to the client. If at any point there + * is an error sending the content an exception is thrown. + */ + public void flush() throws IOException { + try { + if(!observer.isClosed()) { + writer.flush(); + } + } catch(Exception cause) { + if(writer != null) { + observer.error(writer); + } + throw new BodyEncoderException("Error flushing", cause); + } + } + + /** + * This is used to signal to the producer that all content has + * been written and the user no longer needs to write. This will + * either close the underlying transport or it will notify the + * monitor that the response has completed and the next request + * can begin. This ensures the content is flushed to the client. + */ + public void close() throws IOException { + if(!observer.isClosed()) { + if(count < limit) { + observer.error(writer); + } else { + observer.ready(writer); + } + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/QueryBuilder.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/QueryBuilder.java new file mode 100644 index 0000000..7942d77 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/QueryBuilder.java @@ -0,0 +1,148 @@ +/* + * QueryBuilder.java October 2002 + * + * Copyright (C) 2002, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.Protocol.APPLICATION; +import static org.simpleframework.http.Protocol.URL_ENCODED; + +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Query; +import org.simpleframework.http.Request; +import org.simpleframework.http.message.Entity; +import org.simpleframework.http.message.Header; + +/** + * The QueryBuilder object is used to create the query. + * It is created using the request URI query and a form post body if + * sent. The application/x-www-form-urlencoded conent type identifies + * the body as contain form data. If there are duplicates then they + * both are available from the query that is built. + * + * @author Niall Gallagher + */ +class QueryBuilder { + + /** + * This is the request that is used to acquire the data. + */ + private final Request request; + + /** + * This is the header that is used to acquire the data. + */ + private final Header header; + + /** + * Constructor for the QueryBuilder object. This will + * create an object that can be used to construct a single query + * from the multiple sources of data within the request entity. + * + * @param request this is the request to build a query for + * @param entity this is the entity that contains the data + */ + public QueryBuilder(Request request, Entity entity) { + this.header = entity.getHeader(); + this.request = request; + } + + /** + * This method is used to acquire the query part from the HTTP + * request URI target and a form post if it exists. Both the + * query and the form post are merge together in a single query. + * + * @return the query associated with the HTTP target URI + */ + public Query build() {; + Query query = header.getQuery(); + + if(!isFormPost()) { + return query; + } + return getQuery(query); + } + + /** + * This method is used to acquire the query part from the HTTP + * request URI target and a form post if it exists. Both the + * query and the form post are merge together in a single query. + * + * @param query this is the URI query string to be used + * + * @return the query associated with the HTTP target URI + */ + private Query getQuery(Query query) { + String body = getContent(); + + if(body == null) { + return query; + } + return new QueryCombiner(query, body); + } + + /** + * This method attempts to acquire the content of the request + * body. If there is an IOException acquiring the + * content of the body then this will simply return a null + * value without reporting the exception. + * + * @return the content of the body, or null on error + */ + private String getContent() { + try { + return request.getContent(); + } catch(Exception e) { + return null; + } + } + + /** + * This is used to determine if the content type is a form POST + * of type application/x-www-form-urlencoded. Such a type is + * used when a HTML form is used to post data to the server. + * + * @return this returns true if content type is a form post + */ + private boolean isFormPost() { + ContentType type = request.getContentType(); + + if(type == null) { + return false; + } + return isFormPost(type); + } + + /** + * This is used to determine if the content type is a form POST + * of type application/x-www-form-urlencoded. Such a type is + * used when a HTML form is used to post data to the server. + * + * @param type the type to determine if its a form post + * + * @return this returns true if content type is a form post + */ + private boolean isFormPost(ContentType type) { + String primary = type.getPrimary(); + String secondary = type.getSecondary(); + + if(!primary.equals(APPLICATION)) { + return false; + } + return secondary.equals(URL_ENCODED); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/QueryCombiner.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/QueryCombiner.java new file mode 100644 index 0000000..ed4c92e --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/QueryCombiner.java @@ -0,0 +1,148 @@ +/* + * QueryCombiner.java May 2003 + * + * Copyright (C) 2003, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.util.List; +import java.util.Set; + +import org.simpleframework.http.Query; +import org.simpleframework.http.parse.QueryParser; + +/** + * The QueryCombimer is used to parse several strings + * as a complete URL encoded parameter string. This will do the + * following concatenations. + * + *

+ * null + "a=b&c=d&e=f" = "a=b&c=d&e=f"
+ * "a=b" + "e=f&g=h" = "a=b&e=f&g=h";
+ * "a=b&c=d&e=f" + "" = "a=b&c=d&e=f"
+ * 
+ * + * This ensures that the QueryForm can parse the list + * of strings as a single URL encoded parameter string. This can + * parse any number of parameter strings. + * + * @author Niall Gallagher + */ +class QueryCombiner extends QueryParser { + + /** + * Constructor that allows a list of string objects to be + * parsed as a single parameter string. This will check + * each string to see if it is empty, that is, is either + * null or the zero length string. + * + * @param list this is a list of query values to be used + */ + public QueryCombiner(String... list) { + this.parse(list); + } + + /** + * Constructor that allows an array of string objects to + * be parsed as a single parameter string. This will check + * each string to see if it is empty, that is, is either + * null or the zero length string. + * + * @param query this is the query from the HTTP header + * @param list this is the list of strings to be parsed + */ + public QueryCombiner(Query query, String... list) { + this.add(query); + this.parse(list); + } + + /** + * Constructor that allows an array of string objects to + * be parsed as a single parameter string. This will check + * each string to see if it is empty, that is, is either + * null or the zero length string. + * + * @param query this is the query from the HTTP header + * @param post this is the query from the HTTP post body + */ + public QueryCombiner(Query query, Query post) { + this.add(query); + this.add(post); + } + + /** + * This will concatenate the list of parameter strings as a + * single parameter string, before handing it to be parsed + * by the parse(String) method. This method + * will ignore any null or zero length strings in the array. + * + * @param list this is the list of strings to be parsed + */ + public void parse(String[] list) { + StringBuilder text = new StringBuilder(); + + for(int i = 0; i < list.length; i++) { + if(list[i] == null) { + continue; + } else if(list[i].length()==0){ + continue; + } else if(text.length() > 0){ + text.append("&"); + } + text.append(list[i]); + } + parse(text); + } + + /** + * This is used to perform a parse of the form data that is in + * the provided string builder. This will simply convert the + * data in to a string and parse it in the normal fashion. + * + * @param text this is the buffer to be converted to a string + */ + private void parse(StringBuilder text) { + if(text != null){ + ensureCapacity(text.length()); + count = text.length(); + text.getChars(0, count, buf,0); + parse(); + } + } + + /** + * This method is used to insert a collection of tokens into + * the parsers map. This is used when another source of tokens + * is required to populate the connection currently maintained + * within this parsers internal map. Any tokens that currently + * exist with similar names will be overwritten by this. + * + * @param query this is the collection of tokens to be added + */ + private void add(Query query) { + Set keySet = query.keySet(); + + for(String key : keySet) { + List list = query.getAll(key); + String first = query.get(key); + + if(first != null) { + all.put(key, list); + map.put(key, first); + } + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCertificate.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCertificate.java new file mode 100644 index 0000000..1da8b54 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCertificate.java @@ -0,0 +1,183 @@ +/* + * RequestCertificate.java June 2013 + * + * Copyright (C) 2013, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; +import java.util.concurrent.Future; + +import javax.security.cert.X509Certificate; + +import org.simpleframework.http.message.Entity; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.CertificateChallenge; +import org.simpleframework.transport.Channel; + +/** + * The RequestCertificate represents a certificate for + * an HTTP request. It basically wraps the raw SSL certificate that + * comes with the Channel. Wrapping the raw certificate + * allows us to enforce the HTTPS workflow for SSL renegotiation, + * which requires some rather weird behaviour. Most importantly + * we only allow a challenge when the response has not been sent. + * + * @author Niall Gallagher + * + * @see org.simpleframework.transport.CertificateChallenge + */ +class RequestCertificate implements Certificate { + + /** + * This is used to challenge the client for an X509 certificate. + */ + private final CertificateChallenge challenge; + + /** + * This is the raw underlying certificate for the SSL channel. + */ + private final Certificate certificate; + + /** + * This is the channel representing the client connection. + */ + private final Channel channel; + + /** + * Constructor for the RequestCertificate. This is + * used to create a wrapper for the raw SSL certificate that + * is provided by the underlying SSL session. + * + * @param observer the observer used to observe the transaction + * @param entity the request entity containing the data + */ + public RequestCertificate(BodyObserver observer, Entity entity) { + this.challenge = new Challenge(observer, entity); + this.channel = entity.getChannel(); + this.certificate = channel.getCertificate(); + } + + /** + * This will return the X509 certificate chain, if any, that + * has been sent by the client. A certificate chain is typically + * only send when the server explicitly requests the certificate + * on the initial connection or when it is challenged for. + * + * @return this returns the clients X509 certificate chain + */ + public X509Certificate[] getChain() throws Exception { + return certificate.getChain(); + } + + /** + * This returns a challenge for the certificate. A challenge is + * issued by providing a Runnable task which is to + * be executed when the challenge has completed. Typically this + * task should be used to drive completion of an HTTPS request. + * + * @return this returns a challenge for the client certificate + */ + public CertificateChallenge getChallenge() throws Exception { + return challenge; + } + + /** + * This is used to determine if the X509 certificate chain is + * present for the request. If it is not present then a challenge + * can be used to request the certificate. + * + * @return true if the certificate chain is present + */ + public boolean isChainPresent() throws Exception { + return certificate.isChainPresent(); + } + + /** + * The Challenge provides a basic wrapper around the + * challenge provided by the SSL connection. It is used to enforce + * the workflow required by HTTP, this workflow requires that the + * SSL renegotiation be issued before the response is sent. This + * will also throw an exception if a challenge is issued for + * a request that already has a client certificate. + */ + private static class Challenge implements CertificateChallenge { + + /** + * This is the observer used to keep track of the HTTP transaction. + */ + private final BodyObserver observer; + + /** + * This is the certificate associated with the SSL connection. + */ + private final Certificate certificate; + + /** + * This is the channel representing the underlying TCP stream. + */ + private final Channel channel; + + /** + * Constructor for the Challenge object. This is + * basically a wrapper for the raw certificate challenge that + * will enforce some of the workflow required by HTTPS. + * + * @param observer this observer used to track the transaction + * @param entity this entity containing the request data + */ + public Challenge(BodyObserver observer, Entity entity) { + this.channel = entity.getChannel(); + this.certificate = channel.getCertificate(); + this.observer = observer; + } + + /** + * This method will challenge the client for their certificate. + * It does so by performing an SSL renegotiation. Successful + * completion of the SSL renegotiation results in the client + * providing their certificate, and execution of the task. + * + * @param completion task to be run on successful challenge + */ + public Future challenge() throws Exception { + return challenge(null); + } + + /** + * This method will challenge the client for their certificate. + * It does so by performing an SSL renegotiation. Successful + * completion of the SSL renegotiation results in the client + * providing their certificate, and execution of the task. + * + * @param completion task to be run on successful challenge + */ + public Future challenge(Runnable completion) throws Exception { + if(certificate == null) { + throw new IOException("Challenging must be done on a secure connection"); + } + CertificateChallenge challenge = certificate.getChallenge(); + + if(certificate.isChainPresent()) { + throw new IOException("Certificate is already present"); + } + if(observer.isCommitted()) { + throw new IOException("Response has already been committed"); + } + return challenge.challenge(completion); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCollector.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCollector.java new file mode 100644 index 0000000..027318d --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCollector.java @@ -0,0 +1,184 @@ +/* + * RequestCollector.java October 2002 + * + * Copyright (C) 2002, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.simpleframework.http.core.ContainerEvent.REQUEST_READY; +import static org.simpleframework.transport.TransportEvent.READ_WAIT; + +import java.io.IOException; +import java.nio.channels.SocketChannel; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.http.message.Body; +import org.simpleframework.http.message.EntityConsumer; +import org.simpleframework.http.message.Header; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.trace.Trace; + +/** + * The RequestCollector object is used to collect all of + * the data used to form a request entity. This will collect the data + * fragment by fragment from the underlying transport. When all of + * the data is consumed and the entity is created and then it is sent + * to the Selector object for processing. If the request + * has completed the next request can be collected from the + * underlying transport using a new collector object. + * + * @author Niall Gallagher + */ +class RequestCollector implements Collector { + + /** + * This is used to consume the request entity from the channel. + */ + private final EntityConsumer entity; + + /** + * This is the cursor used to read and reset the data. + */ + private final ByteCursor cursor; + + /** + * This is the channel used to acquire the underlying data. + */ + private final Channel channel; + + /** + * This is the trace used to listen for various collect events. + */ + private final Trace trace; + + /** + * This represents the time the request collection began at. + */ + private final Timer timer; + + /** + * The RequestCollector object used to collect the data + * from the underlying transport. In order to collect a body this + * must be given an Allocator which is used to create + * an internal buffer to store the consumed body. + * + * @param allocator this is the allocator used to buffer data + * @param tracker this is the tracker used to create sessions + * @param channel this is the channel used to read the data + */ + public RequestCollector(Allocator allocator, Channel channel) { + this.entity = new EntityConsumer(allocator, channel); + this.timer = new Timer(MILLISECONDS); + this.cursor = channel.getCursor(); + this.trace = channel.getTrace(); + this.channel = channel; + } + + /** + * This is used to collect the data from a Channel + * which is used to compose the entity. If at any stage there + * are no ready bytes on the socket the controller provided can + * be used to queue the collector until such time as the socket + * is ready to read. Also, should the entity have completed reading + * all required content it is handed to the controller as ready, + * which processes the entity as a new client HTTP request. + * + * @param controller this is the controller used to queue this + */ + public void collect(Controller controller) throws IOException { + while(cursor.isReady()) { + if(entity.isFinished()) { + break; + } else { + timer.set(); + entity.consume(cursor); + } + } + if(cursor.isOpen()) { + if(entity.isFinished()) { + trace.trace(REQUEST_READY); + controller.ready(this); + } else { + trace.trace(READ_WAIT); + controller.select(this); + } + } + } + + /** + * This is the time in milliseconds when the request was first + * read from the underlying channel. The time represented here + * represents the time collection of this request began. This + * does not necessarily represent the time the bytes arrived on + * the receive buffers as some data may have been buffered. + * + * @return this represents the time the request was ready at + */ + public long getTime() { + return timer.get(); + } + + /** + * This provides the HTTP request header for the entity. This is + * always populated and provides the details sent by the client + * such as the target URI and the query if specified. Also this + * can be used to determine the method and protocol version used. + * + * @return the header provided by the HTTP request message + */ + public Header getHeader() { + return entity.getHeader(); + } + + /** + * This is used to acquire the body for this HTTP entity. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Part objects. Each + * part can then be read as an individual message. + * + * @return the body provided by the HTTP request message + */ + public Body getBody() { + return entity.getBody(); + } + + /** + * This provides the connected channel for the client. This is + * used to send and receive bytes to and from an transport layer. + * Each channel provided with an entity contains an attribute + * map which contains information about the connection. + * + * @return the connected channel for this HTTP entity + */ + public Channel getChannel() { + return channel; + } + + /** + * This returns the socket channel that is used by the collector + * to read content from. This is a selectable socket, in that + * it can be registered with a Java NIO selector. This ensures + * that the system can be notified when the socket is ready. + * + * @return the socket channel used by this collector object + */ + public SocketChannel getSocket() { + return channel.getSocket(); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestDispatcher.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestDispatcher.java new file mode 100644 index 0000000..6b1531a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestDispatcher.java @@ -0,0 +1,128 @@ +/* + * RequestDispatcher.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.core.ContainerEvent.DISPATCH_FINISHED; +import static org.simpleframework.http.core.ContainerEvent.DISPATCH_REQUEST; +import static org.simpleframework.http.core.ContainerEvent.ERROR; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.message.Entity; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The RequestDispatcher object is used to dispatch a + * request and response to the container. This is the root task that + * executes all transactions. A transaction is dispatched to the + * container which can deal with it asynchronously, however as a + * safeguard the dispatcher will catch any exceptions thrown and close + * the connection if required. Closing the connection if an exception + * is thrown ensures that CLOSE_WAIT issues do not arise with open + * connections that can not be closed within the container. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.Container + */ +class RequestDispatcher implements Runnable { + + /** + * This is the observer object used to signal completion events. + */ + private final ResponseObserver observer; + + /** + * This is the container that is used to handle the transactions. + */ + private final Container container; + + /** + * This is the response object used to response to the request. + */ + private final Response response; + + /** + * This is the request object which contains the request entity. + */ + private final Request request; + + /** + * This is the channel associated with the request to dispatch. + */ + private final Channel channel; + + /** + * This is the trace that is used to track the request dispatch. + */ + private final Trace trace; + + /** + * Constructor for the RequestDispatcher object. This + * creates a request and response object using the provided entity, + * these can then be passed to the container to handle it. + * + * @param container this is the container to handle the request + * @param controller the controller used to handle the next request + * @param entity this contains the current request entity + */ + public RequestDispatcher(Container container, Controller controller, Entity entity) { + this.observer = new ResponseObserver(controller, entity); + this.request = new RequestEntity(observer, entity); + this.response = new ResponseEntity(observer, request, entity); + this.channel = entity.getChannel(); + this.trace = channel.getTrace(); + this.container = container; + } + + /** + * This run method will dispatch the created request + * and response objects to the container. This will interpret the + * target and semantics from the request object and compose a + * response for the request which is sent to the connected client. + */ + public void run() { + try { + dispatch(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } finally { + trace.trace(DISPATCH_FINISHED); + } + } + + /** + * This dispatch method will dispatch the request + * and response objects to the container. This will interpret the + * target and semantics from the request object and compose a + * response for the request which is sent to the connected client. + * If there is an exception this will close the socket channel. + */ + private void dispatch() throws Exception { + try { + trace.trace(DISPATCH_REQUEST); + container.handle(request, response); + } catch(Throwable cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } +} + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestEntity.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestEntity.java new file mode 100644 index 0000000..6060a4f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestEntity.java @@ -0,0 +1,398 @@ +/* + * RequestEntity.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.Protocol.CLOSE; +import static org.simpleframework.http.Protocol.CONNECTION; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Part; +import org.simpleframework.http.Query; +import org.simpleframework.http.Request; +import org.simpleframework.http.message.Body; +import org.simpleframework.http.message.Entity; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; + +/** + * This object is used to represent a HTTP request. This defines the + * attributes that a HTTP request has such as a request line and the + * headers that come with the message header. + *

+ * The Request is used to provide an interface to the + * HTTP InputStream and message header. The stream can + * have certain characteristics, these characteristics are available + * by this object. The Request provides methods that + * allow the InputStream's semantics to be known, for + * example if the stream is keep-alive or if the stream has a length. + *

+ * The Request origin is also retrievable from the + * Request as is the attributes Map object + * which defines specific connection attributes. And acts as a + * simple model for the request transaction. + *

+ * It is important to note that the Request controls + * the processing of the HTTP pipeline. The next HTTP request is + * not processed until the request has read all of the content body + * within the InputStream. The stream must be fully + * read or closed for the next request to be processed. + * + * @author Niall Gallagher + */ +class RequestEntity extends RequestMessage implements Request { + + /** + * This is the certificate associated with the connection. + */ + private Certificate certificate; + + /** + * This will create the form object using the query and body. + */ + private QueryBuilder builder; + + /** + * This channel represents the connected pipeline used. + */ + private Channel channel; + + /** + * The query contains all the parameters for the request. + */ + private Query query; + + /** + * The body contains the message content sent by the client. + */ + private Body body; + + /** + * This is used to contain the values for this request. + */ + private Map map; + + /** + * This is the time at which the request is ready to be used. + */ + private long time; + + /** + * Constructor for the RequestEntity object. This is + * used to create a request that contains all the parts sent by + * the client, including the headers and the request body. Each of + * the request elements are accessible through this object in a + * convenient manner, all parts and parameters, as well as cookies + * can be accessed and used without much effort. + * + * @param observer this is the observer used to monitor events + * @param entity this is the entity that was sent by the client + */ + public RequestEntity(ResponseObserver observer, Entity entity) { + this.certificate = new RequestCertificate(observer, entity); + this.builder = new QueryBuilder(this, entity); + this.channel = entity.getChannel(); + this.header = entity.getHeader(); + this.body = entity.getBody(); + this.time = entity.getTime(); + } + + /** + * This is used to determine if the request has been transferred + * over a secure connection. If the protocol is HTTPS and the + * content is delivered over SSL then the request is considered + * to be secure. Also the associated response will be secure. + * + * @return true if the request is transferred securely + */ + public boolean isSecure() { + return channel.isSecure(); + } + + /** + * This is a convenience method that is used to determine whether + * or not this message has the Connection header with the close + * token. If the close token is present then this stream is not a + * keep-alive connection. However if this has no Connection header + * then the keep alive status is determined by the HTTP version, + * that is HTTP/1.1 is keep alive by default, HTTP/1.0 has the + * connection close by default. + * + * @return returns true if this is keep alive connection + */ + public boolean isKeepAlive(){ + String value = getValue(CONNECTION); + + if(value == null) { + int major = getMajor(); + int minor = getMinor(); + + if(major > 1) { + return true; + } + if(major == 1) { + return minor > 0; + } + return false; + } + return !value.equalsIgnoreCase(CLOSE); + } + + /** + * This is the time in milliseconds when the request was first + * read from the underlying socket. The time represented here + * represents the time collection of this request began. This + * does not necessarily represent the time the bytes arrived on + * the receive buffers as some data may have been buffered. + * + * @return this represents the time the request arrived at + */ + public long getRequestTime() { + return time; + } + + /** + * This provides the underlying channel for the request. It + * contains the TCP socket channel and various other low level + * components. Typically this will only ever be needed when + * there is a need to switch protocols. + * + * @return the underlying channel for this request + */ + public Channel getChannel() { + return channel; + } + + /** + * This is used to acquire the SSL certificate used when the + * server is using a HTTPS connection. For plain text connections + * or connections that use a security mechanism other than SSL + * this will be null. This is only available when the connection + * makes specific use of an SSL engine to secure the connection. + * + * @return this returns the associated SSL certificate if any + */ + public Certificate getClientCertificate() { + if(channel.isSecure()) { + return certificate; + } + return null; + } + + /** + * This is used to acquire the remote client address. This can + * be used to acquire both the port and the I.P address for the + * client. It allows the connected clients to be logged and if + * require it can be used to perform course grained security. + * + * @return this returns the client address for this request + */ + public InetSocketAddress getClientAddress() { + SocketChannel socket = channel.getSocket(); + Socket client = socket.socket(); + + return getClientAddress(client); + } + + /** + * This is used to acquire the remote client address. This can + * be used to acquire both the port and the I.P address for the + * client. It allows the connected clients to be logged and if + * require it can be used to perform course grained security. + * + * @param socket this is the socket to get the address for + * + * @return this returns the client address for this request + */ + private InetSocketAddress getClientAddress(Socket socket) { + InetAddress address = socket.getInetAddress(); + int port = socket.getPort(); + + return new InetSocketAddress(address, port); + } + + /** + * This is used to get the content body. This will essentially get + * the content from the body and present it as a single string. + * The encoding of the string is determined from the content type + * charset value. If the charset is not supported this will throw + * an exception. Typically only text values should be extracted + * using this method if there is a need to parse that content. + * + * @return the body content containing the message body + */ + public String getContent() throws IOException { + ContentType type = getContentType(); + + if(type == null) { + return body.getContent("ISO-8859-1"); + } + return getContent(type); + } + + /** + * This is used to get the content body. This will essentially get + * the content from the body and present it as a single string. + * The encoding of the string is determined from the content type + * charset value. If the charset is not supported this will throw + * an exception. Typically only text values should be extracted + * using this method if there is a need to parse that content. + * + * @param type this is the content type used with the request + * + * @return the input stream containing the message body + */ + public String getContent(ContentType type) throws IOException { + String charset = type.getCharset(); + + if(charset == null) { + charset = "ISO-8859-1"; + } + return body.getContent(charset); + } + + /** + * This is used to read the content body. The specifics of the data + * that is read from this InputStream can be determined + * by the getContentLength method. If the data sent by + * the client is chunked then it is decoded, see RFC 2616 section + * 3.6. Also multipart data is available as Part objects + * however the raw content of the multipart body is still available. + * + * @return the input stream containing the message body + */ + public InputStream getInputStream() throws IOException { + return body.getInputStream(); + } + + /** + * This is used to read the content body. The specifics of the data + * that is read from this ReadableByteChannel can be + * determined by the getContentLength method. If the + * data sent by the client is chunked then it is decoded, see RFC + * 2616 section 3.6. This stream will never provide empty reads as + * the content is internally buffered, so this can do a full read. + * + * @return this returns the byte channel used to read the content + */ + public ReadableByteChannel getByteChannel() throws IOException { + InputStream source = getInputStream(); + + if(source != null) { + return Channels.newChannel(source); + } + return null; + } + + /** + * This can be used to retrieve the response attributes. These can + * be used to keep state with the response when it is passed to + * other systems for processing. Attributes act as a convenient + * model for storing objects associated with the response. This + * also inherits attributes associated with the client connection. + * + * @return the attributes that have been added to this request + */ + public Map getAttributes() { + Map common = channel.getAttributes(); + + if(map == null) { + map = new HashMap(common); + } + return map; + } + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the attribute Map + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param key this is the key of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + public Object getAttribute(Object key) { + return getAttributes().get(key); + } + + /** + * This method is used to acquire the query part from the HTTP + * request URI target and a form post if it exists. Both the + * query and the form post are merge together in a single query. + * + * @return the query associated with the HTTP target URI + */ + public Query getQuery() { + if(query == null) { + query = builder.build(); + } + return query; + } + + /** + * This is used to provide quick access to the parameters. This + * avoids having to acquire the request Form object. + * This basically acquires the parameters object and invokes + * the getParameters method with the given name. + * + * @param name this is the name of the parameter value + */ + public String getParameter(String name) { + return getQuery().get(name); + } + + /** + * This method is used to acquire a Part from the + * HTTP request using a known name for the part. This is typically + * used when there is a file upload with a multipart POST request. + * All parts that are not files can be acquired as string values + * from the attachment object. + * + * @param name this is the name of the part object to acquire + * + * @return the named part or null if the part does not exist + */ + public Part getPart(String name) { + return body.getPart(name); + } + + /** + * This method is used to get all Part objects that + * are associated with the request. Each attachment contains the + * body and headers associated with it. If the request is not a + * multipart POST request then this will return an empty list. + * + * @return the list of parts associated with this request + */ + public List getParts() { + return body.getParts(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestMessage.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestMessage.java new file mode 100644 index 0000000..b22c74e --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestMessage.java @@ -0,0 +1,341 @@ +/* + * RequestMessage.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.util.List; +import java.util.Locale; + +import org.simpleframework.http.Address; +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.Path; +import org.simpleframework.http.Query; +import org.simpleframework.http.RequestHeader; +import org.simpleframework.http.message.Header; + +/** + * The RequestMessage object is used to create a HTTP + * request header representation. All requests for details within a + * request message delegates to an underlying header, which contains + * all of the header names and values sent by the client. The header + * names are case insensitively mapped as required by RFC 2616. + * + * @author Niall Gallagher + */ +class RequestMessage implements RequestHeader { + + /** + * This is the underlying header used to house the headers. + */ + protected Header header; + + /** + * Constructor for the RequestMessage object. This + * is used to create a request message without an underlying + * header. In such an event it is up to the subclass to provide + * the instance, this is useful for testing the request. + */ + public RequestMessage() { + super(); + } + + /** + * Constructor for the RequestMessage object. This + * is used to create a request with a header instance. In such + * a case the header provided will be queried for headers and is + * used to store headers added to this message instance. + * + * @param header this is the backing header for the message + */ + public RequestMessage(Header header) { + this.header = header; + } + + /** + * This can be used to get the URI specified for this HTTP + * request. This corresponds to the /index part of a + * http://www.domain.com/index URL but may contain the full + * URL. This is a read only value for the request. + * + * @return the URI that this HTTP request is targeting + */ + public String getTarget() { + return header.getTarget(); + } + + /** + * This is used to acquire the address from the request line. + * An address is the full URI including the scheme, domain, port + * and the query parts. This allows various parameters to be + * acquired without having to parse the raw request target URI. + * + * @return this returns the address of the request line + */ + public Address getAddress() { + return header.getAddress(); + } + + /** + * This is used to acquire the path as extracted from the HTTP + * request URI. The Path object that is provided by + * this method is immutable, it represents the normalized path + * only part from the request uniform resource identifier. + * + * @return this returns the normalized path for the request + */ + public Path getPath() { + return header.getPath(); + } + + /** + * This method is used to acquire the query part from the + * HTTP request URI target. This will return only the values + * that have been extracted from the request URI target. + * + * @return the query associated with the HTTP target URI + */ + public Query getQuery() { + return header.getQuery(); + } + + /** + * This can be used to get the HTTP method for this request. The + * HTTP specification RFC 2616 specifies the HTTP request methods + * in section 9, Method Definitions. Typically this will be a + * GET, POST or a HEAD method, although any string is possible. + * + * @return the request method for this request message + */ + public String getMethod() { + return header.getMethod(); + } + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 1 + * of a HTTP/1.0 version string. + * + * @return the major version number for the request message + */ + public int getMajor() { + return header.getMajor(); + } + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 0 + * of a HTTP/1.0 version string. This is used to determine if + * the request message has keep alive semantics. + * + * @return the major version number for the request message + */ + public int getMinor() { + return header.getMinor(); + } + + /** + * This method is used to get a List of the names + * for the headers. This will provide the original names for the + * HTTP headers for the message. Modifications to the provided + * list will not affect the header, the list is a simple copy. + * + * @return this returns a list of the names within the header + */ + public List getNames() { + return header.getNames(); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma seperated list of + * values the value returned is the first value in that list. + * This returns null if theres no HTTP message header. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name) { + return header.getValue(name); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if theres no HTTP message header. + * + * @param name the HTTP message header to get the value from + * @param index if there are multiple values this selects one + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name, int index) { + return header.getValue(name, index); + } + + /** + * This can be used to get the integer of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public int getInteger(String name) { + return header.getInteger(name); + } + + /** + * This can be used to get the date of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public long getDate(String name) { + return header.getDate(name); + } + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benifits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearence. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has higest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered array of tokens extracted from the header(s) + */ + public List getValues(String name) { + return header.getValues(name); + } + + /** + * This is used to acquire the locales from the request header. The + * locales are provided in the Accept-Language header. + * This provides an indication as to the languages that the client + * accepts. It provides the locales in preference order. + * + * @return this returns the locales preferred by the client + */ + public List getLocales() { + return header.getLocales(); + } + + /** + * This is used to acquire a cookie usiing the name of that cookie. + * If the cookie exists within the HTTP header then it is returned + * as a Cookie object. Otherwise this method will + * return null. Each cookie object will contain the name, value + * and path of the cookie as well as the optional domain part. + * + * @param name this is the name of the cookie object to acquire + * + * @return this returns a cookie object from the header or null + */ + public Cookie getCookie(String name) { + return header.getCookie(name); + } + + /** + * This is used to acquire all cookies that were sent in the header. + * If any cookies exists within the HTTP header they are returned + * as Cookie objects. Otherwise this method will an + * empty list. Each cookie object will contain the name, value and + * path of the cookie as well as the optional domain part. + * + * @return this returns all cookie objects from the HTTP header + */ + public List getCookies() { + return header.getCookies(); + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + public ContentType getContentType() { + return header.getContentType(); + } + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return the content length, or -1 if it cannot be determined + */ + public long getContentLength() { + return header.getContentLength(); + } + + /** + * This method returns a CharSequence holding the header + * consumed for the request. A character sequence is returned as it + * can provide a much more efficient means of representing the header + * data by just wrapping the consumed byte array. + * + * @return this returns the characters consumed for the header + */ + public CharSequence getHeader() { + return header.getHeader(); + } + + /** + * This is used to provide a string representation of the header + * read. Providing a string representation of the header is used + * so that on debugging the contents of the delivered header can + * be inspected in order to determine a cause of error. + * + * @return this returns a string representation of the header + */ + public String toString() { + return header.toString(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestReader.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestReader.java new file mode 100644 index 0000000..6f8cbaa --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/RequestReader.java @@ -0,0 +1,131 @@ +/* + * RequestReader.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.core.ContainerEvent.ERROR; + +import java.nio.channels.SocketChannel; + +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.reactor.Operation; +import org.simpleframework.transport.trace.Trace; + +/** + * The RequestReader object is used to read the bytes + * that form the request entity. In order to execute a read operation + * the socket must be read ready. This is determined using the socket + * object, which is registered with a controller. If at any point the + * reading results in an error the operation is cancelled and the + * collector is closed, which shuts down the connection. + * + * @author Niall Gallagher + * + * @see org.simpleframework.transport.reactor.Reactor + */ +class RequestReader implements Operation { + + /** + * This is the selector used to process the collection events. + */ + private final Controller controller; + + /** + * This is the collector used to consume the entity bytes. + */ + private final Collector collector; + + /** + * This is the channel object associated with the collector. + */ + private final Channel channel; + + /** + * This is used to collect any trace information. + */ + private final Trace trace; + + /** + * Constructor for the RequestReader object. This is + * used to collect the data required to compose a HTTP request. + * Once all the data has been read by this it is dispatched. + * + * @param controller the controller object used to process events + * @param collector this is the task used to collect the entity + */ + public RequestReader(Controller controller, Collector collector){ + this.channel = collector.getChannel(); + this.trace = channel.getTrace(); + this.collector = collector; + this.controller = controller; + } + + /** + * This is used to acquire the trace object that is associated + * with the operation. A trace object is used to collection details + * on what operations are being performed. For instance it may + * contain information relating to I/O events or errors. + * + * @return this returns the trace associated with this operation + */ + public Trace getTrace() { + return trace; + } + + /** + * This is the SocketChannel used to determine if the + * connection has some bytes that can be read. If it contains any + * data then that data is read from and is used to compose the + * request entity, which consists of a HTTP header and body. + * + * @return this returns the socket for the connected pipeline + */ + public SocketChannel getChannel() { + return channel.getSocket(); + } + + /** + * This run method is used to collect the bytes from + * the connected channel. If a sufficient amount of data is read + * from the socket to form a HTTP entity then the collector uses + * the Selector object to dispatch the request. This + * is sequence of events that occur for each transaction. + */ + public void run() { + try { + collector.collect(controller); + }catch(Throwable cause){ + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * This is used to cancel the operation if it has timed out. If + * the retry is waiting too long to read content from the socket + * then the retry is cancelled and the underlying transport is + * closed. This helps to clean up occupied resources. + */ + public void cancel() { + try { + channel.close(); + } catch(Throwable cause) { + trace.trace(ERROR, cause); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseBuffer.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseBuffer.java new file mode 100644 index 0000000..d2a9f80 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseBuffer.java @@ -0,0 +1,303 @@ +/* + * ResponseBuffer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; + +import org.simpleframework.http.Response; +import org.simpleframework.http.message.Entity; +import org.simpleframework.transport.Channel; + +/** + * The ResponseBuffer object is an output stream that can + * buffer bytes written up to a given size. This is used if a buffer + * is requested for the response output. Such a mechanism allows the + * response to be written without committing the response. Also it + * enables content that has been written to be reset, by simply + * clearing the response buffer. If the response buffer overflows + * then the response is committed. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.ResponseEncoder + */ +class ResponseBuffer extends OutputStream implements WritableByteChannel { + + /** + * This is the transfer object used to transfer the response. + */ + private ResponseEncoder encoder; + + /** + * This is the buffer used to accumulate the response bytes. + */ + private byte[] buffer; + + /** + * This is used to determine if the accumulate was flushed. + */ + private boolean flushed; + + /** + * This is used to determine if the accumulator was closed. + */ + private boolean closed; + + /** + * This counts the number of bytes that have been accumulated. + */ + private int count; + + /** + * Constructor for the ResponseBuffer object. This will + * create a buffering output stream which will flush data to the + * underlying transport provided with the entity. All I/O events + * are reported to the monitor so the server can process other + * requests within the pipeline when the current one is finished. + * + * @param observer this is used to notify of response completion + * @param response this is the response header for this buffer + * @param support this is used to determine the response semantics + * @param entity this is used to acquire the underlying transport + */ + public ResponseBuffer(BodyObserver observer, Response response, Conversation support, Entity entity) { + this(observer, response, support, entity.getChannel()); + } + + /** + * Constructor for the ResponseBuffer object. This will + * create a buffering output stream which will flush data to the + * underlying transport provided with the channel. All I/O events + * are reported to the monitor so the server can process other + * requests within the pipeline when the current one is finished. + * + * @param observer this is used to notify of response completion + * @param response this is the response header for this buffer + * @param support this is used to determine the response semantics + * @param channel this is the channel used to write the data to + */ + public ResponseBuffer(BodyObserver observer, Response response, Conversation support, Channel channel) { + this.encoder = new ResponseEncoder(observer, response, support, channel); + this.buffer = new byte[] {}; + } + + /** + * This is used to determine if the accumulator is still open. If + * the accumulator is still open then data can still be written to + * it and this transmitted to the client. When the accumulator is + * closed the data is committed and this can not be used. + * + * @return this returns true if the accumulator object is open + */ + public boolean isOpen() { + return !closed; + } + + /** + * This is used to reset the buffer so that it can be written to + * again. If the accumulator has already been flushed then the + * stream can not be reset. Resetting the stream is typically + * done if there is an error in writing the response and an error + * message is generated to replaced the partial response. + */ + public void reset() throws IOException { + if(flushed) { + throw new IOException("Response has been flushed"); + } + count = 0; + } + + /** + * This is used to write the provided octet to the buffer. If the + * buffer is full it will be flushed and the octet is appended to + * the start of the buffer. If however the buffer is zero length + * then this will write directly to the underlying transport. + * + * @param octet this is the octet that is to be written + */ + public void write(int octet) throws IOException { + byte value = (byte) octet; + + if(closed) { + throw new IOException("Response has been transferred"); + } + write(new byte[] { value }); + } + + /** + * This is used to write the provided array to the buffer. If the + * buffer is full it will be flushed and the array is appended to + * the start of the buffer. If however the buffer is zero length + * then this will write directly to the underlying transport. + * + * @param array this is the array of bytes to send to the client + * @param off this is the offset within the array to send from + * @param size this is the number of bytes that are to be sent + */ + public void write(byte[] array, int off, int size) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(array, off, size); + + if(size > 0) { + write(buffer); + } + } + + /** + * This is used to write the provided buffer to the buffer. If the + * buffer is full it will be flushed and the buffer is appended to + * the start of the buffer. If however the buffer is zero length + * then this will write directly to the underlying transport. + * + * @param source this is the byte buffer to send to the client + * + * @return this returns the number of bytes that have been sent + */ + public int write(ByteBuffer source) throws IOException { + int mark = source.position(); + int size = source.limit(); + + if(mark > size) { + throw new ResponseException("Buffer position greater than limit"); + } + return write(source, 0, size - mark); + } + + /** + * This is used to write the provided buffer to the buffer. If the + * buffer is full it will be flushed and the buffer is appended to + * the start of the buffer. If however the buffer is zero length + * then this will write directly to the underlying transport. + * + * @param source this is the byte buffer to send to the client + * @param off this is the offset within the array to send from + * @param size this is the number of bytes that are to be sent + * + * @return this returns the number of bytes that have been sent + */ + public int write(ByteBuffer source, int off, int size) throws IOException { + if(closed) { + throw new IOException("Response has been transferred"); + } + int mark = source.position(); + int limit = source.limit(); + + if(limit - mark < size) { // not enough data + size = limit - mark; // reduce expectation + } + if(count + size > buffer.length) { + flush(false); + } + if(size > buffer.length){ + encoder.write(source); + } else { + source.get(buffer, count, size); + count += size; + } + return size; + } + + /** + * This is used to expand the capacity of the internal buffer. If + * there is already content that has been appended to the buffer + * this will copy that data to the newly created buffer. This + * will not decrease the size of the buffer if it is larger than + * the requested capacity. + * + * @param capacity this is the capacity to expand the buffer to + */ + public void expand(int capacity) throws IOException { + if(buffer.length < capacity) { + int size = buffer.length * 2; + int resize = Math.max(capacity, size); + byte[] temp = new byte[resize]; + + System.arraycopy(buffer, 0, temp, 0, count); + buffer = temp; + } + } + + /** + * This is used to flush the contents of the buffer to the + * underlying transport. Once the accumulator is flushed the HTTP + * headers are written such that the semantics of the connection + * match the protocol version and the existing response headers. + */ + public void flush() throws IOException { + flush(true); + } + + /** + * This is used to flush the contents of the buffer to the + * underlying transport. Once the accumulator is flushed the HTTP + * headers are written such that the semantics of the connection + * match the protocol version and the existing response headers. + * + * @param flush indicates whether the transport should be flushed + */ + private void flush(boolean flush) throws IOException { + if(!flushed) { + encoder.start(); + } + if(count > 0) { + encoder.write(buffer, 0, count); + } + if(flush) { + encoder.flush(); + } + flushed = true; + count = 0; + } + + /** + * This will flush the buffer to the underlying transport and + * close the stream. Once the accumulator is flushed the HTTP + * headers are written such that the semantics of the connection + * match the protocol version and the existing response headers. + * Closing this stream does not mean the connection is closed. + */ + public void close() throws IOException { + if(!closed) { + commit(); + } + flushed = true; + closed = true; + } + + /** + * This will close the underlying transfer object which will + * notify the server kernel that the next request is read to be + * processed. If the accumulator is unflushed then this will set + * a Content-Length header such that it matches the number of + * bytes that are buffered within the internal buffer. + */ + private void commit() throws IOException { + if(!flushed) { + encoder.start(count); + } + if(count > 0) { + encoder.write(buffer, 0, count); + } + encoder.close(); + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEncoder.java new file mode 100644 index 0000000..0fbe39b --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEncoder.java @@ -0,0 +1,324 @@ +/* + * ResponseEncoder.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.core.ContainerEvent.WRITE_BODY; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.simpleframework.http.Response; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The ResponseEncoder object acts as a means to determine + * the transfer encoding for the response body. This will ensure that + * the correct HTTP headers are used when the transfer of the body begins. + * In order to determine what headers to use this can be provided + * with a content length value. If the start method is + * provided with the content length then the HTTP headers will use a + * Content-Length header as the message delimiter. If there is no + * content length provided then the chunked encoding is used for + * HTTP/1.1 and connection close is used for HTTP/1.0. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.core.BodyEncoder + */ +class ResponseEncoder { + + /** + * This is used to create a encoder based on the HTTP headers. + */ + private BodyEncoderFactory factory; + + /** + * This is used to determine the type of transfer required. + */ + private Conversation support; + + /** + * This is the response message that is to be committed. + */ + private Response response; + + /** + * Once the header is committed this is used to produce data. + */ + private BodyEncoder encoder; + + /** + * This is the trace used to monitor events in the data transfer. + */ + private Trace trace; + + /** + * Constructor for the ResponseEncoder object, this is + * used to create an object used to transfer a response body. This + * must be given a Conversation that can be used to set + * and get information regarding the type of transfer required. + * + * @param observer this is used to signal for response completion + * @param response this is the actual response message + * @param support this is used to determine the semantics + * @param channel this is the connected TCP channel for the response + */ + public ResponseEncoder(BodyObserver observer, Response response, Conversation support, Channel channel) { + this.factory = new BodyEncoderFactory(observer, support, channel); + this.trace = channel.getTrace(); + this.response = response; + this.support = support; + } + + /** + * This is used to determine if the transfer has started. It has + * started when a encoder is created and the HTTP headers have + * been sent, or at least handed to the underlying transport. + * Once started the semantics of the connection can not change. + * + * @return this returns whether the transfer has started + */ + public boolean isStarted() { + return encoder != null; + } + + /** + * This starts the transfer with no specific content length set. + * This is typically used when dynamic data is emitted ans will + * require chunked encoding for HTTP/1.1 and connection close + * for HTTP/1.0. Once invoked the HTTP headers are committed. + */ + public void start() throws IOException { + if(encoder != null) { + throw new ResponseException("Transfer has already started"); + } + clear(); + configure(); + commit(); + } + + /** + * This starts the transfer with a known content length. This is + * used when there is a Content-Length header set. This will not + * encode the content for HTTP/1.1 however, HTTP/1.0 may need + * a connection close if it does not have keep alive semantics. + * + * @param length this is the length of the response body + */ + public void start(int length) throws IOException { + if(encoder != null) { + throw new ResponseException("Transfer has already started"); + } + clear(); + configure(length); + commit(); + } + + /** + * This method is used to write content to the underlying socket. + * This will make use of the Producer object to + * encode the response body as required. If the encoder has not + * been created then this will throw an exception. + * + * @param array this is the array of bytes to send to the client + */ + public void write(byte[] array) throws IOException { + write(array, 0, array.length); + } + + /** + * This method is used to write content to the underlying socket. + * This will make use of the Producer object to + * encode the response body as required. If the encoder has not + * been created then this will throw an exception. + * + * @param array this is the array of bytes to send to the client + * @param off this is the offset within the array to send from + * @param len this is the number of bytes that are to be sent + */ + public void write(byte[] array, int off, int len) throws IOException { + if(encoder == null) { + throw new ResponseException("Conversation details not ready"); + } + trace.trace(WRITE_BODY, len); + encoder.encode(array, off, len); + } + + /** + * This method is used to write content to the underlying socket. + * This will make use of the Producer object to + * encode the response body as required. If the encoder has not + * been created then this will throw an exception. + * + * @param buffer this is the buffer of bytes to send to the client + */ + public void write(ByteBuffer buffer) throws IOException { + int mark = buffer.position(); + int size = buffer.limit(); + + if(mark > size) { + throw new ResponseException("Buffer position greater than limit"); + } + write(buffer, 0, size - mark); + } + + /** + * This method is used to write content to the underlying socket. + * This will make use of the Producer object to + * encode the response body as required. If the encoder has not + * been created then this will throw an exception. + * + * @param buffer this is the buffer of bytes to send to the client + * @param off this is the offset within the buffer to send from + * @param len this is the number of bytes that are to be sent + */ + public void write(ByteBuffer buffer, int off, int len) throws IOException { + if(encoder == null) { + throw new ResponseException("Conversation details not ready"); + } + trace.trace(WRITE_BODY, len); + encoder.encode(buffer, off, len); + } + + /** + * This method is used to flush the contents of the buffer to + * the client. This method will block until such time as all of + * the data has been sent to the client. If at any point there + * is an error sending the content an exception is thrown. + */ + public void flush() throws IOException { + if(encoder == null) { + throw new ResponseException("Conversation details not ready"); + } + encoder.flush(); + } + + /** + * This is used to signal to the encoder that all content has + * been written and the user no longer needs to write. This will + * either close the underlying transport or it will notify the + * monitor that the response has completed and the next request + * can begin. This ensures the content is flushed to the client. + */ + public void close() throws IOException { + if(encoder == null) { + throw new ResponseException("Conversation details not ready"); + } + encoder.close(); + } + + /** + * This method is used to set the required HTTP headers on the + * response. This will check the existing HTTP headers, and if + * there is insufficient data chunked encoding will be used for + * HTTP/1.1 and connection close will be used for HTTP/1.0. + */ + private void configure() throws IOException { + long length = support.getContentLength(); + boolean empty = support.isEmpty(); + boolean tunnel = support.isTunnel(); + + if(tunnel) { + support.setConnectionUpgrade(); + } else if(empty) { + support.setContentLength(0); + } else if(length >= 0) { + support.setContentLength(length); + } else { + support.setChunkedEncoded(); + } + encoder = factory.getInstance(); + } + + /** + * This method is used to set the required HTTP headers on the + * response. This will check the existing HTTP headers, and if + * there is insufficient data chunked encoding will be used for + * HTTP/1.1 and connection close will be used for HTTP/1.0. + * + * @param count this is the number of bytes to be transferred + */ + private void configure(long count) throws IOException { + long length = support.getContentLength(); + + if(support.isHead()) { + if(count > 0) { + configure(count, count); + } else { + configure(count, length); + } + } else { + configure(count, count); + } + } + + /** + * This method is used to set the required HTTP headers on the + * response. This will check the existing HTTP headers, and if + * there is insufficient data chunked encoding will be used for + * HTTP/1.1 and connection close will be used for HTTP/1.0. + * + * @param count this is the number of bytes to be transferred + * @param length this is the actual length value to be used + */ + private void configure(long count, long length) throws IOException { + boolean empty = support.isEmpty(); + boolean tunnel = support.isTunnel(); + + if(tunnel) { + support.setConnectionUpgrade(); + } else if(empty) { + support.setContentLength(0); + } else if(length >= 0) { + support.setContentLength(length); + } else { + support.setChunkedEncoded(); + } + encoder = factory.getInstance(); + } + + /** + * This is used to clear any previous encoding that has been set + * in the event that content length may be used instead. This is + * used so that an override can be made to the transfer encoding + * such that content length can be used instead. + */ + private void clear() throws IOException { + support.setIdentityEncoded(); + + } + + /** + * This is used to compose the HTTP header and send it over the + * transport to the client. Once done the response is committed + * and no more headers can be set, also the semantics of the + * response have been committed and the encoder is created. + */ + private void commit() throws IOException { + try { + response.commit(); + } catch(Exception cause) { + throw new ResponseException("Unable to commit", cause); + } + } + +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEntity.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEntity.java new file mode 100644 index 0000000..ae7aed9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEntity.java @@ -0,0 +1,437 @@ +/* + * ResponseEntity.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.Protocol.CONTENT_LENGTH; +import static org.simpleframework.http.Protocol.CONTENT_TYPE; +import static org.simpleframework.http.core.ContainerEvent.WRITE_HEADER; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.channels.WritableByteChannel; +import java.util.Map; + +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.message.Entity; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +/** + * This is used to represent the HTTP response. This provides methods + * that can be used to set various characteristics of the response. + * The OutputStream of the Response can be + * retrieved from this interface as can the I.P address of the client + * that will be receiving the Response. The attributes + * of the connection can be retrieved also. This provides a set of + * methods that can be used to set the attributes of the stream so + * the Response can be transported properly. The headers + * can be set and will be sent once a commit is made, or when there + * is content sent over the output stream. + *

+ * This should never allow the message body be sent if it should not + * be sent with the headers as of RFC 2616 rules for the presence of + * a message body. A message body must not be included with a HEAD + * request or with a 304 or a 204 response. A proper implementation + * of this will prevent a message body being sent if the response + * is to a HEAD request of if there is a 304 or 204 response code. + *

+ * It is important to note that the Response controls + * the processing of the HTTP pipeline. The next HTTP request is + * not processed until the response has committed. The response is + * committed once the commit method is invoked if there + * is NO content body. Committing with a content body is done only if + * correct content is given. The OutputStream acts as + * a client and commits the response once the specified content has + * been written to the issued OutputStream. + * + * @author Niall Gallagher + */ +class ResponseEntity extends ResponseMessage implements Response { + + /** + * This is the observer that is used to monitor the response. + */ + private BodyObserver observer; + + /** + * This is used to buffer the bytes that are sent to the client. + */ + private ResponseBuffer buffer; + + /** + * This is the conversation used to determine connection type. + */ + private Conversation support; + + /** + * This is the underlying channel for the connected pipeline. + */ + private Channel channel; + + /** + * This is the sender object used to deliver to response data. + */ + private ByteWriter sender; + + /** + * This is used to trace events that occur with the response + */ + private Trace trace; + + /** + * Constructor for the ResponseEntity object. This is + * used to create a response instance using the provided request, + * entity, and monitor object. To ensure that the response is + * compatible with client the Request is used. Also + * to ensure the next request can be processed the provided monitor + * is used to signal response events to the server kernel. + * + * @param observer this is the observer used to signal events + * @param request this is the request that was sent by the client + * @param entity this is the entity that contains the channel + */ + public ResponseEntity(BodyObserver observer, Request request, Entity entity) { + this.support = new Conversation(request, this); + this.buffer = new ResponseBuffer(observer, this, support, entity); + this.channel = entity.getChannel(); + this.sender = channel.getWriter(); + this.trace = channel.getTrace(); + this.observer = observer; + } + + /** + * This represents the time at which the response has fully written. + * Because the response is delivered asynchronously to the client + * this response time does not represent the time to last byte. + * It simply represents the time at which the response has been + * fully generated and written to the output buffer or queue. This + * returns zero if the response has not finished. + * + * @return this is the time taken to complete the response + */ + public long getResponseTime() { + return observer.getTime(); + } + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the Attributes + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param name this is the name of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + public Object getAttribute(Object name) { + return getAttributes().get(name); + } + + /** + * This can be used to retrieve certain attributes about + * this Response. The attributes contains certain + * properties about the Response. For example if + * this Response goes over a secure line then there may be any + * arbitrary attributes. + * + * @return the response attributes of that have been set + */ + public Map getAttributes() { + return channel.getAttributes(); + } + + /** + * This should be used when the size of the message body is known. For + * performance reasons this should be used so the length of the output + * is known. This ensures that Persistent HTTP (PHTTP) connections + * can be maintained for both HTTP/1.0 and HTTP/1.1 clients. If the + * length of the output is not known HTTP/1.0 clients will require a + * connection close, which reduces performance (see RFC 2616). + *

+ * This removes any previous Content-Length headers from the message + * header. This will then set the appropriate Content-Length header with + * the correct length. If a the Connection header is set with the close + * token then the semantics of the connection are such that the server + * will close it once the OutputStream.close is used. + * + * @param length this is the length of the HTTP message body + */ + public void setContentLength(long length) { + setLong(CONTENT_LENGTH, length); + } + + /** + * This is used to set the content type for the response. Typically + * a response will contain a message body of some sort. This is used + * to conveniently set the type for that response. Setting the + * content type can also be done explicitly if desired. + * + * @param type this is the type that is to be set in the response + */ + public void setContentType(String type) { + setValue(CONTENT_TYPE, type); + } + + /** + * This determines the charset for PrintStream objects + * returned from the getPrintStream method. This will + * return a valid charset regardless of whether the Content-Type + * header has been set, set without a charset, or not set at all. + * If unspecified, the charset returned is ISO-8859-1, + * as suggested by RFC 2616, section 3.7.1. + * + * @return returns the charset used by this response object + */ + private String getCharset() { + ContentType type = getContentType(); + + if(type == null) { + return "ISO-8859-1"; + } + if(type.getCharset()==null){ + return "ISO-8859-1"; + } + return type.getCharset(); + } + + /** + * Used to write a message body with the Response. The + * semantics of this OutputStream will be determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + * + * @return an output stream object used to write the message body + */ + public OutputStream getOutputStream() throws IOException { + return buffer; + } + + /** + * Used to write a message body with the Response. The + * semantics of this OutputStream will be determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + *

+ * This will ensure that there is buffering done so that the output + * can be reset using the reset method. This will + * enable the specified number of bytes to be written without + * committing the response. This specified size is the minimum size + * that the response buffer must be. + * + * @param size the minimum size that the response buffer must be + * + * @return an output stream object used to write the message body + */ + public OutputStream getOutputStream(int size) throws IOException { + if(size > 0) { + buffer.expand(size); + } + return buffer; + } + + /** + * This method is provided for convenience so that the HTTP content + * can be written using the print methods provided by + * the PrintStream. This will basically wrap the + * getOutputStream with a buffer size of zero. + *

+ * The retrieved PrintStream uses the charset used to + * describe the content, with the Content-Type header. This will + * check the charset parameter of the contents MIME type. So if + * the Content-Type was text/plain; charset=UTF-8 the + * resulting PrintStream would encode the written data + * using the UTF-8 encoding scheme. Care must be taken to ensure + * that bytes written to the stream are correctly encoded. + * + * @return a print stream object used to write the message body + */ + public PrintStream getPrintStream() throws IOException { + return getPrintStream(0, getCharset()); + } + + /** + * This method is provided for convenience so that the HTTP content + * can be written using the print methods provided by + * the PrintStream. This will basically wrap the + * getOutputStream with a specified buffer size. + *

+ * The retrieved PrintStream uses the charset used to + * describe the content, with the Content-Type header. This will + * check the charset parameter of the contents MIME type. So if + * the Content-Type was text/plain; charset=UTF-8 the + * resulting PrintStream would encode the written data + * using the UTF-8 encoding scheme. Care must be taken to ensure + * that bytes written to the stream are correctly encoded. + * + * @param size the minimum size that the response buffer must be + * + * @return a print stream object used to write the message body + */ + public PrintStream getPrintStream(int size) throws IOException { + return getPrintStream(size, getCharset()); + } + + /** + * This is used to wrap the getOutputStream object in + * a PrintStream, which will write content using a + * specified charset. The PrintStream created will not + * buffer the content, it will write directly to the underlying + * OutputStream where it is buffered (if there is a + * buffer size greater than zero specified). In future the buffer + * of the PrintStream may be usable. + * + * @param size the minimum size that the response buffer must be + * @param charset this is the charset used by the resulting stream + * + * @return a print stream that encodes in the given charset + */ + private PrintStream getPrintStream(int size, String charset) throws IOException { + if(size > 0) { + buffer.expand(size); + } + return new PrintStream(buffer, false, charset); + } + + /** + * Used to write a message body with the Response. The + * semantics of this WritableByteChannel are determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + * + * @return a writable byte channel used to write the message body + */ + public WritableByteChannel getByteChannel() throws IOException { + return buffer; + } + + /** + * Used to write a message body with the Response. The + * semantics of this WritableByteChannel are determined + * by the HTTP version of the client, and whether or not the content + * length has been set, through the setContentLength + * method. If the length of the output is not known then the output + * is chunked for HTTP/1.1 clients and closed for HTTP/1.0 clients. + *

+ * This will ensure that there is buffering done so that the output + * can be reset using the reset method. This will + * enable the specified number of bytes to be written without + * committing the response. This specified size is the minimum size + * that the response buffer must be. + * + * @param size the minimum size that the response buffer must be + * + * @return a writable byte channel used to write the message body + */ + public WritableByteChannel getByteChannel(int size) throws IOException { + if(size > 0) { + buffer.expand(size); + } + return buffer; + } + + /** + * This is used to determine if the HTTP response message is a + * keep alive message or if the underlying socket was closed. Even + * if the client requests a connection keep alive and supports + * persistent connections, the response can still be closed by + * the server. This can be explicitly indicated by the presence + * of the Connection HTTP header, it can also be + * implicitly indicated by using version HTTP/1.0. + * + * @return this returns true if the connection was closed + */ + public boolean isKeepAlive() { + return support.isKeepAlive(); + } + + /** + * This can be used to determine whether the Response + * has been committed. This is true if the Response + * was committed, either due to an explicit invocation of the + * commit method or due to the writing of content. If + * the Response has committed the reset + * method will not work in resetting content already written. + * + * @return true if the response has been fully committed + */ + public boolean isCommitted() { + return observer.isCommitted(); + } + + /** + * This is used to write the headers that where given to the + * Response. Any further attempts to give headers + * to the Response will be futile as only the headers + * that were given at the time of the first commit will be used + * in the message header. + *

+ * This also performs some final checks on the headers submitted. + * This is done to determine the optimal performance of the + * output. If no specific Connection header has been specified + * this will set the connection so that HTTP/1.0 closes by default. + * + * @exception IOException thrown if there was a problem writing + */ + public void commit() throws IOException { + if(!observer.isCommitted()) { + String header = toString(); + byte[] message = header.getBytes("UTF-8"); + + trace.trace(WRITE_HEADER, header); + sender.write(message); + observer.commit(sender); + } + } + + /** + * This can be used to determine whether the Response + * has been committed. This is true if the Response + * was committed, either due to an explicit invocation of the + * commit method or due to the writing of content. If + * the Response has committed the reset + * method will not work in resetting content already written. + * + * @throws IOException thrown if there is a problem resetting + */ + public void reset() throws IOException { + buffer.reset(); + } + + /** + * This is used to close the connection and commit the request. + * This provides the same semantics as closing the output stream + * and ensures that the HTTP response is committed. This will + * throw an exception if the response can not be committed. + * + * @throws IOException thrown if there is a problem writing + */ + public void close() throws IOException { + buffer.close(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseException.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseException.java new file mode 100644 index 0000000..a02fe78 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseException.java @@ -0,0 +1,58 @@ +/* + * ResponseException.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import java.io.IOException; + +/** + * The ResponseException object is used to represent an + * exception that is thrown when there is a problem producing the + * response body. This can be used to wrap IOException + * objects that are thrown from the underlying transport. + * + * @author Niall Gallagher + */ +class ResponseException extends IOException { + + /** + * Constructor for the ResponseException object. This + * is used to represent an exception that is thrown when producing + * the response body. The producer exception is an I/O exception + * and thus exceptions can propagate out of stream methods. + * + * @param message this is the message describing the exception + */ + public ResponseException(String message) { + super(message); + } + + /** + * Constructor for the ResponseException object. This + * is used to represent an exception that is thrown when producing + * the response body. The producer exception is an I/O exception + * and thus exceptions can propagate out of stream methods. + * + * @param message this is the message describing the exception + * @param cause this is the cause of the producer exception + */ + public ResponseException(String message, Throwable cause) { + super(message); + initCause(cause); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseMessage.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseMessage.java new file mode 100644 index 0000000..4dcfb08 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseMessage.java @@ -0,0 +1,283 @@ +/* + * ResponseMessage.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static org.simpleframework.http.Protocol.CONTENT_LENGTH; +import static org.simpleframework.http.Protocol.CONTENT_TYPE; +import static org.simpleframework.http.Protocol.SET_COOKIE; +import static org.simpleframework.http.Protocol.TRANSFER_ENCODING; + +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.ResponseHeader; +import org.simpleframework.http.Status; +import org.simpleframework.http.message.MessageHeader; +import org.simpleframework.http.parse.ContentTypeParser; + +/** + * The ResponseMessage object represents the header used + * for a response. This is used to get and set the headers in a case + * insensitive manner. It is also used to manage the cookies that are + * send and received. Also, the status code and description can also + * be set through this object as well as the protocol version. + * + * @author Niall Gallagher + */ +class ResponseMessage extends MessageHeader implements ResponseHeader { + + /** + * This is the text description used for the response status. + */ + private String text; + + /** + * This is the major protocol version used for the response. + */ + private int major; + + /** + * This is the minor protocol version used for the response. + */ + private int minor; + + /** + * This is the status code used to identify the response type. + */ + private int code; + + /** + * Constructor for the ResponseMessage object. This + * is used to create a response message with a default status code + * of 200 and a a protocol version of HTTP/1.1. If the response is + * a different status code or version these can be modified. + */ + public ResponseMessage() { + this.text = "OK"; + this.code = 200; + this.major = 1; + this.minor = 1; + } + + /** + * This represents the status code of the HTTP response. + * The response code represents the type of message that is + * being sent to the client. For a description of the codes + * see RFC 2616 section 10, Status Code Definitions. + * + * @return the status code that this HTTP response has + */ + public int getCode() { + return code; + } + + /** + * This method allows the status for the response to be + * changed. This MUST be reflected the the response content + * given to the client. For a description of the codes see + * RFC 2616 section 10, Status Code Definitions. + * + * @param code the new status code for the HTTP response + */ + public void setCode(int code) { + this.code = code; + } + + /** + * This can be used to retrieve the text of a HTTP status + * line. This is the text description for the status code. + * This should match the status code specified by the RFC. + * + * @return the message description of the response + */ + public String getDescription() { + return text; + } + + /** + * This is used to set the text of the HTTP status line. + * This should match the status code specified by the RFC. + * + * @param text the descriptive text message of the status + */ + public void setDescription(String text) { + this.text = text; + } + + /** + * This is used to acquire the status from the response. + * The Status object returns represents the + * code that has been set on the response, it does not + * necessarily represent the description in the response. + * + * @return this is the response for this status line + */ + public Status getStatus() { + return Status.getStatus(code); + } + + /** + * This is used to set the status code and description + * for this response. Setting the code and description in + * this manner provides a much more convenient way to set + * the response status line details. + * + * @param status this is the status to set on the response + */ + public void setStatus(Status status) { + setCode(status.code); + setDescription(status.description); + } + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major type that is the 1 + * of a HTTP/1.0 version string. + * + * @return the major version number for the request message + */ + public int getMajor() { + return major; + } + + /** + * This can be used to set the major number from a HTTP version. + * The major version corresponds to the major type that is the 1 + * of a HTTP/1.0 version string. + * + * @param major the major version number for the request message + */ + public void setMajor(int major) { + this.major = major; + } + + /** + * This can be used to get the minor number from a HTTP version. + * The minor version corresponds to the major type that is the 0 + * of a HTTP/1.0 version string. This is used to determine if + * the request message has keep alive semantics. + * + * @return the minor version number for the request message + */ + public int getMinor() { + return minor; + } + + /** + * This can be used to get the minor number from a HTTP version. + * The minor version corresponds to the major type that is the 0 + * of a HTTP/1.0 version string. This is used to determine if + * the request message has keep alive semantics. + * + * @param minor the minor version number for the request message + */ + public void setMinor(int minor) { + this.minor = minor; + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + public ContentType getContentType() { + String value = getValue(CONTENT_TYPE); + + if(value == null) { + return null; + } + return new ContentTypeParser(value); + } + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return content length, or -1 if it cannot be determined + */ + public long getContentLength() { + return getLong(CONTENT_LENGTH); + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Transfer-Encoding header, if there is + * then this will parse that header and return the first token in + * the comma separated list of values, which is the primary value. + * + * @return this returns the transfer encoding value if it exists + */ + public String getTransferEncoding() { + return getValue(TRANSFER_ENCODING); + } + + /** + * This is used to compose the HTTP response header. All of the + * headers added to the response are added, as well as the cookies + * to form the response message header. To ensure that the text + * produces is as required the header names are in the same case + * as they were added to the response message. + * + * @return a string representation of the response message + */ + public CharSequence getHeader() { + return toString(); + } + + /** + * This is used to compose the HTTP response header. All of the + * headers added to the response are added, as well as the cookies + * to form the response message header. To ensure that the text + * produces is as required the header names are in the same case + * as they were added to the response message. + * + * @return a string representation of the response message + */ + public String toString() { + StringBuilder head = new StringBuilder(256); + + head.append("HTTP/").append(major); + head.append('.').append(minor); + head.append(' ').append(code); + head.append(' ').append(text); + head.append("\r\n"); + + for(String name : getNames()) { + for(String value : getAll(name)) { + head.append(name); + head.append(": "); + head.append(value); + head.append("\r\n"); + } + } + for(Cookie cookie : getCookies()) { + head.append(SET_COOKIE); + head.append(": "); + head.append(cookie); + head.append("\r\n"); + } + return head.append("\r\n").toString(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseObserver.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseObserver.java new file mode 100644 index 0000000..4a4b2c7 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseObserver.java @@ -0,0 +1,238 @@ +/* + * ResponseObserver.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.simpleframework.http.core.ContainerEvent.ERROR; +import static org.simpleframework.http.core.ContainerEvent.RESPONSE_FINISHED; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.simpleframework.http.message.Entity; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +/** + * The ResponseObserver is used to observe the response + * streams. If there is an error or a close requested this will + * close the underlying transport. If however there is a successful + * response then this will flush the transport and hand the channel + * for the pipeline back to the server kernel. This ensures that + * the next HTTP request can be consumed from the transport. + * + * @author Niall Gallagher + */ +class ResponseObserver implements BodyObserver { + + /** + * This is used to determine if the response has committed. + */ + private AtomicBoolean committed; + + /** + * This flag determines whether the connection was closed. + */ + private AtomicBoolean closed; + + /** + * This flag determines whether the was a response error. + */ + private AtomicBoolean error; + + /** + * This is the controller used to initiate a new request. + */ + private Controller controller; + + /** + * This is the channel associated with the client connection. + */ + private Channel channel; + + /** + * This is the trace used to observe the state of the stream. + */ + private Trace trace; + + /** + * This represents a time stamp that records the finish time. + */ + private Timer timer; + + /** + * Constructor for the ResponseObserver object. This + * is used to create an observer using a HTTP request entity and an + * initiator which is used to reprocess a channel if there was a + * successful deliver of a response. + * + * @param controller the controller used to process channels + * @param entity this is the entity associated with the channel + */ + public ResponseObserver(Controller controller, Entity entity) { + this.timer = new Timer(MILLISECONDS); + this.committed = new AtomicBoolean(); + this.closed = new AtomicBoolean(); + this.error = new AtomicBoolean(); + this.channel = entity.getChannel(); + this.trace = channel.getTrace(); + this.controller = controller; + } + + /** + * This is used to close the underlying transport. A closure is + * typically done when the response is to a HTTP/1.0 client + * that does not require a keep alive connection. Also, if the + * container requests an explicit closure this is used when all + * of the content for the response has been sent. + * + * @param writer this is the writer used to send the response + */ + public void close(ByteWriter writer) { + try { + if(!isClosed()) { + closed.set(true); + timer.set(); + trace.trace(RESPONSE_FINISHED); + writer.close(); + } + } catch(Exception cause) { + trace.trace(ERROR, cause); + fail(writer); + } + } + + /** + * This is used when there is an error sending the response. On + * error RFC 2616 suggests a connection closure is the best + * means to handle the condition, and the one clients should be + * expecting and support. All errors result in closure of the + * underlying transport and no more requests are processed. + * + * @param writer this is the writer used to send the response + */ + public void error(ByteWriter writer) { + try { + if(!isClosed()) { + error.set(true); + timer.set(); + trace.trace(RESPONSE_FINISHED); + writer.close(); + } + } catch(Exception cause) { + trace.trace(ERROR, cause); + fail(writer); + } + } + + /** + * This is used when the response has been sent correctly and + * the connection supports persisted HTTP. When ready the channel + * is handed back in to the server kernel where the next request + * on the pipeline is read and used to compose the next entity. + * + * @param writer this is the writer used to send the response + */ + public void ready(ByteWriter writer) { + try { + if(!isClosed()) { + closed.set(true); + writer.flush(); + timer.set(); + trace.trace(RESPONSE_FINISHED); + controller.start(channel); + } + } catch(Exception cause) { + trace.trace(ERROR, cause); + fail(writer); + } + } + + /** + * This is used to purge the writer so that it closes the socket + * ensuring there is no connection leak on shutdown. This is used + * when there is an exception signalling the state of the writer. + * + * @param writer this is the writer that is to be purged + */ + private void fail(ByteWriter writer) { + try { + writer.close(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } + + /** + * This is used to notify the observer that the HTTP response is + * committed and that the header can no longer be changed. It + * is also used to indicate whether the response can be reset. + * + * @param writer this is the writer used to send the response + */ + public void commit(ByteWriter writer) { + committed.set(true); + } + + /** + * This can be used to determine whether the response has been + * committed. If the response is committed then the header can + * no longer be manipulated and the response has been partially + * send to the client. + * + * @return true if the response headers have been committed + */ + public boolean isCommitted() { + return committed.get(); + } + + /** + * This is used to determine if the response has completed or + * if there has been an error. This basically allows the writer + * of the response to take action on certain I/O events. + * + * @return this returns true if there was an error or close + */ + public boolean isClosed() { + return closed.get() || error.get(); + } + + /** + * This is used to determine if the response was in error. If + * the response was in error this allows the writer to throw an + * exception indicating that there was a problem responding. + * + * @return this returns true if there was a response error + */ + public boolean isError(){ + return error.get(); + } + + /** + * This represents the time at which the response was either + * ready, closed or in error. Providing a time here is useful + * as it allows the time taken to generate a response to be + * determined even if the response is written asynchronously. + * + * @return the time when the response completed or failed + */ + public long getTime() { + return timer.get(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/core/Timer.java b/simple/simple-http/src/main/java/org/simpleframework/http/core/Timer.java new file mode 100644 index 0000000..c07d49b --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/core/Timer.java @@ -0,0 +1,94 @@ +/* + * Timer.java November 2012 + * + * Copyright (C) 2012, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.core; + +import static java.lang.System.currentTimeMillis; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.concurrent.TimeUnit; + +/** + * The Timer object is used to set the time a specific + * event occurred at. The time can be set only once from that point + * on all attempts to set the time are ignored. This makes this + * timer useful when there is a desire to record when a certain + * scenario was first encountered, for example when a request is + * first read from the underlying transport. + * + * @author Niall Gallagher + */ +class Timer { + + /** + * This is the time unit that this timer provides the time in. + */ + private TimeUnit unit; + + /** + * This is the time in milliseconds used to record the event. + */ + private volatile long time; + + /** + * Constructor for the Timer object. This is used + * to record when a specific event occurs. The provided time + * unit is used to determine how the time is retrieved. + * + * @param unit this time unit this timer will be using + */ + public Timer(TimeUnit unit) { + this.unit = unit; + this.time = -1L; + } + + /** + * This is used to determine if the timer has been set. If + * the set method has been called on this instance + * before then this will return true, otherwise false. + * + * @return this returns true if the timer has been set + */ + public boolean isSet() { + return time > 0; + } + + /** + * This is used to set the time for a specific event. Invoking + * this method multiple times will have no effect as the time + * is set for the first invocation only. Setting the time in + * this manner enables start times to be recorded effectively. + */ + public void set() { + if(time < 0) { + time = currentTimeMillis(); + } + } + + /** + * This is used to get the time for a specific event. The time + * returned by this method is given in the time unit specified + * on construction of the instance. + * + * @return this returns the time recorded by the timer + */ + public long get() { + return unit.convert(time, MILLISECONDS); + } +} + \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/ArrayConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/ArrayConsumer.java new file mode 100644 index 0000000..ef5db66 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/ArrayConsumer.java @@ -0,0 +1,184 @@ +/* + * ArrayConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.transport.ByteCursor; + +/** + * The ArrayConsumer object is a consumer that consumes + * bytes in to an internal array before processing. This consumes + * all bytes read in to an internal array. Each read is met with an + * invocation of the scan method, which searches for + * the terminal token within the read chunk. Once the terminal token + * has been read the excess bytes are reset and the data can be + * processed by the subclass implementation. The internal array is + * expanded if the number of consumed bytes exceeds its capacity. + * + * @author Niall Gallagher + */ +public abstract class ArrayConsumer implements ByteConsumer { + + /** + * This is the array that is used to contain the read bytes. + */ + protected byte[] array; + + /** + * This is the number of bytes that have been consumed so far. + */ + protected int count; + + /** + * This is the size of the chunk of bytes to read each time. + */ + protected int chunk; + + /** + * This determines whether the terminal token has been read. + */ + protected boolean done; + + /** + * Constructor for the ArrayConsumer object. This is + * used to create a consumer that will consume all bytes in to an + * internal array until a terminal token has been read. If excess + * bytes are read by this consumer they are reset in the cursor. + */ + public ArrayConsumer() { + this(1024); + } + + /** + * Constructor for the ArrayConsumer object. This is + * used to create a consumer that will consume all bytes in to an + * internal array until a terminal token has been read. If excess + * bytes are read by this consumer they are reset in the cursor. + * + * @param size this is the initial array and chunk size to use + */ + public ArrayConsumer(int size) { + this(size, 512); + } + + /** + * Constructor for the ArrayConsumer object. This is + * used to create a consumer that will consume all bytes in to an + * internal array until a terminal token has been read. If excess + * bytes are read by this consumer they are reset in the cursor. + * + * @param size this is the initial array size that is to be used + * @param chunk this is the chunk size to read bytes as + */ + public ArrayConsumer(int size, int chunk) { + this.array = new byte[size]; + this.chunk = chunk; + } + + /** + * This method is used to consume bytes from the provided cursor. + * Each read performed is done in a specific chunk size to ensure + * that a sufficiently large or small amount of data is read from + * the ByteCursor object. After each read the byte + * array is scanned for the terminal token. When the terminal + * token is found the bytes are processed by the implementation. + * + * @param cursor this is the cursor to consume the bytes from + */ + public void consume(ByteCursor cursor) throws IOException { + if(!done) { + int ready = cursor.ready(); + + while(ready > 0) { + int size = Math.min(ready, chunk); + + if(count + size > array.length) { + resize(count + size); + } + size = cursor.read(array, count, size); + count += size; + + if(size > 0) { + int reset = scan(); + + if(reset > 0) { + cursor.reset(reset); + } + if(done) { + process(); + break; + } + } + ready = cursor.ready(); + } + } + } + + /** + * This method is used to add an additional chunk size to the + * internal array. Resizing of the internal array is required as + * the consumed bytes may exceed the initial size of the array. + * In such a scenario the array is expanded the chunk size. + * + * @param size this is the minimum size to expand the array to + */ + protected void resize(int size) throws IOException { + if(array.length < size) { + int expand = array.length + chunk; + int max = Math.max(expand, size); + byte[] temp = new byte[max]; + + System.arraycopy(array, 0, temp, 0, count); + array = temp; + } + } + + /** + * When the terminal token is read from the cursor this will be + * true. The scan method is used to determine the + * terminal token. It is invoked after each read, when the scan + * method returns a non-zero value then excess bytes are reset + * and the consumer has finished. + * + * @return this returns true when the terminal token is read + */ + public boolean isFinished() { + return done; + } + + /** + * This method is invoked after the terminal token has been read. + * It is used to process the consumed data and is typically used to + * parse the input such that it can be used by the subclass for + * some useful purpose. This is called only once by the consumer. + */ + protected abstract void process() throws IOException; + + /** + * This method is used to scan for the terminal token. It searches + * for the token and returns the number of bytes in the buffer + * after the terminal token. Returning the excess bytes allows the + * consumer to reset the bytes within the consumer object. + * + * @return this returns the number of excess bytes consumed + */ + protected abstract int scan() throws IOException; + +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/Body.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/Body.java new file mode 100644 index 0000000..6b3599a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/Body.java @@ -0,0 +1,95 @@ +/* + * Body.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.simpleframework.http.Part; + +/** + * The Body interface is used to represent the body of + * a HTTP entity. It contains the information that is delivered with + * the request. The body is represented by a stream of bytes. In + * order to access the entity body this interface provides a stream + * which can be used to read it. Also, should the message be encoded + * as a multipart message the individual parts can be read using the + * Attachment instance for it. + * + * @author Niall Gallagher + */ +public interface Body { + + /** + * This will acquire the contents of the body in UTF-8. If there + * is no content encoding and the user of the request wants to + * deal with the body as a string then this method can be used. + * It will simply create a UTF-8 string using the body bytes. + * + * @return returns a UTF-8 string representation of the body + */ + String getContent() throws IOException; + + /** + * This will acquire the contents of the body in the specified + * charset. Typically this will be given the charset as taken + * from the HTTP Content-Type header. Although any encoding can + * be specified to convert the body to a string representation. + * + * @return returns an encoded string representation of the body + */ + String getContent(String charset) throws IOException; + + /** + * This is used to acquire the contents of the body as a stream. + * Each time this method is invoked a new stream is created that + * will read the contents of the body from the first byte. This + * ensures that the stream can be acquired several times without + * any issues arising from previous reads. + * + * @return this returns a new string used to read the body + */ + InputStream getInputStream() throws IOException; + + /** + * This method is used to acquire a Part from the + * HTTP request using a known name for the part. This is typically + * used when there is a file upload with a multipart POST request. + * All parts that are not files can be acquired as string values + * from the attachment object. + * + * @param name this is the name of the part object to acquire + * + * @return the named part or null if the part does not exist + */ + Part getPart(String name); + + /** + * This method is used to get all Part objects that + * are associated with the request. Each attachment contains the + * body and headers associated with it. If the request is not a + * multipart POST request then this will return an empty list. + * + * @return the list of parts associated with this request + */ + List getParts(); +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/BodyConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/BodyConsumer.java new file mode 100644 index 0000000..d84d6ed --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/BodyConsumer.java @@ -0,0 +1,43 @@ +/* + * BodyConsumer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +/** + * The BodyConsumer is used to consume the body of an + * HTTP message. Implementations of this consumer must provide the + * Body that has been consumed. If there is no body + * associated with the consumer then an empty body is returned. + * + * @author Niall Gallagher + */ +public interface BodyConsumer extends ByteConsumer { + + /** + * This is used to acquire the body that has been consumed. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Part objects. + * Each part can then be read as an individual message. + * + * @return the body that has been consumed by this instance + */ + Body getBody(); +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/BoundaryConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/BoundaryConsumer.java new file mode 100644 index 0000000..f519ce2 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/BoundaryConsumer.java @@ -0,0 +1,206 @@ +/* + * BoundaryConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.Buffer; + +/** + * The BoundaryConsumer is used to consume a boundary + * for a multipart message. This ensures that the boundary complies + * with the multipart specification in that it ends with a carriage + * return and line feed. This consumer implementation can be used + * multiple times as its internal buffer can be cleared and reset. + * + * @author Niall Gallagher + */ +class BoundaryConsumer extends ArrayConsumer { + + /** + * This is the terminal token for a multipart boundary entity. + */ + private static final byte[] LAST = { '-', '-', '\r', '\n', }; + + /** + * This is the terminal token for a multipart boundary line. + */ + private static final byte[] LINE = { '\r', '\n' }; + + /** + * This represents the start of the boundary line for the part. + */ + private static final byte[] TOKEN = { '-', '-' }; + + /** + * This is used to allocate a buffer for for the boundary. + */ + private Allocator allocator; + + /** + * This is used to consume the contents of the consumed buffer. + */ + private Buffer buffer; + + /** + * This is the actual boundary value that is to be consumed. + */ + private byte[] boundary; + + /** + * This counts the number of characters read from the start. + */ + private int seek; + + /** + * Constructor for the BoundaryConsumer object. This + * is used to create a boundary consumer for validating boundaries + * and consuming them from a provided source. This is used to help + * in reading multipart messages by removing boundaries from the + * stream. + * + * @param boundary this is the boundary value to be consumed + */ + public BoundaryConsumer(Allocator allocator, byte[] boundary) { + this.chunk = boundary.length + LAST.length + TOKEN.length; + this.allocator = allocator; + this.boundary = boundary; + } + + /** + * This does not perform any processing after the boundary has + * been consumed. Because the boundary consumer is used only as a + * means to remove the boundary from the underlying stream there + * is no need to perform any processing of the value consumed. + */ + @Override + protected void process() throws IOException { + if(count < boundary.length + 4) { + throw new IOException("Invalid boundary processed"); + } + } + + /** + * This method is used to scan for the terminal token. It searches + * for the token and returns the number of bytes in the buffer + * after the terminal token. Returning the excess bytes allows the + * consumer to reset the bytes within the consumer object. + * + * @return this returns the number of excess bytes consumed + */ + @Override + protected int scan() throws IOException { + int size = boundary.length; + + if(count >= 2 && seek < 2) { + if(scan(TOKEN)) { + append(TOKEN); + } + } + if(count >= 2 + size && seek < 2 + size) { + if(scan(boundary)) { + append(boundary); + } + } + if(count >= 4 + size && seek < 4 + size) { + if(array[size + 2] == TOKEN[0]) { + if(scan(TOKEN)) { + append(TOKEN); + } + } else if(array[size + 2] == LINE[0]) { + if(scan(LINE)) { + append(LINE); + } + done = true; + return count - seek; + } + } + if(count >= 6 + size && seek < 6 + size) { + if(scan(LINE)) { + append(LINE); + } + done = true; + return count - seek; + } + return 0; + } + + /** + * This is used to append a token to the underlying buffer. Adding + * various tokens ensures that the whole message is reconstructed + * and can be forwarded to any connected service if used as a proxy. + * + * @param token this is the token that is to be appended + */ + private void append(byte[] token) throws IOException { + if(buffer == null) { + buffer = allocator.allocate(chunk); + } + buffer.append(token); + } + + /** + * This is used to scan the specified token from the consumed bytes. + * If the data scanned does not match the token provided then this + * will throw an exception to signify a bad boundary. This will + * return true only when the whole boundary has been consumed. + * + * @param data this is the token to scan from the consumed bytes + * + * @return this returns true of the token has been read + */ + private boolean scan(byte[] data) throws IOException { + int size = data.length; + int pos = 0; + + while(seek < count) { + if(array[seek++] != data[pos++]) { + throw new IOException("Invalid boundary"); + } + if(pos == data.length) { + return true; + } + } + return pos == size; + } + + /** + * This is used to determine whether the boundary has been read + * from the underlying stream. This is true only when the very + * last boundary has been read. This will be the boundary value + * that ends with the two - characters. + * + * @return this returns true with the terminal boundary is read + */ + public boolean isEnd() { + return seek == chunk; + } + + /** + * This is used to clear the state of the of boundary consumer + * such that it can be reused. This is required as the multipart + * body may contain many parts, all delimited with the same + * boundary. Clearing allows the next boundary to be consumed. + */ + public void clear() { + done = false; + count = seek = 0; + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/BufferBody.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/BufferBody.java new file mode 100644 index 0000000..e0e8a75 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/BufferBody.java @@ -0,0 +1,166 @@ +/* + * BufferBody.java February 2012 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; + +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.http.Part; + +/** + * The Body interface is used to represent the body of + * a HTTP entity. It contains the information that is delivered with + * the request. The body is represented by a stream of bytes. In + * order to access the entity body this interface provides a stream + * which can be used to read it. Also, should the message be encoded + * as a multipart message the individual parts can be read using the + * Attachment instance for it. + * + * @author Niall Gallagher + */ +class BufferBody implements Body { + + /** + * This is used to hold the attachments for the HTTP body. + */ + private final PartSeries series; + + /** + * This is usd to hold the bytes representing the HTTP body. + */ + private final Buffer buffer; + + /** + * Constructor for the BufferBody object. This is + * used to create a body that represents a HTTP payload. The + * body enables the payload to be either read in a stream or + * as an encoded string. Also the attachments are available. + */ + public BufferBody() { + this(null); + } + + /** + * Constructor for the BufferBody object. This is + * used to create a body that represents a HTTP payload. The + * body enables the payload to be either read in a stream or + * as an encoded string. Also the attachments are available. + * + * @param buffer this is the buffer representing the body + */ + public BufferBody(Buffer buffer) { + this(buffer, null); + } + + /** + * Constructor for the BufferBody object. This is + * used to create a body that represents a HTTP payload. The + * body enables the payload to be either read in a stream or + * as an encoded string. Also the attachments are available. + * + * @param buffer this is the buffer representing the body + * @param series this is the list of parts for this body + */ + public BufferBody(Buffer buffer, PartSeries series) { + this.buffer = buffer; + this.series = series; + } + + /** + * This method is used to acquire a Part from the + * HTTP request using a known name for the part. This is typically + * used when there is a file upload with a multipart POST request. + * All parts that are not files can be acquired as string values + * from the attachment object. + * + * @param name this is the name of the part object to acquire + * + * @return the named part or null if the part does not exist + */ + public Part getPart(String name) { + if(series != null) { + return series.getPart(name); + } + return null; + } + + /** + * This method is used to get all Part objects that + * are associated with the request. Each attachment contains the + * body and headers associated with it. If the request is not a + * multipart POST request then this will return an empty list. + * + * @return the list of parts associated with this request + */ + public List getParts() { + if(series != null) { + return series.getParts(); + } + return Collections.emptyList(); + } + + /** + * This will acquire the contents of the body in UTF-8. If there + * is no content encoding and the user of the request wants to + * deal with the body as a string then this method can be used. + * It will simply create a UTF-8 string using the body bytes. + * + * @return returns a UTF-8 string representation of the body + */ + public String getContent() throws IOException { + if(buffer == null) { + return new String(); + } + return buffer.encode(); + } + + /** + * This will acquire the contents of the body in the specified + * charset. Typically this will be given the charset as taken + * from the HTTP Content-Type header. Although any encoding can + * be specified to convert the body to a string representation. + * + * @return returns an encoded string representation of the body + */ + public String getContent(String charset) throws IOException { + if(buffer == null) { + return new String(); + } + return buffer.encode(charset); + } + + /** + * This is used to acquire the contents of the body as a stream. + * Each time this method is invoked a new stream is created that + * will read the contents of the body from the first byte. This + * ensures that the stream can be acquired several times without + * any issues arising from previous reads. + * + * @return this returns a new string used to read the body + */ + public InputStream getInputStream() throws IOException { + if(buffer == null) { + return new EmptyInputStream(); + } + return buffer.open(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/BufferPart.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/BufferPart.java new file mode 100644 index 0000000..73a5ea0 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/BufferPart.java @@ -0,0 +1,160 @@ +/* + * BufferPart.java February 2012 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; +import java.io.InputStream; + +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.http.ContentDisposition; +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Part; + +/** + * The BufferPart is used to represent a part within + * a request message. Typically a part represents either a text + * parameter or a file, with associated headers. The contents of + * the part can be acquire as an InputStream or as a + * string encoded in the default HTTP encoding ISO-8859-1 or in + * the encoding specified with the Content-Type header. + * + * @author Niall Gallagher + */ +class BufferPart implements Part { + + /** + * This is the segment representing the headers for the part. + */ + private final Segment segment; + + /** + * This is the body that forms the payload for the part. + */ + private final Body body; + + /** + * Constructor for the BufferPart object. This is + * used to create a part from a multipart body. Each part will + * contain the headers associated with it as well as the body. + * + * @param segment this holds the headers for the part + * @param buffer this represents the body for the part + */ + public BufferPart(Segment segment, Buffer buffer) { + this.body = new BufferBody(buffer); + this.segment = segment; + } + + /** + * This method is used to determine the type of a part. Typically + * a part is either a text parameter or a file. If this is true + * then the content represented by the associated part is a file. + * + * @return this returns true if the associated part is a file + */ + public boolean isFile() { + return getDisposition().isFile(); + } + + /** + * This method is used to acquire the name of the part. Typically + * this is used when the part represents a text parameter rather + * than a file. However, this can also be used with a file part. + * + * @return this returns the name of the associated part + */ + public String getName() { + return getDisposition().getName(); + } + + /** + * This method is used to acquire the file name of the part. This + * is used when the part represents a text parameter rather than + * a file. However, this can also be used with a file part. + * + * @return this returns the file name of the associated part + */ + public String getFileName() { + return getDisposition().getFileName(); + } + + /** + * This is used to acquire the content of the part as a string. + * The encoding of the string is taken from the content type. + * If no content type is sent the content is decoded in the + * standard default of ISO-8859-1. + * + * @return this returns a string representing the content + * + * @throws IOException thrown if the content can not be created + */ + public String getContent() throws IOException { + return body.getContent(); + } + + /** + * This is used to acquire an InputStream for the + * part. Acquiring the stream allows the content of the part to + * be consumed by reading the stream. Each invocation of this + * method will produce a new stream starting from the first byte. + * + * @return this returns the stream for this part object + * + * @throws IOException thrown if the stream can not be created + */ + public InputStream getInputStream() throws IOException { + return body.getInputStream(); + } + + /** + * This is used to acquire the content type for this part. This + * is typically the type of content for a file part, as provided + * by a MIME type from the HTTP "Content-Type" header. + * + * @return this returns the content type for the part object + */ + public ContentType getContentType() { + return segment.getContentType(); + } + + /** + * This is used to acquire the content disposition for the part. + * The content disposition contains the Content-Disposition header + * details sent with the part in the multipart request body. + * + * @return value of the header mapped to the specified name + */ + public ContentDisposition getDisposition() { + return segment.getDisposition(); + } + + /** + * This is used to acquire the header value for the specified + * header name. Providing the header values through this method + * ensures any special processing for a know content type can be + * handled by an application. + * + * @param name the name of the header to get the value for + * + * @return value of the header mapped to the specified name + */ + public String getHeader(String name) { + return segment.getValue(name); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/ByteConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/ByteConsumer.java new file mode 100644 index 0000000..886434d --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/ByteConsumer.java @@ -0,0 +1,64 @@ +/* + * ByteConsumer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.transport.ByteCursor; + +/** + * The ByteConsumer object is used to consume and process + * bytes from a cursor. This is used to consume bytes from a pipeline + * and process the content in order to produce a valid HTTP message. + * Using a consumer allows the server to gather and process the data + * from the stream bit by bit without blocking. + *

+ * A consumer has completed its task when it has either exhausted its + * stream, or when it has consume a terminal token. For instance a + * consumer for a HTTP header will have two CRLF bytes + * tokens to identify the end of the header, once this has been read + * any excess bytes are reset on the cursor and it has finished. + * + * @author Niall Gallagher + * + * @see org.simpleframework.transport.ByteCursor + */ +public interface ByteConsumer { + + /** + * This method is used to consume bytes from the provided cursor. + * Consuming of bytes from the cursor should be done in such a + * way that it does not block. So typically only the number of + * ready bytes in the ByteCursor object should be + * read. If there are no ready bytes then this method return. + * + * @param cursor used to consume the bytes from the cursor + */ + void consume(ByteCursor cursor) throws IOException; + + /** + * This is used to determine whether the consumer has finished + * reading. The consumer is considered finished if it has read a + * terminal token or if it has exhausted the stream and can not + * read any more. Once finished the consumed bytes can be parsed. + * + * @return true if the consumer has finished reading its content + */ + boolean isFinished(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/ChunkedConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/ChunkedConsumer.java new file mode 100644 index 0000000..41549d6 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/ChunkedConsumer.java @@ -0,0 +1,258 @@ +/* + * ChunkedConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.Buffer; + +/** + * The ChunkedConsumer is reads an decodes a stream + * using the chunked transfer coding. This is used so that any data + * sent in the chunked transfer coding can be decoded. All bytes are + * appended to an internal buffer so that they can be read without + * having to parse the encoding. + *

+ *
+ *    length := 0
+ *    read chunk-size, chunk-extension (if any) and CRLF
+ *    while (chunk-size > 0) {
+ *       read chunk-data and CRLF
+ *       append chunk-data to entity-body
+ *       length := length + chunk-size
+ *       read chunk-size and CRLF
+ *    }
+ *    read entity-header
+ *    while (entity-header not empty) {
+ *       append entity-header to existing header fields
+ *       read entity-header
+ *    }
+ *
+ * 
+ * The above algorithm is taken from RFC 2616 section 19.4.6. This + * coding scheme is used in HTTP pipelines so that dynamic content, + * that is, content with which a length cannot be determined does + * not require a connection close to delimit the message body. + * + * @author Niall Gallagher + */ +public class ChunkedConsumer extends UpdateConsumer { + + /** + * This is used to create the internal buffer for the body. + */ + private Allocator allocator; + + /** + * This is the internal buffer used to capture the body read. + */ + private Buffer buffer; + + /** + * This is used to determine whether a full chunk has been read. + */ + private boolean terminal; + + /** + * This is used to determine if the zero length chunk was read. + */ + private boolean last; + + /** + * This is used to accumulate the bytes of the chunk size line. + */ + private byte line[]; + + /** + * This is the number of bytes appended to the line buffer. + */ + private int count; + + /** + * This is the number of bytes left in the current chunk. + */ + private int chunk; + + /** + * Constructor for the ChunkedConsumer object. This + * is used to create a consumer that reads chunked encoded data and + * appended that data in decoded form to an internal buffer so that + * it can be read in a clean decoded fromat. + * + * @param allocator this is used to allocate the internal buffer + */ + public ChunkedConsumer(Allocator allocator) { + this(allocator, 1024); + } + + /** + * Constructor for the ChunkedConsumer object. This + * is used to create a consumer that reads chunked encoded data and + * appended that data in decoded form to an internal buffer so that + * it can be read in a clean decoded fromat. + * + * @param allocator this is used to allocate the internal buffer + * @param chunk this is the maximum size line allowed + */ + private ChunkedConsumer(Allocator allocator, int chunk) { + this.line = new byte[chunk]; + this.allocator = allocator; + } + + /** + * This is used to acquire the body that has been consumed. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Attachment objects. + * Each part can then be read as an individual message. + * + * @return the body that has been consumed by this instance + */ + public Body getBody() { + return new BufferBody(buffer); + } + + /** + * This method is used to append the contents of the array to the + * internal buffer. The appended bytes can be acquired from the + * internal buffer using an InputStream, or the text + * of the appended bytes can be acquired by encoding the bytes. + * + * @param array this is the array of bytes to be appended + * @param off this is the start offset in the array to read from + * @param len this is the number of bytes to write to the buffer + */ + private void append(byte[] array, int off, int len) throws IOException { + if(buffer == null) { + buffer = allocator.allocate(); + } + buffer.append(array, off, len); + } + + /** + * This is used to process the bytes that have been read from the + * cursor. This will keep reading bytes from the stream until such + * time as the zero length chunk has been read from the stream. If + * the zero length chunk is encountered then the overflow count is + * returned so it can be used to reset the cursor. + * + * @param array this is a chunk read from the cursor + * @param off this is the offset within the array the chunk starts + * @param size this is the number of bytes within the array + * + * @return this returns the number of bytes overflow that is read + */ + @Override + protected int update(byte[] array, int off, int size) throws IOException { + int mark = off + size; + + while(off < mark){ + if(terminal || last) { + while(off < mark) { + if(array[off++] == '\n') { // CR[LF] + if(last) { // 0; CRLFCR[LF] + finished = true; + return mark - off; + } + terminal = false; + break; + } + } + } else if(chunk == 0) { + while(chunk == 0) { + if(off >= mark) { + break; + } else if(array[off++] == '\n') { // CR[LF] + parse(); + + if(chunk == 0) { // 0; CR[LF]CRLF + last = true; + break; + } + } else { + line[count++] = array[off-1]; + } + } + } else { + int write = Math.min(mark - off, chunk); + + append(array, off, write); + chunk -= write; + off += write; + + if(chunk == 0) { // []CRLF + terminal = true; + } + } + } + return 0; + } + + /** + * This method is used to convert the size in hexidecimal to a + * decimal int. This will use the specified number + * of bytes from the internal buffer and parse each character + * read as a hexidecimal character. This stops interpreting the + * size line when a non-hexidecimal character is encountered. + */ + private void parse() throws IOException { + int off = 0; + + while(off < count) { + int octet = toDecimal(line[off]); + + if(octet < 0){ + if(off < 1) { + throw new IOException("Invalid chunk size line"); + } + break; + } + chunk <<= 4; + chunk ^= octet; + off++; + } + count = 0; + } + + /** + * This performs a conversion from a character to an integer. If + * the character given, as a byte, is a hexidecimal + * char this will convert it into its integer equivelant. So a + * char of A is converted into 10. + * + * @param octet this is an ISO 8869-1 hexidecimal character + * + * @return returns the hex character into its decinal value + */ + private int toDecimal(byte octet){ + if(octet >= 'A' && octet <= 'Z') { + return (octet - 'A') + 10; + } + if(octet >= '0' && octet <= '9') { + return octet - '0'; + } + if(octet >= 'a' && octet <= 'f') { + return (octet - 'a') + 10; + } + return -1; + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/ConsumerFactory.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/ConsumerFactory.java new file mode 100644 index 0000000..b3e5dc0 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/ConsumerFactory.java @@ -0,0 +1,201 @@ +/* + * ConsumerFactory.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import static org.simpleframework.http.Protocol.BOUNDARY; +import static org.simpleframework.http.Protocol.CHUNKED; +import static org.simpleframework.http.Protocol.MULTIPART; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.http.ContentType; + +/** + * The ConsumerFactory object is used to create a factory + * for creating consumers. This allows the request to determine the + * type of content sent and allows consumption of the request body in + * a the manner specified by the HTTP header. This will allow multipart + * and chunked content to be consumed from the pipeline. + * + * @author Niall Gallagher + */ +class ConsumerFactory { + + /** + * This is used to allocate the memory associated with the body. + */ + protected Allocator allocator; + + /** + * This is the header associated with the request body consumed. + */ + protected Segment segment; + + /** + * Constructor for the ConsumerFactory object. This + * will create a factory that makes use of the HTTP header in order + * to determine the type of the body that is to be consumed. + * + * @param allocator this is the allocator used to allocate memory + * @param segment this is the HTTP header used to determine type + */ + public ConsumerFactory(Allocator allocator, Segment segment) { + this.allocator = allocator; + this.segment = segment; + } + + /** + * This method is used to create a body consumer to read the body + * from the pipeline. This will examine the HTTP header associated + * with the body to determine how to consume the data. This will + * provide an empty consumer if no specific delimiter was provided. + * + * @return this returns the consumer used to consume the body + */ + public BodyConsumer getInstance() { + long length = getContentLength(); + + if(length < 0) { + return getInstance(8192); + } + return getInstance(length); + } + + /** + * This method is used to create a body consumer to read the body + * from the pipeline. This will examine the HTTP header associated + * with the body to determine how to consume the data. This will + * provide an empty consumer if no specific delimiter was provided. + * + * @param length this is the length of the body to be consumed + * + * @return this returns the consumer used to consume the body + */ + public BodyConsumer getInstance(long length) { + byte[] boundary = getBoundary(segment); + + if(isUpload(segment)) { + return new FileUploadConsumer(allocator, boundary, length); + } + if(isChunked(segment)) { + return new ChunkedConsumer(allocator); + } + if(isFixed(segment)) { + return new FixedLengthConsumer(allocator, length); + } + return new EmptyConsumer(); + } + + /** + * This is used to extract information from the HTTP header that + * can be used to determine the type of the body. This will look + * at the HTTP headers provided to find a specific token which + * enables it to determine how to consume the body. + * + * @param header this is the header associated with the body + * + * @return the boundary for a multipart upload body + */ + protected byte[] getBoundary(Segment header) { + ContentType type = header.getContentType(); + + if(type != null) { + String token = type.getParameter(BOUNDARY); + + if(token != null) { + return token.getBytes(); + } + } + return null; + } + + /** + * This is used to extract information from the HTTP header that + * can be used to determine the type of the body. This will look + * at the HTTP headers provided to find a specific token which + * enables it to determine how to consume the body. + * + * @param segment this is the header associated with the body + * + * @return true if the content type is that of a multipart body + */ + protected boolean isUpload(Segment segment) { + ContentType type = segment.getContentType(); + + if(type != null) { + String token = type.getPrimary(); + + if(token.equals(MULTIPART)) { + return true; + } + } + return false; + } + + /** + * This is used to extract information from the HTTP header that + * can be used to determine the type of the body. This will look + * at the HTTP headers provided to find a specific token which + * enables it to determine how to consume the body. + * + * @param segment this is the header associated with the body + * + * @return true if the body is to be consumed as a chunked body + */ + protected boolean isChunked(Segment segment) { + String encoding = segment.getTransferEncoding(); + + if(encoding != null) { + if(encoding.equals(CHUNKED)) { + return true; + } + } + return false; + } + + /** + * This is used to extract information from the HTTP header that + * can be used to determine the type of the body. This will look + * at the HTTP headers provided to find a specific token which + * enables it to determine how to consume the body. + * + * @param segment this is the header associated with the body + * + * @return true if there was a content length in the header + */ + protected boolean isFixed(Segment segment) { + long length = segment.getContentLength(); + + if(length > 0) { + return true; + } + return false; + } + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return the content length, or -1 if it cannot be determined + */ + protected long getContentLength() { + return segment.getContentLength(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/ContentConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/ContentConsumer.java new file mode 100644 index 0000000..76742c2 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/ContentConsumer.java @@ -0,0 +1,226 @@ +/* + * ContentConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.http.Part; +import org.simpleframework.transport.ByteCursor; + +/** + * The ContentConsumer object represents a consumer for + * a multipart body part. This will read the contents of the cursor + * until such time as it reads the terminal boundary token, which is + * used to frame the content. Once the boundary token has been read + * this will add itself as a part to a part list. This part list can + * then be used with the HTTP request to examine and use the part. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.message.PartConsumer + */ +class ContentConsumer extends UpdateConsumer { + + /** + * This represents the start of the boundary token for the body. + */ + private static final byte[] START = { '\r', '\n', '-', '-' }; + + /** + * This is the part list that this part is to be added to. + */ + private PartSeries series; + + /** + * This is used to allocate the internal buffer when required. + */ + private Allocator allocator; + + /** + * Represents the HTTP headers that were provided for the part. + */ + private Segment segment; + + /** + * This is the internal buffer used to house the part body. + */ + private Buffer buffer; + + /** + * Represents the message boundary that terminates the part body. + */ + private byte[] boundary; + + /** + * This is used to determine if the start token had been read. + */ + private int start; + + /** + * This is used to determine how many boundary tokens are read. + */ + private int seek; + + /** + * Constructor for the ContentConsumer object. This + * is used to create a consumer that reads the body of a part in + * a multipart request body. The terminal token must be provided + * so that the end of the part body can be determined. + * + * @param allocator this is used to allocate the internal buffer + * @param segment this represents the headers for the part body + * @param series this is the part list that this body belongs in + * @param boundary this is the message boundary for the body part + */ + public ContentConsumer(Allocator allocator, Segment segment, PartSeries series, byte[] boundary) { + this.allocator = allocator; + this.boundary = boundary; + this.segment = segment; + this.series = series; + } + + /** + * This is used to acquire the body for this HTTP entity. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Part objects. Each + * part can then be read as an individual message. + * + * @return the body provided by the HTTP request message + */ + public Body getBody() { + return new BufferBody(buffer); + } + + /** + * This is used to acquire the part for this HTTP entity. This + * will return a part which can be used to read the content of + * the message, the part created contains the contents of the + * body and the headers associated with it. + * + * @return the part provided by the HTTP request message + */ + public Part getPart() { + return new BufferPart(segment, buffer); + } + + /** + * This method is used to append the contents of the array to the + * internal buffer. The appended bytes can be acquired from the + * internal buffer using an InputStream, or the text + * of the appended bytes can be acquired by encoding the bytes. + * + * @param array this is the array of bytes to be appended + * @param off this is the start offset in the array to read from + * @param len this is the number of bytes to write to the buffer + */ + private void append(byte[] array, int off, int len) throws IOException { + if(buffer == null) { + buffer = allocator.allocate(); + } + buffer.append(array, off, len); + } + + /** + * This is used to push the start and boundary back on to the + * cursor. Pushing the boundary back on to the cursor is required + * to ensure that the next consumer will have valid data to + * read from it. Simply resetting the boundary is not enough as + * this can cause an infinite loop if the connection is bad. + * + * @param cursor this is the cursor used by this consumer + */ + @Override + protected void commit(ByteCursor cursor) throws IOException { + cursor.push(boundary); + cursor.push(START); + } + + /** + * This is used to process the bytes that have been read from the + * cursor. This will search for the boundary token within the body + * of the message part, when it is found this will returns the + * number of bytes that represent the overflow. + * + * @param array this is a chunk read from the cursor + * @param off this is the offset within the array the chunk starts + * @param size this is the number of bytes within the array + * + * @return this returns the number of bytes overflow that is read + */ + @Override + protected int update(byte[] array, int off, int size) throws IOException { + int skip = start + seek; // did we skip previously + int last = off + size; + int next = start; + int mark = off; + + while(off < last) { + if(start == START.length) { // search for boundary + if(array[off++] != boundary[seek++]) { // boundary not found + if(skip > 0) { + append(START, 0, next); // write skipped start + append(boundary, 0, skip - next); // write skipped boundary + } + skip = start = seek = 0; // reset scan position + } + if(seek == boundary.length) { // boundary found + int excess = seek + start; // boundary bytes read + int total = off - mark; // total bytes read + int valid = total - excess; // body bytes read + + finished = true; + + if(valid > 0) { + append(array, mark, valid); + } + Part part = getPart(); + + if(part != null) { + series.addPart(part); + } + return size - total; // remaining excluding boundary + } + } else { + byte octet = array[off++]; // current + + if(octet != START[start++]) { + if(skip > 0) { + append(START, 0, next); // write skipped start + } + skip = start = 0; // reset + + if(octet == START[0]) { // is previous byte the start + start++; + } + } + } + } + int excess = seek + start; // boundary bytes read + int total = off - mark; // total bytes read + int valid = total - excess; // body bytes read + + if(valid > 0) { // can we append processed data + append(array, mark, valid); + } + return 0; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/ContinueDispatcher.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/ContinueDispatcher.java new file mode 100644 index 0000000..56f6472 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/ContinueDispatcher.java @@ -0,0 +1,88 @@ +/* + * ContinueDispatcher.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import static org.simpleframework.http.core.ContainerEvent.DISPATCH_CONTINUE; + +import java.io.IOException; + +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +/** + * The ContinueDispatcher object is used to send the HTTP + * 100 continue status if required. This is delivered to the client + * to tell the client that the server is willing to accept the request + * body. Once this is sent the transport will likely wait until there + * is a read ready event. + * + * @author Niall Gallagher + */ +class ContinueDispatcher { + + /** + * This is the status code that is sent to prompt the client. + */ + private static final byte[] STATUS = { 'H', 'T','T', 'P', '/','1','.', '1',' ', '1','0','0',' '}; + + /** + * This is the optional description for the expect status code. + */ + private static final byte[] MESSAGE = {'C','o','n','t','i','n','u','e', '\r','\n','\r','\n'}; + + /** + * This is the writer that is used to deliver the continue. + */ + private final ByteWriter writer; + + /** + * This is the trace used to capture a continue response if any. + */ + private final Trace trace; + + /** + * Constructor for the ContinueDispatcher object. This + * will create an object that will deliver the continue status code. + * Because the transport performs an asynchronous write this will + * not block the execution of this method and delay execution. + * + * @param channel this is the channel used to deliver the prompt + */ + public ContinueDispatcher(Channel channel) { + this.writer = channel.getWriter(); + this.trace = channel.getTrace(); + } + + /** + * This will execute the continue if the header contains the + * expectation header. If there is no expectation then this will + * return without sending anything back to the connected client. + * + * @param header this is the header read from the channel + */ + public void execute(Header header) throws IOException { + if(header.isExpectContinue()) { + trace.trace(DISPATCH_CONTINUE); + writer.write(STATUS); + writer.write(MESSAGE); + writer.flush(); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyConsumer.java new file mode 100644 index 0000000..9fb8145 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyConsumer.java @@ -0,0 +1,69 @@ +/* + * EmptyConsumer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import org.simpleframework.transport.ByteCursor; + +/** + * The EmptyConsumer object is used to represent a body + * of zero length. This is the most common body consumer created as + * it represents the body for GET messages that have nothing within + * the body part. + * + * @author Niall Gallagher + */ +public class EmptyConsumer implements BodyConsumer { + + /** + * This is used to acquire the body that has been consumed. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Attachment objects. + * Each part can then be read as an individual message. + * + * @return the body that has been consumed by this instance + */ + public Body getBody() { + return new BufferBody(); + } + + /** + * This method will not consume any bytes from the cursor. This + * ensures that the next byte read from the stream is the first + * character of the next HTTP message within the pipeline. + * + * @param cursor this is the cursor which will not be read from + */ + public void consume(ByteCursor cursor) { + return; + } + + /** + * This will return true immediately. Because the empty consumer + * represents a zero length body and no bytes are read from the + * cursor, this should not be processed and return finished. + * + * @return this will always return true for the zero length body + */ + public boolean isFinished() { + return true; + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyInputStream.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyInputStream.java new file mode 100644 index 0000000..2d1f9ff --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyInputStream.java @@ -0,0 +1,44 @@ +/* + * EmptyInputStream.java October 2002 + * + * Copyright (C) 2002, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.InputStream; + +/** + * The EmptyInputStream object provides a stream that + * is immediately empty. Each read method with this input stream + * will return a -1 value indicating that the stream has come to an + * end and no more data can be read from it. + * + * @author Niall Gallagher + */ +class EmptyInputStream extends InputStream { + + /** + * This is used to provide a -1 value when an attempt is made to + * read from the stream. Implementing this method as so also + * ensures that all the other read methods return a -1 value. + * + * @return this returns a -1 when an attempt is made to read + */ + public int read() { + return -1; + } + +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/Entity.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/Entity.java new file mode 100644 index 0000000..6668eeb --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/Entity.java @@ -0,0 +1,75 @@ +/* + * Entity.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import org.simpleframework.transport.Channel; + +/** + * The Entity object is used to represent the HTTP entity + * received from the client. The entity contains a header and body as + * well as the underlying Channel for the connection. If + * there is no body with the entity this will provide an empty body + * object which provides a zero length sequence of bytes. + * + * @author Niall Gallagher + */ +public interface Entity { + + /** + * This is the time in milliseconds when the request was first + * read from the underlying channel. The time represented here + * represents the time collection of this request began. This + * does not necessarily represent the time the bytes arrived on + * the receive buffers as some data may have been buffered. + * + * @return this represents the time the request was ready at + */ + long getTime(); + + /** + * This is used to acquire the body for this HTTP entity. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Part objects. Each + * part can then be read as an individual message. + * + * @return the body provided by the HTTP request message + */ + Body getBody(); + + /** + * This provides the HTTP request header for the entity. This is + * always populated and provides the details sent by the client + * such as the target URI and the query if specified. Also this + * can be used to determine the method and protocol version used. + * + * @return the header provided by the HTTP request message + */ + Header getHeader(); + + /** + * This provides the connected channel for the client. This is + * used to send and receive bytes to and from an transport layer. + * Each channel provided with an entity contains an attribute + * map which contains information about the connection. + * + * @return the connected channel for this HTTP entity + */ + Channel getChannel(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/EntityConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/EntityConsumer.java new file mode 100644 index 0000000..9cf12fb --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/EntityConsumer.java @@ -0,0 +1,184 @@ +/* + * EntityConsumer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import static org.simpleframework.http.core.ContainerEvent.BODY_FINISHED; +import static org.simpleframework.http.core.ContainerEvent.HEADER_FINISHED; +import static org.simpleframework.http.core.ContainerEvent.READ_BODY; +import static org.simpleframework.http.core.ContainerEvent.READ_HEADER; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.trace.Trace; + +/** + * The EntityConsumer object is used to consume data + * from a cursor and build a request entity. Each constituent part of + * the entity is consumed from the pipeline and can be acquired from + * this consumer object. The Header and Body + * can be used to extract the individual parts of the entity. + * + * @author Niall Gallagher + */ +public class EntityConsumer implements ByteConsumer { + + /** + * This is used to determine if there a continue is expected. + */ + protected ContinueDispatcher dispatcher; + + /** + * This is used to create a body consumer for the entity. + */ + protected ConsumerFactory factory; + + /** + * This is used to consume the header for the request entity. + */ + protected RequestConsumer header; + + /** + * This is used to consume the body for the request entity. + */ + protected BodyConsumer body; + + /** + * This is used to trace the progress of the request consumption. + */ + protected Trace trace; + + /** + * Constructor for the EntityConsumer object. This + * is used to build an entity from the constituent parts. Once + * all of the parts have been consumed they are available from + * the exposed methods of this consumed instance. + * + * @param allocator this is used to allocate the memory used + * @param channel this is the channel used to send a response + */ + public EntityConsumer(Allocator allocator, Channel channel) { + this.header = new RequestConsumer(); + this.dispatcher = new ContinueDispatcher(channel); + this.factory = new ConsumerFactory(allocator, header); + this.trace = channel.getTrace(); + } + + /** + * This is used to acquire the body for this HTTP entity. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Attachment objects. + * Each part can then be read as an individual message. + * + * @return the body provided by the HTTP request message + */ + public Body getBody() { + return body.getBody(); + } + + /** + * This provides the HTTP request header for the entity. This is + * always populated and provides the details sent by the client + * such as the target URI and the query if specified. Also this + * can be used to determine the method and protocol version used. + * + * @return the header provided by the HTTP request message + */ + public Header getHeader() { + return header; + } + + /** + * This consumes the header and body from the cursor. The header + * is consumed first followed by the body if there is any. There + * is a body of there is a Content-Length or a Transfer-Encoding + * header present. If there is no body then a substitute body + * is given which has an empty input stream. + * + * @param cursor used to consumed the bytes for the entity + */ + public void consume(ByteCursor cursor) throws IOException { + while(cursor.isReady()) { + if(header.isFinished()) { + if(body == null) { + CharSequence sequence = header.getHeader(); + + trace.trace(HEADER_FINISHED, sequence); + body = factory.getInstance(); + } + trace.trace(READ_BODY); + body.consume(cursor); + + if(body.isFinished()) { + trace.trace(BODY_FINISHED); + break; + } + } else { + trace.trace(READ_HEADER); + header.consume(cursor); + } + } + if(header.isFinished()) { + if(body == null) { + CharSequence sequence = header.getHeader(); + + trace.trace(HEADER_FINISHED, sequence); + dispatcher.execute(header); + body = factory.getInstance(); + } + } + } + + /** + * This is determined finished when the body has been consumed. + * If only the header has been consumed then the body will be + * created using the header information, the body is then read + * from the cursor, which may read nothing for an empty body. + * + * @return this returns true if the entity has been built + */ + public boolean isFinished() { + if(header.isFinished()) { + if(body == null) { + CharSequence sequence = header.getHeader(); + + trace.trace(HEADER_FINISHED, sequence); + body = factory.getInstance(); + } + return body.isFinished(); + } + return false; + + } + + /** + * This is used to determine if the header has finished. Exposing + * this method ensures the entity consumer can be used to determine + * if the header for the entity can be consumed before fully + * processing the entity body of the request message. + * + * @return determines if the header has been fully consumed + */ + public boolean isHeaderFinished() { + return header.isFinished(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/FileUploadConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/FileUploadConsumer.java new file mode 100644 index 0000000..8318013 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/FileUploadConsumer.java @@ -0,0 +1,272 @@ +/* + * FileUploadConsumer.java February 2013 + * + * Copyright (C) 2013, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.transport.ByteCursor; + +/** + * The FileUploadConsumer object is used to consume a + * list of parts encoded in the multipart format. This is can consume + * any number of parts from a cursor. Each part consumed is added to an + * internal part list which can be used to acquire the contents of the + * upload and inspect the headers provided for each uploaded part. To + * ensure that only a fixed number of bytes are consumed this wraps + * the provided cursor with a counter to ensure reads a limited amount. + * + * @author Niall Gallagher + */ +public class FileUploadConsumer implements BodyConsumer { + + /** + * This is used to read and parse the contents of the part series. + */ + private final BodyConsumer consumer; + + /** + * This counts the number of bytes remaining the the part series. + */ + private final AtomicLong count; + + /** + * Constructor for the FileUploadConsumer object. + * This is used to create an object that read a series of parts + * from a fixed length body. When consuming the body this will not + * read any more than the content length from the cursor. + * + * @param allocator this is the allocator used to allocate buffers + * @param boundary this is the boundary that is used by this + * @param length this is the number of bytes for this part series + */ + public FileUploadConsumer(Allocator allocator, byte[] boundary, long length) { + this.consumer = new PartSeriesConsumer(allocator, boundary, length); + this.count = new AtomicLong(length); + } + + /** + * This is used to acquire the body that has been consumed. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Part objects. + * Each part can then be read as an individual message. + * + * @return the body that has been consumed by this instance + */ + public Body getBody() { + return consumer.getBody(); + } + + /** + * This method is used to consume bytes from the provided cursor. + * Consuming of bytes from the cursor should be done in such a + * way that it does not block. So typically only the number of + * ready bytes in the ByteCursor object should be + * read. If there are no ready bytes then this will return. + * + * @param cursor used to consume the bytes from the HTTP pipeline + */ + public void consume(ByteCursor cursor) throws IOException { + ByteCounter counter = new ByteCounter(cursor); + + while(counter.isReady()) { + if(consumer.isFinished()) { + break; + } + consumer.consume(counter); + } + } + + /** + * This is used to determine whether the consumer has finished + * reading. The consumer is considered finished if it has read a + * terminal token or if it has exhausted the stream and can not + * read any more. Once finished the consumed bytes can be parsed. + * + * @return true if the consumer has finished reading its content + */ + public boolean isFinished() { + long remaining = count.get(); + + if(consumer.isFinished()) { + return true; + } + return remaining <= 0; + } + + /** + * The ByteCounter is a wrapper for a cursor that can + * be used to restrict the number of bytes consumed. This will + * count the bytes consumed and ensure that any requested data is + * restricted to a chunk less than or equal to the remaining bytes. + */ + private class ByteCounter implements ByteCursor { + + /** + * This is the cursor that this counter will delegate to. + */ + private final ByteCursor cursor; + + /** + * Constructor for the Counter object. This is used + * to create a special cursor that counts the bytes read and + * limits reads to the remaining bytes left in the part series. + * + * @param cursor this is the cursor that is delegated to + */ + public ByteCounter(ByteCursor cursor) { + this.cursor = cursor; + } + + /** + * Determines whether the cursor is still open. The cursor is + * considered open if there are still bytes to read. If there is + * still bytes buffered and the underlying transport is closed + * then the cursor is still considered open. + * + * @return true if the read method does not return a -1 value + */ + public boolean isOpen() throws IOException { + return cursor.isOpen(); + } + + /** + * Determines whether the cursor is ready for reading. When the + * cursor is ready then it guarantees that some amount of bytes + * can be read from the underlying stream without blocking. + * + * @return true if some data can be read without blocking + */ + public boolean isReady() throws IOException { + long limit = count.get(); + + if(limit > 0) { + return cursor.isReady(); + } + return false; + } + + /** + * Provides the number of bytes that can be read from the stream + * without blocking. This is typically the number of buffered or + * available bytes within the stream. When this reaches zero then + * the cursor may perform a blocking read. + * + * @return the number of bytes that can be read without blocking + */ + public int ready() throws IOException { + int limit = (int)count.get(); + int ready = cursor.ready(); + + if(ready > limit) { + return limit; + } + return ready; + } + + /** + * Reads a block of bytes from the underlying stream. This will + * read up to the requested number of bytes from the underlying + * stream. If there are no ready bytes on the stream this can + * return zero, representing the fact that nothing was read. + * + * @param data this is the array to read the bytes in to + * + * @return this returns the number of bytes read from the stream + */ + public int read(byte[] data) throws IOException { + return read(data, 0, data.length); + } + + /** + * Reads a block of bytes from the underlying stream. This will + * read up to the requested number of bytes from the underlying + * stream. If there are no ready bytes on the stream this can + * return zero, representing the fact that nothing was read. + * + * @param data this is the array to read the bytes in to + * @param off this is the offset to begin writing the bytes to + * @param len this is the number of bytes that are requested + * + * @return this returns the number of bytes read from the stream + */ + public int read(byte[] data, int off, int len) throws IOException { + int limit = (int)count.get(); + int size = Math.min(limit, len); + int chunk = cursor.read(data, off, size); + + if(chunk > 0) { + count.addAndGet(-chunk); + } + return chunk; + } + + /** + * Pushes the provided data on to the cursor. Data pushed on to + * the cursor will be the next data read from the cursor. This + * complements the reset method which will reset + * the cursors position on a stream. Allowing data to be pushed + * on to the cursor allows more flexibility. + * + * @param data this is the data to be pushed on to the cursor + */ + public void push(byte[] data) throws IOException { + push(data, 0, data.length); + } + + /** + * Pushes the provided data on to the cursor. Data pushed on to + * the cursor will be the next data read from the cursor. This + * complements the reset method which will reset + * the cursors position on a stream. Allowing data to be pushed + * on to the cursor allows more flexibility. + * + * @param data this is the data to be pushed on to the cursor + * @param off this is the offset to begin reading the bytes + * @param len this is the number of bytes that are to be used + */ + public void push(byte[] data, int off, int len) throws IOException { + if(len > 0) { + count.addAndGet(len); + } + cursor.push(data, off, len); + } + + /** + * Moves the cursor backward within the stream. This ensures + * that any bytes read from the last read can be pushed back + * in to the stream so that they can be read again. This will + * throw an exception if the reset can not be performed. + * + * @param len this is the number of bytes to reset back + * + * @return this is the number of bytes that have been reset + */ + public int reset(int len) throws IOException { + int reset = cursor.reset(len); + + if(reset > 0) { + count.addAndGet(reset); + } + return reset; + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/FixedLengthConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/FixedLengthConsumer.java new file mode 100644 index 0000000..5358d4b --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/FixedLengthConsumer.java @@ -0,0 +1,128 @@ +/* + * FixedLengthConsumer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.Buffer; + +/** + * The FixedLengthConsumer object reads a fixed number of + * bytes from a cursor. This is typically used when the Content-Length + * header is used as the body delimiter. In order to determine when + * the full body has been consumed this counts the bytes read. Once + * all the bytes have been read any overflow will be reset. All of the + * bytes read are appended to the internal buffer so they can be read. + * + * @author Niall Gallagher + */ +public class FixedLengthConsumer extends UpdateConsumer { + + /** + * This is the allocator used to allocate the buffer used. + */ + private Allocator allocator; + + /** + * This is the internal buffer used to accumulate the body. + */ + private Buffer buffer; + + /** + * This is the number of bytes to be consumed from the cursor. + */ + private long limit; + + /** + * Constructor for the FixedLengthConsumer object. This + * is used to create a consumer that reads a fixed number of bytes + * from a cursor and accumulates those bytes in an internal buffer + * so that it can be read at a later stage. + * + * @param allocator this is used to allocate the internal buffer + * @param limit this is the number of bytes that are to be read + */ + public FixedLengthConsumer(Allocator allocator, long limit) { + this.allocator = allocator; + this.limit = limit; + } + + + /** + * This is used to acquire the body that has been consumed. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Attachment objects. + * Each part can then be read as an individual message. + * + * @return the body that has been consumed by this instance + */ + public Body getBody() { + return new BufferBody(buffer); + } + + /** + * This method is used to append the contents of the array to the + * internal buffer. The appended bytes can be acquired from the + * internal buffer using an InputStream, or the text + * of the appended bytes can be acquired by encoding the bytes. + * + * @param array this is the array of bytes to be appended + * @param off this is the start offset in the array to read from + * @param len this is the number of bytes to write to the buffer + */ + private void append(byte[] array, int off, int len) throws IOException { + if(buffer == null) { + buffer = allocator.allocate(limit); + } + buffer.append(array, off, len); + } + + /** + * This is used to process the bytes that have been read from the + * cursor. This will count the number of bytes read, once all of + * the bytes that form the body have been read this returns the + * number of bytes that represent the overflow. + * + * @param array this is a chunk read from the cursor + * @param off this is the offset within the array the chunk starts + * @param count this is the number of bytes within the array + * + * @return this returns the number of bytes overflow that is read + */ + @Override + protected int update(byte[] array, int off, int count) throws IOException { + int mark = (int)limit; + + if(count >= limit) { + append(array, off, mark); + finished = true; + limit = 0; + return count - mark; + } + if(count > 0) { + append(array, off, count); + limit -= count; + } + return 0; + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/Header.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/Header.java new file mode 100644 index 0000000..4b79716 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/Header.java @@ -0,0 +1,213 @@ +/* + * Header.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.util.List; +import java.util.Locale; + +import org.simpleframework.http.Address; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.Path; +import org.simpleframework.http.Query; + +/** + * This is a Header object that is used to represent a + * basic form for the HTTP request message. This is used to extract + * values such as the request line and header values from the request + * message. Access to header values is done case insensitively. + *

+ * As well as providing the header values and request line values + * this will also provide convenience methods which enable the user + * to determine the length of the body this message header prefixes. + * + * @author Niall Gallagher + */ +public interface Header extends Segment { + + /** + * This can be used to get the target specified for this HTTP + * request. This corresponds to the URI sent in the request + * line. Typically this will be the path part of the URI, but + * can be the full URI if the request is a proxy request. + * + * @return the target URI that this HTTP request specifies + */ + String getTarget(); + + /** + * This method returns a CharSequence holding the data + * consumed for the request. A character sequence is returned as it + * can provide a much more efficient means of representing the header + * data by just wrapping the consumed byte array. + * + * @return this returns the characters consumed for the header + */ + CharSequence getHeader(); + + /** + * This is used to acquire the address from the request line. + * An address is the full URI including the scheme, domain, + * port and the query parts. This allows various parameters + * to be acquired without having to parse the target. + * + * @return this returns the address of the request line + */ + Address getAddress(); + + /** + * This is used to acquire the path as extracted from the + * the HTTP request URI. The Path object that is + * provided by this method is immutable, it represents the + * normalized path only part from the request URI. + * + * @return this returns the normalized path for the request + */ + Path getPath(); + + /** + * This method is used to acquire the query part from the + * HTTP request URI target. This will return only the values + * that have been extracted from the request URI target. + * + * @return the query associated with the HTTP target URI + */ + Query getQuery(); + + /** + * This can be used to get the HTTP method for this request. The + * HTTP specification RFC 2616 specifies the HTTP request methods + * in section 9, Method Definitions. Typically this will be a + * GET or POST method, but can be any valid alphabetic token. + * + * @return the HTTP method that this request has specified + */ + String getMethod(); + + /** + * This can be used to get the major number from a HTTP version. + * The major version corresponds to the major protocol type, that + * is the 1 of a HTTP/1.1 version string. Typically the major + * type is 1, by can be 0 for HTTP/0.9 clients. + * + * @return the major version number for the HTTP message + */ + int getMajor(); + + /** + * This can be used to get the minor number from a HTTP version. + * The minor version corresponds to the minor protocol type, that + * is the 0 of a HTTP/1.0 version string. This number is typically + * used to determine whether persistent connections are supported. + * + * @return the minor version number for the HTTP message + */ + int getMinor(); + + /** + * This method is used to get a List of the names + * for the headers. This will provide the original names for the + * HTTP headers for the message. Modifications to the provided + * list will not affect the header, the list is a simple copy. + * + * @return this returns a list of the names within the header + */ + List getNames(); + + /** + * This can be used to get the integer of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + int getInteger(String name); + + /** + * This can be used to get the date of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + long getDate(String name); + + /** + * This is used to acquire a cookie usiing the name of that cookie. + * If the cookie exists within the HTTP header then it is returned + * as a Cookie object. Otherwise this method will + * return null. Each cookie object will contain the name, value + * and path of the cookie as well as the optional domain part. + * + * @param name this is the name of the cookie object to acquire + * + * @return this returns a cookie object from the header or null + */ + Cookie getCookie(String name); + + /** + * This is used to acquire all cookies that were sent in the header. + * If any cookies exists within the HTTP header they are returned + * as Cookie objects. Otherwise this method will an + * empty list. Each cookie object will contain the name, value and + * path of the cookie as well as the optional domain part. + * + * @return this returns all cookie objects from the HTTP header + */ + List getCookies(); + + /** + * This is used to acquire the locales from the request header. The + * locales are provided in the Accept-Language header. + * This provides an indication as to the languages that the client + * accepts. It provides the locales in preference order. + * + * @return this returns the locales preferred by the client + */ + List getLocales(); + + /** + * This is used to determine if the header represents one that + * requires the HTTP/1.1 continue expectation. If the request + * does require this expectation then it should be send the + * 100 status code which prompts delivery of the message body. + * + * @return this returns true if a continue expectation exists + */ + boolean isExpectContinue(); + + /** + * This method returns a string representing the header that was + * consumed by this consumer. For performance reasons it is better + * to acquire the character sequence representing the header as it + * does not require the allocation on new memory. + * + * @return this returns a string representation of this request + */ + String toString(); +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/HeaderConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/HeaderConsumer.java new file mode 100644 index 0000000..55dab40 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/HeaderConsumer.java @@ -0,0 +1,114 @@ +/* + * HeaderConsumer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.util.List; + +import org.simpleframework.http.Cookie; + +/** + * The HeaderConsumer object is used to consume a HTTP + * header from the cursor. This extends the segment consumer with + * methods specific to the header. Also this enables session cookies + * to be created using the cookies extracted from the header. + * + * @author Niall Gallagher + */ +public abstract class HeaderConsumer extends SegmentConsumer implements Header { + + /** + * Constructor for the HeaderConsumer object. This + * is used to create a consumer capable of reading a header from + * a provided cursor. All methods of the Header + * interface are implemented in this object. + */ + protected HeaderConsumer() { + super(); + } + + /** + * This can be used to get the date of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public long getDate(String name) { + return header.getDate(name); + } + + /** + * This can be used to get the integer of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public int getInteger(String name) { + return header.getInteger(name); + } + + /** + * This method is used to get a List of the names + * for the headers. This will provide the original names for the + * HTTP headers for the message. Modifications to the provided + * list will not affect the header, the list is a simple copy. + * + * @return this returns a list of the names within the header + */ + public List getNames() { + return header.getNames(); + } + + /** + * This is used to acquire a cookie using the name of that cookie. + * If the cookie exists within the HTTP header then it is returned + * as a Cookie object. Otherwise this method will + * return null. Each cookie object will contain the name, value + * and path of the cookie as well as the optional domain part. + * + * @param name this is the name of the cookie object to acquire + * + * @return this returns a cookie object from the header or null + */ + public Cookie getCookie(String name) { + return header.getCookie(name); + } + + /** + * This is used to acquire all cookies that were sent in the header. + * If any cookies exists within the HTTP header they are returned + * as Cookie objects. Otherwise this method will an + * empty list. Each cookie object will contain the name, value and + * path of the cookie as well as the optional domain part. + * + * @return this returns all cookie objects from the HTTP header + */ + public List getCookies() { + return header.getCookies(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/Message.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/Message.java new file mode 100644 index 0000000..ac01dff --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/Message.java @@ -0,0 +1,273 @@ +/* + * Message.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.util.List; + +import org.simpleframework.http.Cookie; + +/** + * The Message object is used to store an retrieve the + * headers for both a request and response. Headers are stored and + * retrieved in a case insensitive manner according to RFC 2616. + * The message also allows multiple header values to be added to a + * single header name, headers such as Cookie and Set-Cookie can be + * added multiple times with different values. + * + * @author Niall Gallagher + */ +public interface Message { + + /** + * This is used to acquire the names of the of the headers that + * have been set in the response. This can be used to acquire all + * header values by name that have been set within the response. + * If no headers have been set this will return an empty list. + * + * @return a list of strings representing the set header names + */ + List getNames(); + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void setValue(String name, String value); + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void setInteger(String name, int value); + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTP date string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + void setDate(String name, long date); + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void addValue(String name, String value); + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getInteger in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + void addInteger(String name, int value); + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTPdate string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + void addDate(String name, long date); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * @param index gets the value at the index if there are multiple + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name, int index); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the integer + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + int getInteger(String name); + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the long value + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + long getDate(String name); + + /** + * This returns the Cookie object stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If the cookie does + * not exist under the specified name this will return null. + * + * @param name this is the name of the cookie to be retrieved + * + * @return returns the Cookie by the given name + */ + Cookie getCookie(String name); + + /** + * This returns all Cookie objects stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If there are no + * cookies then this will return an empty list. + * + * @return returns all the Cookie in the response + */ + List getCookies(); + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * This is a convenience method that avoids cookie creation. + * + * @param name this is the cookie to be added to the response + * @param value this is the cookie value that is to be used + * + * @return returns the cookie that has been set in the response + */ + Cookie setCookie(String name, String value); + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * + * @param cookie this is the cookie to be added to the response + * + * @return returns the cookie that has been set in the response + */ + Cookie setCookie(Cookie cookie); + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered list of tokens extracted from the header(s) + */ + List getValues(String name); + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param list this is the list of individual header values + * + * @return ordered list of tokens extracted from the header(s) + */ + List getValues(List list); + + /** + * This is used to acquire all the individual header values from + * the message. The header values provided by this are unparsed + * and represent the actual string values that have been added to + * the message keyed by a given header name. + * + * @param name the name of the header to get the values for + * + * @return this returns a list of the values for the header name + */ + List getAll(String name); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/MessageHeader.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/MessageHeader.java new file mode 100644 index 0000000..b809efe --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/MessageHeader.java @@ -0,0 +1,477 @@ +/* + * Message.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.util.LinkedList; +import java.util.List; + +import org.simpleframework.common.KeyMap; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.parse.DateParser; +import org.simpleframework.http.parse.ValueParser; + +/** + * The Message object is used to store an retrieve the + * headers for both a request and response. Headers are stored and + * retrieved in a case insensitive manner according to RFC 2616. + * The message also allows multiple header values to be added to a + * single header name, headers such as Cookie and Set-Cookie can be + * added multiple times with different values. + * + * @author Niall Gallagher + */ +public class MessageHeader implements Message { + + /** + * This is used to store the cookies added to the HTTP header. + */ + private final KeyMap cookies; + + /** + * This is used to store multiple header values for a name. + */ + private final KeyMap values; + + /** + * This is used to store the individual names for the header. + */ + private final KeyMap names; + + /** + * This is used to parse all date headers added to the message. + */ + private final DateParser parser; + + /** + * Constructor for the Message object. This is used + * to create a case insensitive means for storing HTTP header + * names and values. Dates can also be added to message as a + * long value and is converted to RFC 1123 compliant date string. + */ + public MessageHeader() { + this.cookies = new KeyMap(); + this.values = new KeyMap(); + this.names = new KeyMap(); + this.parser = new DateParser(); + } + + /** + * This is used to acquire the names of the of the headers that + * have been set in the response. This can be used to acquire all + * header values by name that have been set within the response. + * If no headers have been set this will return an empty list. + * + * @return a list of strings representing the set header names + */ + public List getNames() { + return names.getValues(); + } + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void setValue(String name, String value) { + List list = getAll(name); + + if(value == null) { + String token = name.toLowerCase(); + + values.remove(token); + names.remove(token); + } else { + list.clear(); + list.add(value); + } + } + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void setInteger(String name, int value) { + setValue(name, String.valueOf(value)); + } + + /** + * This can be used to set a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void setLong(String name, long value) { + setValue(name, String.valueOf(value)); + } + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTP date string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * This will perform a remove using the issued header + * name before the header value is set. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + public void setDate(String name, long date) { + setValue(name, parser.convert(date)); + } + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getValue in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void addValue(String name, String value) { + List list = getAll(name); + + if(value != null) { + list.add(value); + } + } + + /** + * This can be used to add a HTTP message header to this object. + * The name and value of the HTTP message header will be used to + * create a HTTP message header object which can be retrieved using + * the getInteger in combination with the get methods. + * + * @param name the name of the HTTP message header to be added + * @param value the value the HTTP message header will have + */ + public void addInteger(String name, int value) { + addValue(name, String.valueOf(value)); + } + + /** + * This is used as a convenience method for adding a header that + * needs to be parsed into a HTTPdate string. This will convert + * the date given into a date string defined in RFC 2616 sec 3.3.1. + * + * @param name the name of the HTTP message header to be added + * @param date the value constructed as an RFC 1123 date string + */ + public void addDate(String name, long date) { + addValue(name, parser.convert(date)); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name) { + return getValue(name, 0); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the full string + * representing the named header value. If the named header does + * not exist then this will return a null value. + * + * @param name the HTTP message header to get the value from + * @param index this is the index to get the value from + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name, int index) { + List list = getAll(name); + + if(list.size() > index) { + return list.get(index); + } + return null; + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the integer + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public int getInteger(String name) { + String value = getValue(name); + + if(value == null) { + return -1; + } + return Integer.parseInt(value); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the long + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public long getLong(String name) { + String value = getValue(name); + + if(value == null) { + return -1L; + } + return Long.parseLong(value); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. This will return the long value + * representing the named header value. If the named header does + * not exist then this will return a value of minus one, -1. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public long getDate(String name) { + String value = getValue(name); + + if(value == null) { + return -1; + } + return parser.convert(value); + } + + /** + * This returns the Cookie object stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If the cookie does + * not exist under the specified name this will return null. + * + * @param name this is the name of the cookie to be retrieved + * + * @return returns the Cookie by the given name + */ + public Cookie getCookie(String name) { + return cookies.get(name); + } + + /** + * This returns all Cookie objects stored under the + * specified name. This is used to retrieve cookies that have been + * set with the setCookie methods. If there are no + * cookies then this will return an empty list. + * + * @return returns all the Cookie in the response + */ + public List getCookies() { + return cookies.getValues(); + } + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * This is a convenience method that avoids cookie creation. + * + * @param name this is the cookie to be added to the response + * @param value this is the cookie value that is to be used + * + * @return returns the cookie that has been set in the response + */ + public Cookie setCookie(String name, String value) { + return setCookie(new Cookie(name, value, true)); + } + + /** + * The setCookie method is used to set a cookie value + * with the cookie name. This will add a cookie to the response + * stored under the name of the cookie, when this is committed it + * will be added as a Set-Cookie header to the resulting response. + * + * @param cookie this is the cookie to be added to the response + * + * @return returns the cookie that has been set in the response + */ + public Cookie setCookie(Cookie cookie) { + String name = cookie.getName(); + + if(name != null) { + cookies.put(name, cookie); + } + return cookie; + } + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered list of tokens extracted from the header(s) + */ + public List getValues(String name) { + return getValues(getAll(name)); + } + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param list this is the list of individual header values + * + * @return ordered list of tokens extracted from the header(s) + */ + public List getValues(List list) { + return new ValueParser(list).list(); + } + + /** + * This is used to acquire all the individual header values from + * the message. The header values provided by this are unparsed + * and represent the actual string values that have been added to + * the message keyed by a given header name. + * + * @param name the name of the header to get the values for + * + * @return this returns a list of the values for the header name + */ + public List getAll(String name) { + String token = name.toLowerCase(); + Series series = values.get(token); + + if(series == null) { + return getAll(name, token); + } + return series.getValues(); + } + + /** + * This is used to acquire all the individual header values from + * the message. The header values provided by this are unparsed + * and represent the actual string values that have been added to + * the message keyed by a given header name. + * + * @param name the name of the header to get the values for + * @param token this provides a lower case version of the header + * + * @return this returns a list of the values for the header name + */ + private List getAll(String name, String token) { + Series series = new Series(); + String value = names.get(token); + + if(value == null) { + names.put(token, name); + } + values.put(token, series); + + return series.getValues(); + } + + /** + * The Series object is used to represent a list of + * HTTP header value for a given name. It allows multiple values + * to exist for a given header, such as the Cookie header. Most + * entries will contain a single value. + */ + private class Series { + + /** + * Contains the header values that belong to the entry name. + */ + private List value; + + /** + * Constructor for the Entry object. The entry is + * created using the name of the HTTP header. Values can be + * added to the entry list in order to build up the header. + */ + public Series() { + this.value = new LinkedList(); + } + + /** + * This returns the list of header values associated with the + * header name. Each value is added as an individual header + * prefixed by the header name and a semicolon character. + * + * @return this returns the list of values for the header + */ + public List getValues() { + return value; + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartBodyConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartBodyConsumer.java new file mode 100644 index 0000000..b30f28f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartBodyConsumer.java @@ -0,0 +1,129 @@ +/* + * PartBodyConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.transport.ByteCursor; + +/** + * The PartBodyConsumer object is used to consume a part + * the contents of a multipart body. This will consume the part and + * add it to a part list, once the part has been consumed and added + * to the part list a terminal token is consumed, which is a carriage + * return and line feed. + * + * @author Niall Gallagher + */ +class PartBodyConsumer implements BodyConsumer { + + /** + * This is the token that is consumed after the content body. + */ + private static final byte[] LINE = { '\r', '\n' }; + + /** + * This is used to consume the content from the multipart upload. + */ + private ContentConsumer content; + + /** + * This is used to consume the final terminal token from the part. + */ + private ByteConsumer token; + + /** + * Constructor for the PartBodyConsumer object. This + * is used to create a consumer that reads the body of a part in + * a multipart request body. The terminal token must be provided + * so that the end of the part body can be determined. + * + * @param allocator this is used to allocate the internal buffer + * @param segment this represents the headers for the part body + * @param boundary this is the message boundary for the body part + */ + public PartBodyConsumer(Allocator allocator, Segment segment, byte[] boundary) { + this(allocator, segment, new PartData(), boundary); + } + + /** + * Constructor for the PartBodyConsumer object. This + * is used to create a consumer that reads the body of a part in + * a multipart request body. The terminal token must be provided + * so that the end of the part body can be determined. + * + * @param allocator this is used to allocate the internal buffer + * @param segment this represents the headers for the part body + * @param series this is the part list that this body belongs in + * @param boundary this is the message boundary for the body part + */ + public PartBodyConsumer(Allocator allocator, Segment segment, PartSeries series, byte[] boundary) { + this.content = new ContentConsumer(allocator, segment, series, boundary); + this.token = new TokenConsumer(allocator, LINE); + } + + /** + * This is used to acquire the body that has been consumed. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Attachment objects. + * Each part can then be read as an individual message. + * + * @return the body that has been consumed by this instance + */ + public Body getBody() { + return content.getBody(); + } + + /** + * This is used to consume the part body from the cursor. This + * initially reads the body of the part, which represents the + * actual payload exposed via the Part interface + * once the payload has been consumed the terminal is consumed. + * + * @param cursor this is the cursor to consume the body from + */ + public void consume(ByteCursor cursor) throws IOException { + while(cursor.isReady()) { + if(content.isFinished()) { + if(token.isFinished()) { + break; + } + token.consume(cursor); + } else { + content.consume(cursor); + } + } + } + + /** + * This is used to determine whether the part body has been read + * from the cursor successfully. In order to determine if all of + * the bytes have been read successfully this will check to see + * of the terminal token had been consumed. + * + * @return true if the part body and terminal have been read + */ + public boolean isFinished() { + return token.isFinished(); + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartConsumer.java new file mode 100644 index 0000000..cc54558 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartConsumer.java @@ -0,0 +1,135 @@ +/* + * PartConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.transport.ByteCursor; + +/** + * The PartConsumer object is used to consume a part + * from a part list. A part consists of a header and a body, which + * can be either a simple chunk of data or another part list. This + * must be able to cope with either a simple body or a part list. + * + * @author Niall Gallagher + */ +class PartConsumer implements ByteConsumer { + + /** + * This is used to consume the header message of the part. + */ + private SegmentConsumer header; + + /** + * This is used to consume the body data from the part. + */ + private BodyConsumer body; + + /** + * This is used to determine what type the body data is. + */ + private PartFactory factory; + + /** + * This is used to add the consumed parts to when finished. + */ + private PartSeries series; + + /** + * This is the current consumer used to read from the cursor. + */ + private ByteConsumer current; + + /** + * This is the terminal token that ends the part payload. + */ + private byte[] terminal; + + /** + * Constructor for the PartConsumer object. This is + * used to create a consumer used to read the contents of a part + * and the boundary that terminates the content. Any parts that + * are created by this are added to the provided part list. + * + * @param allocator this is the allocator used to creat buffers + * @param series this is the part list used to store the parts + * @param terminal this is the terminal token for the part + * @param length this is the length of the parent part series + */ + public PartConsumer(Allocator allocator, PartSeries series, byte[] terminal, long length) { + this.header = new PartHeaderConsumer(allocator); + this.factory = new PartFactory(allocator, header, length); + this.terminal = terminal; + this.current = header; + this.series = series; + } + + /** + * This is used to create a new body consumer used to consume the + * part body from for the list. This will ensure that the part + * data is created based on the part header consumed. The types + * of part supported are part lists and part body. + * + * @return this returns a consumed for the part content + */ + private BodyConsumer getConsumer() { + return factory.getInstance(series, terminal); + } + + /** + * This is used to consume the part body from the cursor. This + * initially reads the body of the part, which represents the + * actual payload exposed via the Part interface + * once the payload has been consumed the terminal is consumed. + * + * @param cursor this is the cursor to consume the body from + */ + public void consume(ByteCursor cursor) throws IOException { + while(cursor.isReady()) { + if(header.isFinished()) { + if(body == null) { + body = getConsumer(); + current = body; + } else { + if(body.isFinished()) + break; + } + } + current.consume(cursor); + } + } + + /** + * This is used to determine whether the part body has been read + * from the cursor successfully. In order to determine if all of + * the bytes have been read successfully this will check to see + * of the terminal token had been consumed. + * + * @return true if the part body and terminal have been read + */ + public boolean isFinished() { + if(body != null) { + return body.isFinished(); + } + return false; + } +} + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartData.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartData.java new file mode 100644 index 0000000..cf7a90a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartData.java @@ -0,0 +1,101 @@ +/* + * PartData.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.util.ArrayList; +import java.util.List; + +import org.simpleframework.common.KeyMap; +import org.simpleframework.http.Part; + +/** + * The PartData object represents an ordered list of + * parts that were uploaded within a HTTP entity body. This allows + * the parts to be iterated over, or if required accessed by name. + * In order to access the Part object by name it must + * have had a name within the Content-Disposition header. + * + * @author Niall Gallagher + */ +class PartData implements PartSeries { + + /** + * This is the key map that is used to store the part objects. + */ + private final KeyMap map; + + /** + * This is the list of attachments for this part list object. + */ + private final List list; + + /** + * Constructor for the PartData object. This is used + * to create an order list of parts that is used by the request + * to access the individual parts uploaded with a HTTP body. + */ + public PartData() { + this.list = new ArrayList(); + this.map = new KeyMap(); + } + + /** + * This is used to acquire the attachments associated with this + * list. If no parts have been collected by this list then it + * will return an empty list. The order of the parts in the list + * are the insertion order for consistency. + * + * @return this returns the parts collected in iteration order + */ + public List getParts() { + return list; + } + + /** + * This is used to add a part to the list. The order the parts are + * added to the list is the iteration order. If the part has a name + * that is not null then it is added to an internal map using that + * name. This allows it to be accesses by name at a later time. + * + * @param part this is the part that is to be added to the list + * + * @return returns true if the list has changed due to the add + */ + public boolean addPart(Part part) { + String name = part.getName(); + + if(name != null) { + map.put(name, part); + } + return list.add(part); + } + + /** + * This method is used to acquire a Part from the list + * using a known name for the part. This is a convenient way to + * access a part when the name for the part is known. + * + * @param name this is the name of the part to acquire + * + * @return the named part or null if the part does not exist + */ + public Part getPart(String name) { + return map.get(name); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryConsumer.java new file mode 100644 index 0000000..8e1a38c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryConsumer.java @@ -0,0 +1,112 @@ +/* + * PartEntryConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.transport.ByteCursor; + +/** + * The PartEntryConsumer object is used to consume each + * part from the part list. This is combines the task of consuming + * the part, which consists of a header and a body, and a boundary + * which identifies the end of the message content. + * + * @author Niall Gallagher + */ +class PartEntryConsumer implements ByteConsumer { + + /** + * This is used to consume the boundary at the end of a part. + */ + private final BoundaryConsumer boundary; + + /** + * This is used to consume the actual part from the list. + */ + private final ByteConsumer consumer; + + /** + * Constructor for the PartEntryConsumer object. This + * is used to create a consumer that will read the message part + * and the boundary that terminates the part. All contents that + * are read are appended to an internal buffer. + * + * @param allocator this is the allocator used for the buffer + * @param series this is the list used to accumulate the parts + * @param terminal this is the terminal token for the part list + * @param length this is the length of the parent part series + */ + public PartEntryConsumer(Allocator allocator, PartSeries series, byte[] terminal, long length) { + this.consumer = new PartConsumer(allocator, series, terminal, length); + this.boundary = new BoundaryConsumer(allocator, terminal); + } + + /** + * This is used to consume the part body from the cursor. This + * initially reads the body of the part, which represents the + * actual content exposed via the Part interface + * once the content has been consumed the terminal is consumed. + * + * @param cursor this is the cursor to consume the body from + */ + public void consume(ByteCursor cursor) throws IOException { + while(cursor.isReady()) { + if(!boundary.isFinished()) { + boundary.consume(cursor); + } else { + if(consumer.isFinished()) { + break; + } + if(boundary.isEnd()) { + break; + } + consumer.consume(cursor); + } + } + } + + /** + * This is used to determine whether the part body has been read + * from the cursor successfully. In order to determine if all of + * the bytes have been read successfully this will check to see + * of the terminal token had been consumed. + * + * @return true if the part body and terminal have been read + */ + public boolean isFinished() { + if(boundary.isEnd()) { + return true; + } + return consumer.isFinished(); + } + + /** + * This is used to determine whether the terminal token read is + * the final terminal token. The final terminal token is a + * normal terminal token, however it ends with two hyphens and + * a carriage return line feed, this ends the part list. + * + * @return true if this was the last part within the list + */ + public boolean isEnd() { + return boundary.isEnd(); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryFactory.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryFactory.java new file mode 100644 index 0000000..aba5738 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryFactory.java @@ -0,0 +1,84 @@ +/* + * PartEntryFactory.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import org.simpleframework.common.buffer.Allocator; + +/** + * This PartEntryFactory object provides a factory for + * creating part entry consumers. The part entry consumers created + * read individual entries from a list of parts within a stream. + * This is basically a convenience factory for the list consumer. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.message.PartSeriesConsumer + */ +class PartEntryFactory { + + /** + * This is used to accumulate all the parts of the upload. + */ + private final PartSeries series; + + /** + * This is used to allocate the buffers used by the entry. + */ + private final Allocator allocator; + + /** + * This is the terminal token used to delimiter the upload. + */ + private final byte[] terminal; + + /** + * This is the length of the parent part series body. + */ + private final long length; + + /** + * Constructor for the PartEntryFactory object. + * This is used to create a factory for entry consumers that + * can be used to read an entry from a part list. + * + * @param allocator this is the allocator used for buffers + * @param series this is the list of parts that are extracted + * @param terminal this is the terminal buffer to be used + * @param length this is the length of the parent part series + */ + public PartEntryFactory(Allocator allocator, PartSeries series, byte[] terminal, long length) { + this.allocator = allocator; + this.terminal = terminal; + this.series = series; + this.length = length; + } + + + /** + * This creates a new part entry consumer that can be used to + * read the next part from the list. The consumer instantiated + * by this factory acquires the allocator, list and boundary + * from the enclosing part list consumer instance. + * + * @return a part entry consumer for this part list consumer + */ + public PartEntryConsumer getInstance() { + return new PartEntryConsumer(allocator, series, terminal, length); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartFactory.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartFactory.java new file mode 100644 index 0000000..f394ba6 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartFactory.java @@ -0,0 +1,78 @@ +/* + * PartFactory.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import org.simpleframework.common.buffer.Allocator; + +/** + * The PartFactory represents a factory for creating the + * consumers that are used to read a multipart upload message. This + * supports two types of consumers for the multipart upload, lists + * and bodies. A part list is basically a collection of parts and or + * part lists. The part type is determined from the part header. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.message.PartSeriesConsumer + * @see org.simpleframework.http.message.PartBodyConsumer + */ +class PartFactory extends ConsumerFactory { + + /** + * This is the overall length of the parent part series. + */ + private final long length; + + /** + * Constructor for the PartFactory object. This is + * used to create a factory using a buffer allocator, which will + * create a buffer for accumulating the entire message body, + * also to ensure the correct part type is created this requires + * the header information for the part. + * + * @param allocator this is used to allocate the internal buffer + * @param header this is used to determine the part type + * @param length this is the length of the parent part series + */ + public PartFactory(Allocator allocator, Segment header, long length) { + super(allocator, header); + this.length = length; + } + + /** + * This method is used to create the consumer given the list and + * boundary for the part. In order to determine the part type + * this will consult the header consumed for the part. Depending + * on whether it is a list or body a suitable consumer is created. + * + * @param series this is the list used to collect the parts + * @param boundary this is the boundary used to terminate the part + * + * @return this will return the consumer for the part body + */ + public BodyConsumer getInstance(PartSeries series, byte[] boundary) { + byte[] terminal = getBoundary(segment); + + if(isUpload(segment)) { + return new PartSeriesConsumer(allocator, series, terminal, length); + } + return new PartBodyConsumer(allocator, segment, series, boundary); + } +} + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartHeaderConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartHeaderConsumer.java new file mode 100644 index 0000000..7612d8d --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartHeaderConsumer.java @@ -0,0 +1,85 @@ +/* + * PartHeaderConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.Buffer; + +/** + * The PartHeaderConsumer object is used to consume the + * header for a multipart message. This performs a parse of the + * HTTP headers within the message up to the terminal carriage return + * and line feed token. Once this had been read the contents of the + * header are appended to a buffer so they can be read later. + * + * @author Niall Gallagher + */ +class PartHeaderConsumer extends SegmentConsumer { + + /** + * This is used to allocate the internal buffer for the header. + */ + private Allocator allocator; + + /** + * This is the internal buffer used to store the header. + */ + private Buffer buffer; + + /** + * Constructor for the PartHeaderConsumer object. An + * allocator is required so that the header consumer can create a + * buffer to store the contents of the consumed message. + * + * @param allocator this is the allocator used to create a buffer + */ + public PartHeaderConsumer(Allocator allocator) { + this.allocator = allocator; + } + + /** + * This is used to process the header consumer once all of the + * headers have been read. This will simply parse all of the + * headers and append the consumed bytes to the internal buffer. + * Appending the bytes ensures that the whole upload can be + * put back together as a single byte stream if required. + */ + @Override + protected void process() throws IOException { + headers(); + append(); + } + + /** + * This is used to allocate the internal buffer and append the + * consumed bytes to the buffer. Once the header is added to + * the internal buffer this is finished and the next part of + * the upload can be consumed. + */ + private void append() throws IOException { + if(buffer == null) { + buffer = allocator.allocate(count); + } + buffer.append(array, 0, count); + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeries.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeries.java new file mode 100644 index 0000000..c971d97 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeries.java @@ -0,0 +1,68 @@ +/* + * PartSeries.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.util.List; + +import org.simpleframework.http.Part; + +/** + * The PartSeries object represents an ordered list of + * parts that were uploaded within a HTTP entity body. This allows + * the parts to be iterated over, or if required accessed by name. + * In order to access the Part object by name it must + * have had a name within the Content-Disposition header. + * + * @author Niall Gallagher + */ +interface PartSeries { + + /** + * This is used to acquire the attachments associated with this + * list. If no parts have been collected by this list then it + * will return an empty list. The order of the parts in the list + * are the insertion order for consistency. + * + * @return this returns the parts collected in iteration order + */ + List getParts(); + + /** + * This is used to add a part to the list. The order the parts are + * added to the list is the iteration order. If the part has a name + * that is not null then it is added to an internal map using that + * name. This allows it to be accesses by name at a later time. + * + * @param part this is the part that is to be added to the list + * + * @return returns true if the list has changed due to the add + */ + boolean addPart(Part part); + + /** + * This method is used to acquire a Part from the list + * using a known name for the part. This is a convenient way to + * access a part when the name for the part is known. + * + * @param name this is the name of the part to acquire + * + * @return the named part or null if the part does not exist + */ + Part getPart(String name); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeriesConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeriesConsumer.java new file mode 100644 index 0000000..c3a0417 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeriesConsumer.java @@ -0,0 +1,165 @@ +/* + * PartSeriesConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.BufferAllocator; +import org.simpleframework.transport.ByteCursor; + +/** + * The PartSeriesConsumer object is used to consume a list + * of parts encoded in the multipart format. This is can consume any + * number of parts from a cursor. Each part consumed is added to an + * internal part list which can be used to acquire the contents of the + * upload and inspect the headers provided for each uploaded part. To + * ensure that only a fixed number of bytes are consumed this uses a + * content length for an internal buffer. + * + * @author Niall Gallagher + */ +class PartSeriesConsumer implements BodyConsumer { + + /** + * This is used to consume individual parts from the part list. + */ + private PartEntryConsumer consumer; + + /** + * This is the factory that is used to create the consumers used. + */ + private PartEntryFactory factory; + + /** + * This is used to both allocate and buffer the part list body. + */ + private BufferAllocator buffer; + + /** + * This is used to accumulate all the parts of the upload. + */ + private PartSeries series; + + /** + * Constructor for the PartSeriesConsumer object. This + * will create a consumer that is capable of breaking an upload in + * to individual parts so that they can be accessed and used by + * the receiver of the HTTP request message. + * + * @param allocator this is used to allocate the internal buffer + * @param boundary this is the boundary used for the upload + */ + public PartSeriesConsumer(Allocator allocator, byte[] boundary) { + this(allocator, boundary, 8192); + } + + /** + * Constructor for the PartSeriesConsumer object. This + * will create a consumer that is capable of breaking an upload in + * to individual parts so that they can be accessed and used by + * the receiver of the HTTP request message. + * + * @param allocator this is used to allocate the internal buffer + * @param boundary this is the boundary used for the upload + * @param length this is the number of bytes the upload should be + */ + public PartSeriesConsumer(Allocator allocator, byte[] boundary, long length) { + this(allocator, new PartData(), boundary, length); + } + + /** + * Constructor for the PartSeriesConsumer object. This + * will create a consumer that is capable of breaking an upload in + * to individual parts so that they can be accessed and used by + * the receiver of the HTTP request message. + * + * @param allocator this is used to allocate the internal buffer + * @param boundary this is the boundary used for the upload + * @param series this is the part list used to accumulate the parts + */ + public PartSeriesConsumer(Allocator allocator, PartSeries series, byte[] boundary) { + this(allocator, series, boundary, 8192); + } + + /** + * Constructor for the PartSeriesConsumer object. This + * will create a consumer that is capable of breaking an upload in + * to individual parts so that they can be accessed and used by + * the receiver of the HTTP request message. + * + * @param allocator this is used to allocate the internal buffer + * @param series this is the part list used to accumulate the parts + * @param boundary this is the boundary used for the upload + * @param length this is the number of bytes the upload should be + */ + public PartSeriesConsumer(Allocator allocator, PartSeries series, byte[] boundary, long length) { + this.buffer = new BufferAllocator(allocator, length); + this.consumer = new PartEntryConsumer(buffer, series, boundary, length); + this.factory = new PartEntryFactory(buffer, series, boundary, length); + this.series = series; + } + + /** + * This is used to acquire the body that has been consumed. This + * will return a body which can be used to read the content of + * the message, also if the request is multipart upload then all + * of the parts are provided as Attachment objects. + * Each part can then be read as an individual message. + * + * @return the body that has been consumed by this instance + */ + public Body getBody() { + return new BufferBody(buffer, series); + } + + /** + * This is used to consume the part list from the cursor. This + * initially reads the list of parts, which represents the + * actual content exposed via the PartSeries object, + * once the content has been consumed the terminal is consumed. + * + * @param cursor this is the cursor to consume the list from + */ + public void consume(ByteCursor cursor) throws IOException { + while(cursor.isReady()) { + if(!consumer.isFinished()) { + consumer.consume(cursor); + } else { + if(!consumer.isEnd()) { + consumer = factory.getInstance(); + } else { + break; + } + } + } + } + + /** + * This is used to determine whether the part body has been read + * from the cursor successfully. In order to determine if all of + * the bytes have been read successfully this will check to see + * of the terminal token had been consumed. + * + * @return true if the part body and terminal have been read + */ + public boolean isFinished() { + return consumer.isEnd(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/RequestConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/RequestConsumer.java new file mode 100644 index 0000000..0687271 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/RequestConsumer.java @@ -0,0 +1,457 @@ +/* + * RequestConsumer.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.util.List; + +import org.simpleframework.http.Address; +import org.simpleframework.http.Path; +import org.simpleframework.http.Query; +import org.simpleframework.http.parse.AddressParser; + +/** + * The RequestConsumer object is used to parse the HTTP + * request line followed by the HTTP message headers. This parses the + * request URI such that the query parameters and path are extracted + * and normalized. It performs this using external parsers, which + * will remove and escaped characters and normalize the path segments. + * Finally this exposes the HTTP version used using the major and + * minor numbers sent with the HTTP request. + * + * @author Niall Gallagher + */ +public class RequestConsumer extends HeaderConsumer { + + /** + * This is the address parser used to parse the request URI. + */ + protected AddressParser parser; + + /** + * This is the method token send with the HTTP request header. + */ + protected String method; + + /** + * This represents the raw request URI in an unparsed form. + */ + protected String target; + + /** + * This is the major version number of the HTTP request header. + */ + protected int major; + + /** + * This is the minor version number of the HTTP request header. + */ + protected int minor; + + /** + * Constructor for the RequestConsumer object. This + * is used to create a consumer which can consume a HTTP request + * header and provide the consumed contents via a known interface. + * This also further breaks down the request URI for convenience. + */ + public RequestConsumer() { + super(); + } + + /** + * This can be used to get the target specified for this HTTP + * request. This corresponds to the URI sent in the request + * line. Typically this will be the path part of the URI, but + * can be the full URI if the request is a proxy request. + * + * @return the target URI that this HTTP request specifies + */ + public String getTarget() { + return target; + } + + /** + * This is used to acquire the address from the request line. + * An address is the full URI including the scheme, domain, + * port and the query parts. This allows various parameters + * to be acquired without having to parse the target. + * + * @return this returns the address of the request line + */ + public Address getAddress() { + if(parser == null) { + parser = new AddressParser(target); + } + return parser; + } + + /** + * This method is used to acquire the query part from the + * HTTP request URI target. This will return only the values + * that have been extracted from the request URI target. + * + * @return the query associated with the HTTP target URI + */ + public Query getQuery() { + return getAddress().getQuery(); + } + + /** + * This is used to acquire the path as extracted from the + * the HTTP request URI. The Path object that is + * provided by this method is immutable, it represents the + * normalized path only part from the request URI. + * + * @return this returns the normalized path for the request + */ + public Path getPath() { + return getAddress().getPath(); + } + + /** + * This can be used to get the HTTP method for this request. The + * HTTP specification RFC 2616 specifies the HTTP request methods + * in section 9, Method Definitions. Typically this will be a + * GET or POST method, but can be any valid alphabetic token. + * + * @return the HTTP method that this request has specified + */ + public String getMethod() { + return method; + } + + /** + * This can be used to get the major number from a HTTP version. + * The major version corrosponds to the major protocol type, that + * is the 1 of a HTTP/1.0 version string. Typically the major + * type is 1, by can be 0 for HTTP/0.9 clients. + * + * @return the major version number for the HTTP message + */ + public int getMajor() { + return major; + } + + /** + * This can be used to get the minor number from a HTTP version. + * The minor version corrosponds to the minor protocol type, that + * is the 0 of a HTTP/1.0 version string. This number is typically + * used to determine whether persistent connections are supported. + * + * @return the minor version number for the HTTP message + */ + public int getMinor() { + return minor; + } + + /** + * This can be used to get the date of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public long getDate(String name) { + return header.getDate(name); + } + + /** + * This can be used to get the integer of the first message header + * that has the specified name. This is a convenience method that + * avoids having to deal with parsing the value of the requested + * HTTP message header. This returns -1 if theres no HTTP header + * value for the specified name. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the date as a long from the header value + */ + public int getInteger(String name) { + return header.getInteger(name); + } + + /** + * This method is used to get a List of the names + * for the headers. This will provide the original names for the + * HTTP headers for the message. Modifications to the provided + * list will not affect the header, the list is a simple copy. + * + * @return this returns a list of the names within the header + */ + public List getNames() { + return header.getNames(); + } + + /** + * This method is invoked after the terminal token has been read. + * It is used to process the consumed data and is typically used to + * parse the input such that it can be used by the subclass for + * some useful puropse. This is called only once by the consumer. + */ + @Override + protected void process() { + method(); + target(); + version(); + end(); + headers(); + } + + /** + * This will parse URI target from the first line of the header + * and store the parsed string internally. The target token is + * used to create an Address object which provides + * all the details of the target including the query part. + */ + private void target() { + Token token = new Token(array, pos, 0); + + while(pos < count){ + if(white(array[pos])){ + pos++; + break; + } + token.size++; + pos++; + } + target = token.toString(); + } + + /** + * This will parse HTTP method from the first line of the header + * and store the parsed string internally. The method is used to + * determine what action to take with the request, it also acts + * as a means to determine the semantics of the request. + */ + private void method() { + Token token = new Token(array, pos, 0); + + while(pos < count){ + if(white(array[pos])){ + pos++; + break; + } + token.size++; + pos++; + } + method = token.toString(); + } + + /** + * This will parse HTTP version from the first line of the header + * and store the parsed string internally. The method is used to + * determine what version of HTTP is being used. Typically this + * will be HTTP/1.1 however HTTP/1.0 must be supported and this + * has different connection semantics with regards to pipelines. + */ + protected void version() { + pos += 5; /* "HTTP/" */ + major(); /* "1" */ + pos++; /* "." */ + minor(); /* "1" */ + } + + /** + * This will parse the header from the current offset and convert + * the bytes found into an int as it parses the digits it comes + * accross. This will cease to parse bytes when it encounters a + * non digit byte or the end of the readable bytes. + */ + private void major() { + while(pos < count){ + if(!digit(array[pos])){ + break; + } + major *= 10; + major += array[pos]; + major -= '0'; + pos++; + } + } + + /** + * This will parse the header from the current offset and convert + * the bytes found into an int as it parses the digits it comes + * accross. This will cease to parse bytes when it encounters a + * non digit byte or the end of the readable bytes. + */ + private void minor() { + while(pos < count){ + if(!digit(array[pos])){ + break; + } + minor *= 10; + minor += array[pos]; + minor -= '0'; + pos++; + } + } + + /** + * This is used to determine if a given ISO-8859-1 byte is a digit + * character, between an ISO-8859-1 0 and 9. If it is, this will + * return true otherwise it returns false. + * + * @param octet this is to be checked to see if it is a digit + * + * @return true if the byte is a digit character, false otherwise + */ + protected boolean digit(byte octet) { + return octet >= '0' && octet <= '9'; + } + + /** + * This method returns a CharSequence holding the data + * consumed for the request. A character sequence is returned as it + * can provide a much more efficient means of representing the header + * data by just wrapping the consumed byte array. + * + * @return this returns the characters consumed for the header + */ + public CharSequence getHeader() { + return new Token(array, 0, count); + } + + /** + * This is used to convert the byte range to a string. This + * will use UTF-8 encoding for the string which is compatible + * with the HTTP default header encoding of ISO-8859-1. + * + * @return the encoded string representing the token + */ + public String toString() { + return getHeader().toString(); + } + + /** + * This is a sequence of characters representing the header data + * consumed. Here the internal byte buffer is simply wrapped so + * that it can be a represented as a CharSequence. + * Wrapping the consumed array in this manner ensures that no + * further memory allocation is required. + */ + private static class Token implements CharSequence { + + /** + * This is the array that contains the header bytes. + */ + public byte[] array; + + /** + * This is the number of bytes to use from the array. + */ + public int size; + + /** + * This is the offset in the array the token begins at. + */ + public int off; + + /** + * Constructor for the ByteSequence object. This + * is used to represent the data that has been consumed by + * the header. It acts as a light weight wrapper for the data + * and avoids having to create new strings for each event. + * + * @param array this is the array representing the header + * @param off the starting offset for the token range + * @param size the number of bytes used for the token + */ + private Token(byte[] array, int off, int size) { + this.array = array; + this.size = size; + this.off = off; + } + + /** + * This returns the length of the header in bytes. The length + * includes the request line and all of the control characters + * including the carriage return and line feed at the end of + * the request header. + * + * @return this returns the number of bytes for the header + */ + public int length() { + return size; + } + + /** + * This is used to acquire the character at the specified index. + * Characters returned from this method are simply the bytes + * casted to a character. This may not convert the character + * correctly and a more sensible method should be used. + * + * @param index the index to extract the character from + * + * @return this returns the character found at the index + */ + public char charAt(int index) { + return (char) array[index]; + } + + /** + * This returns a section of characters within the specified + * range. Acquiring a section in this manner is simply done by + * setting a start and end offset within the internal array. + * + * @param start this is the start index to be used + * @param end this is the end index to be used + * + * @return this returns a new sequence within the original + */ + public CharSequence subSequence(int start, int end) { + return new Token(array, start, end - start); + } + + /** + * This is used to create a string from the header bytes. This + * converts the header bytes to a string using a compatible + * encoding. This may produce different results depending on + * the time it is invoked, as the header consumes more data. + * + * @return this returns an encoded version of the header + */ + public String toString() { + return toString("UTF-8"); + } + + /** + * This is used to create a string from the header bytes. This + * converts the header bytes to a string using a compatible + * encoding. This may produce different results depending on + * the time it is invoked, as the header consumes more data. + * + * @param charset this is the encoding to use for the header + * + * @return this returns an encoded version of the header + */ + public String toString(String charset) { + try { + return new String(array, off, size, charset); + } catch(Exception e) { + return null; + } + } + } +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/Segment.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/Segment.java new file mode 100644 index 0000000..915b231 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/Segment.java @@ -0,0 +1,163 @@ +/* + * Segment.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import org.simpleframework.http.ContentDisposition; +import org.simpleframework.http.ContentType; + +import java.util.List; + +/** + * The Segment object represents a collection of header + * values that is followed by a body. This is used to represent the + * header of a multipart upload part. The raw value of each header + * for the part can be acquired using this interface, also the type + * and the disposition of the body can be determined from this. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.Part + */ +public interface Segment { + + /** + * This method is used to determine the type of a part. Typically + * a part is either a text parameter or a file. If this is true + * then the content represented by the associated part is a file. + * + * @return this returns true if the associated part is a file + */ + boolean isFile(); + + /** + * This method is used to acquire the name of the part. Typically + * this is used when the part represents a text parameter rather + * than a file. However, this can also be used with a file part. + * + * @return this returns the name of the associated part + */ + String getName(); + + /** + * This method is used to acquire the file name of the part. This + * is used when the part represents a text parameter rather than + * a file. However, this can also be used with a file part. + * + * @return this returns the file name of the associated part + */ + String getFileName(); + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if there is no HTTP message header. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name); + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if there is no HTTP message header. + * + * @param name the HTTP message header to get the value from + * @param index acquires a specific header value from multiple + * + * @return this returns the value that the HTTP message header + */ + String getValue(String name, int index); + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered array of tokens extracted from the header(s) + */ + List getValues(String name); + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + ContentType getContentType(); + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Disposition header, if there is + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content disposition value if it exists + */ + ContentDisposition getDisposition(); + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Transfer-Encoding header, if there is + * then this will parse that header and return the first token in + * the comma separated list of values, which is the primary value. + * + * @return this returns the transfer encoding value if it exists + */ + String getTransferEncoding(); + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return the content length, or -1 if it cannot be determined + */ + long getContentLength(); +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java new file mode 100644 index 0000000..5c994ce --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java @@ -0,0 +1,750 @@ +/* + * SegmentConsumer.java February 2007 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import static org.simpleframework.http.Protocol.ACCEPT_LANGUAGE; +import static org.simpleframework.http.Protocol.CONTENT_DISPOSITION; +import static org.simpleframework.http.Protocol.CONTENT_LENGTH; +import static org.simpleframework.http.Protocol.CONTENT_TYPE; +import static org.simpleframework.http.Protocol.COOKIE; +import static org.simpleframework.http.Protocol.EXPECT; +import static org.simpleframework.http.Protocol.TRANSFER_ENCODING; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.simpleframework.http.ContentDisposition; +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.parse.ContentDispositionParser; +import org.simpleframework.http.parse.ContentTypeParser; +import org.simpleframework.http.parse.CookieParser; +import org.simpleframework.http.parse.LanguageParser; + +/** + * The SegmentConsumer object provides a consumer that is + * used to consume a HTTP header. This will read all headers within a + * HTTP header message until the carriage return line feed empty line + * is encountered. Once all headers are consumed they are available + * using the case insensitive header name. This will remove leading + * and trailing whitespace from the names and values parsed. + * + * @author Niall Gallagher + */ +public class SegmentConsumer extends ArrayConsumer implements Segment { + + /** + * This is the terminal carriage return and line feed end line. + */ + private static final byte[] TERMINAL = { 13, 10, 13, 10 }; + + /** + * This is used to represent the content disposition header. + */ + protected ContentDisposition disposition; + + /** + * This is used to parse the languages accepted in the request. + */ + protected LanguageParser language; + + /** + * This is used to parse the cookie headers that are consumed. + */ + protected CookieParser cookies; + + /** + * This is used to store all consumed headers by the header name. + */ + protected MessageHeader header; + + /** + * This is used to parse the content type header consumed. + */ + protected ContentType type; + + /** + * This represents the transfer encoding value of the body. + */ + protected String encoding; + + /** + * During parsing this is used to store the parsed header name, + */ + protected String name; + + /** + * During parsing this is used to store the parsed header value. + */ + protected String value; + + /** + * This is used to determine if there is a continue expected. + */ + protected boolean expect; + + /** + * Represents the length of the body from the content length. + */ + protected long length; + + /** + * This represents the length limit of the HTTP header cosumed. + */ + protected long limit; + + /** + * This is used to track the read offset within the header. + */ + protected int pos; + + /** + * This is used to track how much of the terminal is read. + */ + protected int scan; + + /** + * Constructor for the SegmentConsumer object. This + * is used to create a segment consumer used to consume and parse + * a HTTP message header. This delegates parsing of headers if + * they represent special headers, like content type or cookies. + */ + public SegmentConsumer() { + this(1048576); + } + + /** + * Constructor for the SegmentConsumer object. This + * is used to create a segment consumer used to consume and parse + * a HTTP message header. This delegates parsing of headers if + * they represent special headers, like content type or cookies. + * + * @param limit this is the length limit for a HTTP header + */ + public SegmentConsumer(int limit) { + this.language = new LanguageParser(); + this.cookies = new CookieParser(); + this.header = new MessageHeader(); + this.limit = limit; + this.length = -1; + } + + /** + * This method is used to determine the type of a part. Typically + * a part is either a text parameter or a file. If this is true + * then the content represented by the associated part is a file. + * + * @return this returns true if the associated part is a file + */ + public boolean isFile() { + if(disposition == null) { + return false; + } + return disposition.isFile(); + } + + /** + * This method is used to acquire the name of the part. Typically + * this is used when the part represents a text parameter rather + * than a file. However, this can also be used with a file part. + * + * @return this returns the name of the associated part + */ + public String getName() { + if(disposition == null) { + return null; + } + return disposition.getName(); + } + + /** + * This method is used to acquire the file name of the part. This + * is used when the part represents a text parameter rather than + * a file. However, this can also be used with a file part. + * + * @return this returns the file name of the associated part + */ + public String getFileName() { + if(disposition == null) { + return null; + } + return disposition.getFileName(); + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Type header, if there is then + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content type value if it exists + */ + public ContentType getContentType() { + return type; + } + + /** + * This is a convenience method that can be used to determine + * the length of the message body. This will determine if there + * is a Content-Length header, if it does then the + * length can be determined, if not then this returns -1. + * + * @return the content length, or -1 if it cannot be determined + */ + public long getContentLength() { + return length; + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Transfer-Encoding header, if there is + * then this will parse that header and return the first token in + * the comma separated list of values, which is the primary value. + * + * @return this returns the transfer encoding value if it exists + */ + public String getTransferEncoding() { + return encoding; + } + + /** + * This is a convenience method that can be used to determine the + * content type of the message body. This will determine whether + * there is a Content-Disposition header, if there is + * this will parse that header and represent it as a typed object + * which will expose the various parts of the HTTP header. + * + * @return this returns the content disposition value if it exists + */ + public ContentDisposition getDisposition() { + return disposition; + } + + /** + * This is used to acquire the locales from the request header. The + * locales are provided in the Accept-Language header. + * This provides an indication as to the languages that the client + * accepts. It provides the locales in preference order. + * + * @return this returns the locales preferred by the client + */ + public List getLocales() { + if(language != null) { + return language.list(); + } + return Collections.emptyList(); + } + + /** + * This can be used to get the values of HTTP message headers + * that have the specified name. This is a convenience method that + * will present that values as tokens extracted from the header. + * This has obvious performance benefits as it avoids having to + * deal with substring and trim calls. + *

+ * The tokens returned by this method are ordered according to + * there HTTP quality values, or "q" values, see RFC 2616 section + * 3.9. This also strips out the quality parameter from tokens + * returned. So "image/html; q=0.9" results in "image/html". If + * there are no "q" values present then order is by appearance. + *

+ * The result from this is either the trimmed header value, that + * is, the header value with no leading or trailing whitespace + * or an array of trimmed tokens ordered with the most preferred + * in the lower indexes, so index 0 is has highest preference. + * + * @param name the name of the headers that are to be retrieved + * + * @return ordered array of tokens extracted from the header(s) + */ + public List getValues(String name) { + return header.getValues(name); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if theres no HTTP message header. + * + * @param name the HTTP message header to get the value from + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name) { + return header.getValue(name); + } + + /** + * This can be used to get the value of the first message header + * that has the specified name. The value provided from this will + * be trimmed so there is no need to modify the value, also if + * the header name specified refers to a comma separated list of + * values the value returned is the first value in that list. + * This returns null if there is no HTTP message header. + * + * @param name the HTTP message header to get the value from + * @param index acquires a specific header value from multiple + * + * @return this returns the value that the HTTP message header + */ + public String getValue(String name, int index) { + return header.getValue(name, index); + } + + /** + * This is used to determine if the header represents one that + * requires the HTTP/1.1 continue expectation. If the request + * does require this expectation then it should be send the + * 100 status code which prompts delivery of the message body. + * + * @return this returns true if a continue expectation exists + */ + public boolean isExpectContinue() { + return expect; + } + + /** + * This method is used to add an additional chunk size to the + * internal array. Resizing of the internal array is required as + * the consumed bytes may exceed the initial size of the array. + * In such a scenario the array is expanded the chunk size. + * + * @param size this is the minimum size to expand the array to + */ + @Override + protected void resize(int size) throws IOException { + if(size > limit) { + throw new IOException("Header has exceeded maximum size"); + } + super.resize(size); + } + + /** + * This is used to process the headers when the terminal token + * has been fully read from the consumed bytes. Processing will + * extract all headers from the HTTP header message and further + * parse those values if required. + */ + @Override + protected void process() throws IOException { + headers(); + } + + /** + * This is used to parse the headers from the consumed HTTP header + * and add them to the segment. Once added they are available via + * the header name in a case insensitive manner. If the header has + * a special value, that is, if further information is required it + * will be extracted and exposed in the segment interface. + */ + protected void headers() { + while(pos < count) { + header(); + add(name, value); + } + } + + /** + * This is used to parse a header from the consumed HTTP message + * and add them to the segment. Once added it is available via + * the header name in a case insensitive manner. If the header has + * a special value, that is, if further information is required it + * will be extracted and exposed in the segment interface. + */ + private void header() { + adjust(); + name(); + adjust(); + value(); + end(); + } + + /** + * This is used to add the name and value specified as a special + * header within the segment. Special headers are those where + * there are values of interest to the segment. For instance the + * Content-Length, Content-Type, and Cookie headers are parsed + * using an external parser to extract the values. + * + * @param name this is the name of the header to be added + * @param value this is the value of the header to be added + */ + protected void add(String name, String value) { + if(equal(ACCEPT_LANGUAGE, name)) { + language(value); + }else if(equal(CONTENT_LENGTH, name)) { + length(value); + } else if(equal(CONTENT_TYPE, name)) { + type(value); + } else if(equal(CONTENT_DISPOSITION, name)) { + disposition(value); + } else if(equal(TRANSFER_ENCODING, name)) { + encoding(value); + } else if(equal(EXPECT, name)) { + expect(value); + } else if(equal(COOKIE, name)) { + cookie(value); + } + header.addValue(name, value); + } + + /** + * This is used to determine if the expect continue header is + * present and thus there is a requirement to send the continue + * status before the client sends the request body. This will + * basically assume the expectation is always continue. + * + * @param value the value in the expect continue header + */ + protected void expect(String value) { + expect = true; + } + + /** + * This will accept any cookie header and parse it such that all + * cookies within it are converted to Cookie objects + * and made available as typed objects. If the value can not be + * parsed this will not add the cookie value. + * + * @param value this is the value of the cookie to be parsed + */ + protected void cookie(String value) { + cookies.parse(value); + + for(Cookie cookie : cookies) { + header.setCookie(cookie); + } + } + + /** + * This is used to parse the Accept-Language header + * value. This allows the locales the client is interested in to + * be provided in preference order and allows the client do alter + * and response based on the locale the client has provided. + * + * @param value this is the value that is to be parsed + */ + protected void language(String value) { + language = new LanguageParser(value); + } + + /** + * This is used to parse the content type header header so that + * the MIME type is available to the segment. This provides an + * instance of the ContentType object to represent + * the content type header, which exposes the charset value. + * + * @param value this is the content type value to parse + */ + protected void type(String value) { + type = new ContentTypeParser(value); + } + + /** + * This is used to parse the content disposition header header so + * that the MIME type is available to the segment. This provides + * an instance of the Disposition object to represent + * the content disposition, this exposes the upload type. + * + * @param value this is the content type value to parse + */ + protected void disposition(String value) { + disposition = new ContentDispositionParser(value); + } + + /** + * This is used to store the transfer encoding header value. This + * is used to determine the encoding of the body this segment + * represents. Typically this will be the chunked encoding. + * + * @param value this is the value representing the encoding + */ + protected void encoding(String value) { + encoding = value; + } + + /** + * This is used to parse a provided header value for the content + * length. If the string provided is not an integer value this will + * throw a number format exception, by default length is -1. + * + * @param value this is the header value of the content length + */ + protected void length(String value) { + try { + length = Long.parseLong(value); + }catch(Exception e) { + length = -1; + } + } + + /** + * This updates the token for the header name. The name is parsed + * according to the presence of a colon ':'. Once a colon character + * is encountered then this header name is considered to be read + * from the buffer and is used to key the value after the colon. + */ + private void name() { + Token token = new Token(pos, 0); + + while(pos < count){ + if(array[pos] == ':') { + pos++; + break; + } + token.size++; + pos++; + } + name = token.text(); + } + + + /** + * This is used to parse the HTTP header value. This will parse it + * in such a way that the line can be folded over several lines + * see RFC 2616 for the syntax of a folded line. The folded line + * is basically a way to wrap a single HTTP header into several + * lines using a tab at the start of the following line to indicate + * that the header flows onto the next line. + */ + private void value() { + Token token = new Token(pos, 0); + + scan: for(int mark = 0; pos < count;){ + if(terminal(array[pos])) { /* CR or LF */ + for(int i = 0; pos < count; i++){ + if(array[pos++] == 10) { /* skip the LF */ + if(pos < array.length) { + if(space(array[pos])) { + mark += i + 1; /* account for bytes examined */ + break; /* folding line */ + } + } + break scan; /* not a folding line */ + } + } + } else { + if(!space(array[pos])){ + token.size = ++mark; + } else { + mark++; + } + pos++; + } + } + value = token.text(); + } + + /** + * This will update the offset variable so that the next read will + * be of a non whitespace character. According to RFC 2616 a white + * space character is a tab or a space. This will remove multiple + * occurrences of whitespace characters until an non-whitespace + * character is encountered. + */ + protected void adjust() { + while(pos < count) { + if(!space(array[pos])){ + break; + } + pos++; + } + } + + /** + * This will update the offset variable so that the next read will + * be a non whitespace character or terminal character. According to + * RFC 2616 a white space character is a tab or a space. This will + * remove multiple occurrences of whitespace characters until an + * non-whitespace character or a non-terminal is encountered. This + * is basically used to follow through to the end of a header line. + */ + protected void end() { + while(pos < count) { + if(!white(array[pos])){ + break; + } + pos++; + } + } + + /** + * This method is used to scan for the terminal token. It searches + * for the token and returns the number of bytes in the buffer + * after the terminal token. Returning the excess bytes allows the + * consumer to reset the bytes within the consumer object. + * + * @return this returns the number of excess bytes consumed + */ + @Override + protected int scan() { + int length = count; + + while(pos < count) { + if(array[pos++] != TERMINAL[scan++]) { + scan = 0; + } + if(scan == TERMINAL.length) { + done = true; + count = pos; + pos = 0; + return length - count; + } + } + return 0; + } + + /** + * This is used to determine if two header names are equal, this is + * done to ensure that the case insensitivity of HTTP header names + * is observed. Special headers are processed using this consumer + * and this is used to ensure the correct header is always matched. + * + * @param name this is the name to compare the parsed token with + * @param token this is the header name token to examine + * + * @return true of the header name token is equal to the name + */ + protected boolean equal(String name, String token) { + return name.equalsIgnoreCase(token); + } + + /** + * This identifies a given ISO-8859-1 byte as a space character. A + * space is either a space or a tab character in ISO-8859-1. + * + * @param octet the byte to determine whether it is a space + * + * @return true if it is a space character, false otherwise + */ + protected boolean space(byte octet) { + return octet == ' ' || octet == '\t'; + } + + /** + * This determines if an ISO-8859-1 byte is a terminal character. A + * terminal character is a carriage return or a line feed character. + * + * @param octet the byte to determine whether it is a terminal + * + * @return true if it is a terminal character, false otherwise + */ + protected boolean terminal(byte octet){ + return octet == 13 || octet == 10; + } + + + /** + * This is used to determine if a given ISO-8859-1 byte is a white + * space character, such as a tab or space or a terminal character, + * such as a carriage return or a new line. If it is, this will + * return true otherwise it returns false. + * + * @param octet this is to be checked to see if it is a space + * + * @return true if the byte is a space character, false otherwise + */ + protected boolean white(byte octet) { + switch(octet) { + case ' ': case '\r': + case '\n': case '\t': + return true; + default: + return false; + } + } + + /** + * This is used to provide a string representation of the header + * read. Providing a string representation of the header is used + * so that on debugging the contents of the delivered header can + * be inspected in order to determine a cause of error. + * + * @return this returns a string representation of the header + */ + @Override + public String toString() { + return new String(array, 0, count); + } + + /** + * This is used to track the boundaries of a token so that it can + * be converted in to a usable string. This will track the length + * and offset within the consumed array of the token. When the + * token is to be used it can be converted in to a string. + */ + private class Token { + + /** + * This is used to track the number of bytes within the array. + */ + public int size; + + /** + * This is used to mark the start offset within the array. + */ + public int off; + + /** + * Constructor for the Token object. This is used + * to create a new token to track the range of bytes that will + * be used to create a string representing the parsed value. + * + * @param off the starting offset for the token range + * @param size the number of bytes used for the token + */ + public Token(int off, int size) { + this.off = off; + this.size = size; + } + + /** + * This is used to convert the byte range to a string. This + * will use UTF-8 encoding for the string which is compatible + * with the HTTP default header encoding of ISO-8859-1. + * + * @return the encoded string representing the token + */ + public String text() { + return text("UTF-8"); + } + + /** + * This is used to convert the byte range to a string. This + * will use specified encoding, if that encoding is not + * supported then this will return null for the token value. + * + * @return the encoded string representing the token + */ + public String text(String charset) { + try { + return new String(array, off, size, charset); + } catch(IOException e) { + return null; + } + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/TokenConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/TokenConsumer.java new file mode 100644 index 0000000..2b48b7c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/TokenConsumer.java @@ -0,0 +1,113 @@ +/* + * TokenConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.Buffer; + +/** + * The TokenConsumer object is used to consume a token + * from the cursor. Once the token has been consumed the consumer + * is finished and the contents of the consumed token is appended + * to an allocated buffer so that it can be extracted. + * + * @author Niall Gallagher + */ +class TokenConsumer extends ArrayConsumer { + + /** + * This is used to allocate a buffer to append the contents. + */ + private Allocator allocator; + + /** + * This is used to append the contents of consumed token. + */ + private Buffer buffer; + + /** + * This is the token that is to be consumed from the cursor. + */ + private byte[] token; + + /** + * This tracks the number of bytes that are read from the token. + */ + private int seek; + + /** + * This is the length of the token that is to be consumed. + */ + private int length; + + /** + * The TokenConsumer object is used to read a token + * from the cursor. This tracks the bytes read from the cursor, + * when it has fully read the token bytes correctly it will + * finish and append the consumed bytes to a buffer. + * + * @param allocator the allocator used to create a buffer + * @param token this is the token that is to be consumed + */ + public TokenConsumer(Allocator allocator, byte[] token) { + this.allocator = allocator; + this.length = token.length; + this.token = token; + this.chunk = length; + } + + /** + * This is used to append the consumed bytes to a created buffer + * so that it can be used when he is finished. This allows the + * contents to be read from an input stream or as a string. + */ + @Override + protected void process() throws IOException { + if(buffer == null) { + buffer = allocator.allocate(length); + } + buffer.append(token); + } + + /** + * This is used to scan the token from the array. Once the bytes + * have been read from the consumed bytes this will return the + * number of bytes that need to be reset within the buffer. + * + * @return this returns the number of bytes to be reset + */ + @Override + protected int scan() throws IOException { + int size = token.length; + int pos = 0; + + if(count >= size) { + while(seek < count) { + if(array[seek++] != token[pos++]) { + throw new IOException("Invalid token"); + } + } + done = true; + return count - seek; + } + return 0; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/UpdateConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/UpdateConsumer.java new file mode 100644 index 0000000..5d514c9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/UpdateConsumer.java @@ -0,0 +1,143 @@ +/* + * UpdateConsumer.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.transport.ByteCursor; + +/** + * The UpdateConsumer object is used to create a consumer + * that is used to consume and process large bodies. Typically a large + * body will be one that is delivered as part of a multipart upload + * or as a large form POST. The task of the large consumer is to + * consume all the bytes for the body, and reset the cursor after the + * last byte that has been send with the body. This ensures that the + * next character read from the cursor is the first character of a + * HTTP header within the pipeline. + * + * @author Niall Gallagher + */ +public abstract class UpdateConsumer implements BodyConsumer { + + /** + * This is an external array used to copy data between buffers. + */ + protected byte[] array; + + /** + * This is used to determine whether the consumer has finished. + */ + protected boolean finished; + + /** + * Constructor for the UpdateConsumer object. This is + * used to create a consumer with a one kilobyte buffer used to + * read the contents from the cursor and transfer it to the buffer. + */ + protected UpdateConsumer() { + this(2048); + } + + /** + * Constructor for the UpdateConsumer object. This is + * used to create a consumer with a variable size buffer used to + * read the contents from the cursor and transfer it to the buffer. + * + * @param chunk this is the size of the buffer used to read bytes + */ + protected UpdateConsumer(int chunk) { + this.array = new byte[chunk]; + } + + /** + * This is used to determine whether the consumer has finished + * reading. The consumer is considered finished if it has read a + * terminal token or if it has exhausted the stream and can not + * read any more. Once finished the consumed bytes can be parsed. + * + * @return true if the consumer has finished reading its content + */ + public boolean isFinished() { + return finished; + } + + /** + * This method is used to consume bytes from the provided cursor. + * Consuming of bytes from the cursor should be done in such a + * way that it does not block. So typically only the number of + * ready bytes in the ByteCursor object should be + * read. If there are no ready bytes then this will return. + * + * @param cursor used to consume the bytes from the HTTP pipeline + */ + public void consume(ByteCursor cursor) throws IOException { + int ready = cursor.ready(); + + while(ready > 0) { + int size = Math.min(ready, array.length); + int count = cursor.read(array, 0, size); + + if(count > 0) { + int reset = update(array, 0, count); + + if(reset > 0) { + cursor.reset(reset); + } + } + if(finished) { + commit(cursor); + break; + } + ready = cursor.ready(); + } + } + + /** + * This method can be used to commit the consumer when all data + * has been consumed. It is often used to push back some data on + * to the cursor so that the next consumer can read valid tokens + * from the stream of bytes. If no commit is required then the + * default implementation of this will simply return quietly. + * + * @param cursor this is the cursor used by this consumer + */ + protected void commit(ByteCursor cursor) throws IOException { + if(!finished) { + throw new IOException("Consumer not finished"); + } + } + + /** + * This is used to process the bytes that have been read from the + * cursor. Depending on the delimiter used this knows when the + * end of the body has been encountered. If the end is encountered + * this method must return the number of bytes overflow, and set + * the state of the consumer to finished. + * + * @param array this is a chunk read from the cursor + * @param off this is the offset within the array the chunk starts + * @param count this is the number of bytes within the array + * + * @return this returns the number of bytes overflow that is read + */ + protected abstract int update(byte[] array, int off, int count) throws IOException; +} + + diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/AddressParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/AddressParser.java new file mode 100644 index 0000000..06d5509 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/AddressParser.java @@ -0,0 +1,1347 @@ +/* + * AddressParser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import org.simpleframework.common.KeyMap; +import org.simpleframework.common.parse.Parser; +import org.simpleframework.http.Address; +import org.simpleframework.http.Path; +import org.simpleframework.http.Query; + +/** + * This parser is used to parse uniform resource identifiers. + * The uniform resource identifier syntax is given in RFC 2396. + * This parser can parse relative and absolute URI's. The + * uniform resource identifier syntax that this parser will + * parse are based on the generic web based URL similar to + * the syntax represented in RFC 2616 section 3.2.2. The syntax + * used to parse this URI is a modified version of RFC 2396 + *

+ *
+ *    URI         = (absoluteURI | relativeURI)
+ *    absoluteURI = scheme ":" ("//" netpath | relativeURI)
+ *    relativeURI = path ["?" querypart]
+ *    netpath     = domain [":" port] relativeURI
+ *    path        = *("/" segment)
+ *    segment     = *pchar *( ";" param )
+ *
+ * 
+ * This implements the Address interface and provides + * methods that access the various parts of the URI. The parameters + * in the path segments of the uniform resource identifier are + * stored in name value pairs. If parameter names are not unique + * across the path segments then only the deepest parameter will be + * stored from the path segment. For example if the URI represented + * was http://domain/path1;x=y/path2;x=z the value for + * the parameter named x would be z. + *

+ * This will normalize the path part of the uniform resource + * identifier. A normalized path is one that contains no back + * references like "./" and "../". The normalized path will not + * contain the path parameters. + *

+ * The setPath method is used to reset the path this + * uniform resource identifier has, it also resets the parameters. + * The parameters are extracted from the new path given. + * + * @author Niall Gallagher + */ +public class AddressParser extends Parser implements Address { + + /** + * Parameters are stored so that the can be viewed. + */ + private ParameterMap param; + + /** + * This is the path used to represent the address path. + */ + private Path normal; + + /** + * This contains the query parameters for the address. + */ + private Query data; + + /** + * Used to track the characters that form the path. + */ + private Token path; + + /** + * Used to track the characters that form the domain. + */ + private Token domain; + + /** + * Used to track the characters that form the query. + */ + private Token query; + + /** + * Used to track the name characters of a parameter. + */ + private Token name; + + /** + * Used to track the value characters of a parameter. + */ + private Token value; + + /** + * References the scheme that this URI contains. + */ + private Token scheme; + + /** + * Contains the port number if it was specified. + */ + private int port; + + /** + * Default constructor will create a AddressParser + * that contains no specifics. The instance will return + * null for all the get methods. The parsers + * get methods are populated by using the parse + * method. + */ + public AddressParser(){ + this.param = new ParameterMap(); + this.path = new Token(); + this.domain = new Token(); + this.query = new Token(); + this.scheme = new Token(); + this.name = new Token(); + this.value = new Token(); + } + + /** + * This is primarily a convenience constructor. This will parse + * the String given to extract the specifics. This + * could be achieved by calling the default no-arg constructor + * and then using the instance to invoke the parse + * method on that String to extract the parts. + * + * @param text a String containing a URI value + */ + public AddressParser(String text){ + this(); + parse(text); + } + + /** + * This allows the scheme of the URL given to be returned. + * If the URI does not contain a scheme then this will + * return null. The scheme of the URI is the part that + * specifies the type of protocol that the URI is used + * for, an example gopher://domain/path is + * a URI that is intended for the gopher protocol. The + * scheme is the string gopher. + * + * @return this returns the scheme tag for the URI if + * there is one specified for it + */ + public String getScheme(){ + return scheme.toString(); + } + + /** + * This is used to retrieve the domain of this URI. The + * domain part in the URI is an optional part, an example + * http://domain/path?querypart. This will + * return the value of the domain part. If there is no + * domain part then this will return null otherwise the + * domain value found in the uniform resource identifier. + * + * @return the domain part of this uniform resource + * identifier this represents + */ + public String getDomain(){ + return domain.toString(); + } + + /** + * This is used to retrieve the path of this URI. The path part + * is the most fundamental part of the URI. This will return + * the value of the path. If there is no path part then this + * will return / to indicate the root. + *

+ * The Path object returned by this will contain + * no path parameters. The path parameters are available using + * the Address methods. The reason that this does not + * contain any of the path parameters is so that if the path is + * needed to be converted into an OS specific path then the path + * parameters will not need to be separately parsed out. + * + * @return the path that this URI contains, this value will not + * contain any back references such as "./" and "../" or any + * path parameters + */ + public Path getPath(){ + if(normal == null) { + String text = path.toString(); + + if(text == null) { + normal = new PathParser("/"); + } + if(normal == null){ + normal = new PathParser(text); + } + } + return normal; + } + + /** + * This is used to retrieve the query of this URI. The query part + * in the URI is an optional part. This will return the value + * of the query part. If there is no query part then this will + * return an empty Query object. The query is + * an optional member of a URI and comes after the path part, it + * is preceded by a question mark, ? character. + * For example the following URI contains query for + * its query part, http://host:port/path?query. + *

+ * This returns a org.simpleframework.http.Query + * object that can be used to interact directly with the query + * values. The Query object is a read-only interface + * to the query parameters, and so will not affect the URI. + * + * @return a Query object for the query part + */ + public Query getQuery(){ + if(data == null) { + String text = query.toString(); + + if(text == null) { + data = new QueryParser(); + } + if(data == null){ + data = new QueryParser(text); + } + } + return data; + } + + /** + * This is used to retrieve the port of the uniform resource + * identifier. The port part in this is an optional part, an + * example http://host:port/path?querypart. This + * will return the value of the port. If there is no port then + * this will return -1 because this represents + * an impossible uniform resource identifier port. The port + * is an optional part. + * + * @return this returns the port of the uniform resource + * identifier + */ + public int getPort(){ + return port <= 0? -1 : port; + } + + /** + * This extracts the parameter values from the uniform resource + * identifier represented by this object. The parameters that a + * uniform resource identifier contains are embedded in the path + * part of the URI. If the path contains no parameters then this + * will return an empty Map instance. + *

+ * This will produce unique name and value parameters. Thus if the + * URI contains several path segments with similar parameter names + * this will return the deepest parameter. For example if the URI + * represented was http://domain/path1;x=y/path2;x=z + * the value for the parameter named x would be + * z. + * + * @return this will return the parameter names found in the URI + */ + public KeyMap getParameters(){ + return param; + } + + /** + * This allows the scheme for the URI to be specified. + * If the URI does not contain a scheme then this will + * attach the scheme and the :// identifier + * to ensure that the Address.toString will + * produce the correct syntax. + *

+ * Caution must be taken to ensure that the port and + * the scheme are consistent. So if the original URI + * was http://domain:80/path and the scheme + * was changed to ftp the port number that + * remains is the standard HTTP port not the FTP port. + * + * @param value this specifies the protocol this URI + * is intended for + */ + public void setScheme(String value){ + scheme.value = value; + } + + /** + * This will set the domain to whatever value is in the + * string parameter. If the string is null then this URI + * objects toString method will not contain + * the domain. The result of the toString + * method will be /path/path?query. If the + * path is non-null this URI will contain the path. + * + * @param value this will be the new domain of this + * uniform resource identifier, if it is not null + */ + public void setDomain(String value){ + path.toString(); + query.toString(); + scheme.toString(); + domain.clear(); + parseDomain(value); + } + + /** + * This will set the domain to whatever value is in the + * string parameter. If the string is null then this URI + * objects toString method will not contain + * the domain. The result of the toString + * method will be /path/path?query. If the + * path is non-null this URI will contain the path. + * + * @param value this will be the new domain of this + * uniform resource identifier, if it is not null + */ + private void parseDomain(String value){ + count = value.length(); + ensureCapacity(count); + value.getChars(0, count, buf, 0); + normal = null; + off = 0; + hostPort(); + } + + /** + * This will set the port to whatever value it is given. If + * the value is 0 or less then the toString will + * will not contain the optional port. If port number is above + * 0 then the toString method will produce a URI + * like http://host:123/path but only if there is + * a valid domain. + * + * @param port the port value that this URI is to have + */ + public void setPort(int port) { + this.port = port; + } + + /** + * This will set the path to whatever value it is given. If the + * value is null then this Address.toString method will + * not contain the path, that is if path is null then it will be + * interpreted as /. + *

+ * This will reset the parameters this URI has. If the value + * given to this method has embedded parameters these will form + * the parameters of this URI. The value given may not be the + * same value that the getPath produces. The path + * will have all back references and parameters stripped. + * + * @param text the path that this URI is to be set with + */ + public void setPath(String text) { + if(!text.startsWith("/")){ + text = "/" + text; + } + domain.toString(); + query.toString(); + scheme.toString(); + param.clear(); + path.clear(); + parsePath(text); /*extract params*/ + } + + /** + * This will set the path to whatever value it is given. If the + * value is null then this Address.toString method + * will not contain the path, that is if path is null then it will + * be interpreted as /. + *

+ * This will reset the parameters this URI has. If the value + * given to this method has embedded parameters these will form + * the parameters of this URI. The value given may not be the + * same value that the getPath produces. The path + * will have all back references and parameters stripped. + * + * @param path the path that this URI is to be set with + */ + public void setPath(Path path) { + if(path != null){ + normal = path; + }else { + setPath("/"); + } + } + + /** + * This is used to parse the path given with the setPath + * method. The path contains name and value pairs. These parameters + * are embedded into the path segments using a semicolon character, + * ';'. Since the parameters to not form part of the actual path + * mapping they are removed from the path and stored. Each parameter + * can then be extracted from this parser using the methods provided + * by the Address interface. + * + * @param path this is the path that is to be parsed and have the + * parameter values extracted + */ + private void parsePath(String path){ + count = path.length(); + ensureCapacity(count); + path.getChars(0, count, buf, 0); + normal = null; + off = 0; + path(); + } + + /** + * This will set the query to whatever value it is given. If the + * value is null then this Address.toString method + * will not contain the query. If the query was abc + * then the toString method would produce a string + * like http://host:port/path?abc. If the query is + * null this URI would have no query part. The query must not + * contain the ? character. + * + * @param value the query that this uniform resource identifier + * is to be set to if it is non-null + */ + public void setQuery(String value) { + query.value = value; + data = null; + } + + /** + * This will set the query to whatever value it is given. If the + * value is null then this Address.toString method + * will not contain the query. If the Query.toString + * returns null then the query will be empty. This is basically + * the setQuery(String) method with the string value + * from the issued Query.toString method. + * + * @param query a Query object that contains + * the name value parameters for the query + */ + public void setQuery(Query query) { + if(value != null) { + data = query; + }else { + setQuery(""); + } + } + + /** + * This will check to see what type of URI this is if it is an + * absoluteURI or a relativeURI. To + * see the definition of a URI see RFC 2616 for the definition + * of a URL and for more specifics see RFC 2396 for the + * expressions. + */ + protected void parse(){ + if(count > 0){ + if(buf[0] == '/'){ + relativeURI(); + }else{ + absoluteURI(); + } + } + } + + /** + * This will empty each tokens cache. A tokens cache is used + * to represent a token once the token's toString + * method has been called. Thus when the toString + * method is called then the token depends on the value of the + * cache alone in further calls to toString. + * However if a URI has just been parsed and that method has + * not been invoked then the cache is created from the buf if + * its length is greater than zero. + */ + protected void init(){ + param.clear(); + domain.clear(); + path.clear(); + query.clear(); + scheme.clear(); + off =port = 0; + normal = null; + data = null; + } + + /** + * This is a specific definition of a type of URI. An absolute + * URI is a URI that contains a host and port. It is the most + * frequently used type of URI. This will define the host and + * the optional port part. As well as the relative URI part. + * This uses a simpler syntax than the one specified in RFC 2396 + *

+    *
+    *    absoluteURI = scheme ":" ("//" netpath | relativeURI)
+    *    relativeURI = path ["?" querypart]
+    *    netpath     = domain [":" port] relativeURI
+    *    path        = *("/" segment)
+    *    segment     = *pchar *( ";" param )
+    *
+    * 
+ * This syntax is sufficient to handle HTTP style URI's as well + * as GOPHER and FTP and various other 'simple' schemes. See + * RFC 2396 for the syntax of an absoluteURI. + */ + private void absoluteURI(){ + scheme(); + netPath(); + } + + /** + * This will check to see if there is a scheme in the URI. If + * there is a scheme found in the URI this returns true and + * removes that scheme tag of the form "ftp:" or "http:" + * or whatever the protocol scheme tag may be for the URI. + *

+ * The syntax for the scheme is given in RFC 2396 as follows + *

+    *
+    *    scheme = alpha *( alpha | digit | "+" | "-" | "." )
+    *
+    * 
+ * This will however also skips the "://" from the tag + * so of the URI was gopher://domain/path then + * the URI would be domain/path afterwards. + */ + private void scheme(){ + int mark = off; + int pos = off; + + if(alpha(buf[off])){ + while(off < count){ + char next = buf[off++]; + + if(schemeChar(next)){ + pos++; + }else if(next == ':'){ + if(!skip("//")) { + off = mark; + pos = mark; + } + break; + }else{ + off = mark; + pos = mark; + break; + } + } + scheme.len = pos - mark; + scheme.off = mark; + } + } + + /** + * This method is used to assist the scheme method. This will + * check to see if the type of the character is the same as + * those described in RFC 2396 for a scheme character. The + * scheme tag can contain an alphanumeric of the following + * "+", "-", ".". + * + * @param c this is the character that is being checked + * + * @return this returns true if the character is a valid + * scheme character + */ + private boolean schemeChar(char c){ + switch(c){ + case '+': case '-': + case '.': + return true; + default: + return alphanum(c); + } + } + + /** + * The network path is the path that contains the network + * address of the host that this URI is targeted at. This + * will parse the domain name of the host and also a port + * number before parsing a relativeURI + *
+    *
+    *    netpath     = domain [":" port] relativeURI
+    *
+    * 
+ * This syntax is modified from the URI specification on + * RFC 2396. + */ + private void netPath(){ + hostPort(); + relativeURI(); + } + + /** + * This is used to extract the host and port combination. + * Typically a URI will not explicitly specify a port, however + * if there is a semicolon at the end of the domain it should + * be interpreted as the port part of the URI. + */ + private void hostPort() { + domain(); + if(skip(":")){ + port(); + } + } + + /** + * This is a specific definition of a type of URI. A relative + * URI is a URI that contains no host or port. It is basically + * the resource within the host. This will extract the path and + * the optional query part of the URI. Rfc2396 has the proper + * definition of a relativeURI. + */ + private void relativeURI(){ + path(); + if(skip("?")){ + query(); + } + } + + /** + * This is used to extract the optional port from a given URI. + * This will read a sequence of digit characters and convert + * the String of digit characters into a decimal + * number. The digits will be added to the port variable. If + * there is no port number this will not update the read offset. + */ + private void port() { + while(off < count){ + if(!digit(buf[off])){ + break; + } + port *= 10; + port += buf[off]; + port -= '0'; + off++; + } + } + + /** + * This is used to extract the domain from the given URI. This + * will firstly initialize the token object that represents the + * domain. This allows the token's toString method to + * return the extracted value of the token rather than getting + * confused with previous values set by a previous parse method. + *

+ * This uses the following delimiters to determine the end of the + * domain ?,: and /. This + * ensures that the read offset does not go out of bounds and + * consequently throw an IndexOutOfBoundsException. + */ + private void domain(){ + int mark = off; + + loop: while(off < count){ + switch(buf[off]){ + case '/': case ':': + case '?': + break loop; + default: + off++; + } + } + domain.len = off - mark; + domain.off = mark; + } + + /** + * This is used to extract the segments from the given URI. This + * will firstly initialize the token object that represents the + * path. This allows the token's toString method to + * return the extracted value of the token rather than getting + * confused with previous values set by a previous parse method. + *

+ * This is slightly different from RFC 2396 in that it defines a + * pchar as the RFC 2396 definition of a pchar without the escaped + * chars. So this method has to ensure that no escaped chars go + * unchecked. This ensures that the read offset does not go out + * of bounds and throw an IndexOutOfBoundsException. + */ + private void path(){ + int mark = off; + int pos = off; + + while(skip("/")) { + buf[pos++] = '/'; + + while(off < count){ + if(buf[off]==';'){ + while(skip(";")){ + param(); + insert(); + } + break; + } + if(buf[off]=='%'){ + escape(); + }else if(!pchar(buf[off])){ + break; + } + buf[pos++]=buf[off++]; + } + } + path.len = pos -mark; + path.off = mark; + } + + /** + * This is used to extract the query from the given URI. This + * will firstly initialize the token object that represents the + * query. This allows the token's toString method + * to return the extracted value of the token rather than getting + * confused with previous values set by a previous parse method. + * The calculation of the query part of a URI is basically the + * end of the URI. + */ + private void query() { + query.len = count - off; + query.off = off; + } + + /** + * This is an expression that is defined by RFC 2396 it is used + * in the definition of a segment expression. This is basically + * a list of pchars. + *

+ * This method has to ensure that no escaped chars go unchecked. + * This ensures that the read offset does not goe out of bounds + * and consequently throw an out of bounds exception. + */ + private void param() { + name(); + if(skip("=")){ /* in case of error*/ + value(); + } + } + + /** + * This extracts the name of the parameter from the character + * buffer. The name of a parameter is defined as a set of + * pchars including escape sequences. This will extract the + * parameter name and buffer the chars. The name ends when a + * equals character, "=", is encountered or in the case of a + * malformed parameter when the next character is not a pchar. + */ + private void name(){ + int mark = off; + int pos = off; + + while(off < count){ + if(buf[off]=='%'){ /* escaped */ + escape(); + }else if(buf[off]=='=') { + break; + }else if(!pchar(buf[off])){ + break; + } + buf[pos++] = buf[off++]; + } + name.len = pos - mark; + name.off = mark; + } + + /** + * This extracts a parameter value from a path segment. The + * parameter value consists of a sequence of pchars and some + * escape sequences. The parameter value is buffered so that + * the name and values can be paired. The end of the value + * is determined as the end of the buffer or the last pchar. + */ + private void value(){ + int mark = off; + int pos = off; + + while(off < count){ + if(buf[off]=='%'){ /* escaped */ + escape(); + }else if(!pchar(buf[off])) { + break; + } + buf[pos++] = buf[off++]; + } + value.len = pos - mark; + value.off = mark; + } + + /** + * This method adds the name and value to a map so that the next + * name and value can be collected. The name and value are added + * to the map as string objects. Once added to the map the + * Token objects are set to have zero length so they + * can be reused to collect further values. This will add the + * values to the map as an array of type string. This is done so + * that if there are multiple values that they can be stored. + */ + private void insert(){ + if(value.length() > 0){ + if(name.length() > 0) + insert(name,value); + } + name.clear(); + value.clear(); + } + + /** + * This will add the given name and value to the parameters map. + * This will only store a single value per parameter name, so + * only the parameter that was latest encountered will be saved. + * The getQuery method can be used to collect + * the parameter values using the parameter name. + * + * @param name this is the name of the value to be inserted + * @param value this is the value of a that is to be inserted + */ + private void insert(Token name, Token value){ + insert(name.toString(), value.toString()); + } + + /** + * This will add the given name and value to the parameters map. + * This will only store a single value per parameter name, so + * only the parameter that was latest encountered will be saved. + * The getQuery method can be used to collect + * the parameter values using the parameter name. + * + * @param name this is the name of the value to be inserted + * @param value this is the value of a that is to be inserted + */ + private void insert(String name, String value) { + param.put(name, value); + } + + /** + * This converts an encountered escaped sequence, that is all + * embedded hexidecimal characters into a native UCS character + * value. This does not take any characters from the stream it + * just prepares the buffer with the correct byte. The escaped + * sequence within the URI will be interpreded as UTF-8. + *

+ * This will leave the next character to read from the buffer + * as the character encoded from the URI. If there is a fully + * valid escaped sequence, that is "%" HEX HEX. + * This decodes the escaped sequence using UTF-8 encoding, all + * encoded sequences should be in UCS-2 to fit in a Java char. + */ + private void escape() { + int peek = peek(off); + + if(!unicode(peek)) { + binary(peek); + } + } + + /** + * This method determines, using a peek character, whether the + * sequence of escaped characters within the URI is binary data. + * If the data within the escaped sequence is binary then this + * will ensure that the next character read from the URI is the + * binary octet. This is used strictly for backward compatible + * parsing of URI strings, binary data should never appear. + * + * @param peek this is the first escaped character from the URI + * + * @return currently this implementation always returns true + */ + private boolean binary(int peek) { + if(off + 2 < count) { + off += 2; + buf[off]= bits(peek); + } + return true; + } + + /** + * This method determines, using a peek character, whether the + * sequence of escaped characters within the URI is in UTF-8. If + * a UTF-8 character can be successfully decoded from the URI it + * will be the next character read from the buffer. This can + * check for both UCS-2 and UCS-4 characters. However, because + * the Java char can only hold UCS-2, the UCS-4 + * characters will have only the low order octets stored. + *

+ * The WWW Consortium provides a reference implementation of a + * UTF-8 decoding for Java, in this the low order octets in the + * UCS-4 sequence are used for the character. So, in the + * absence of a defined behaviour, the W3C behaviour is assumed. + * + * @param peek this is the first escaped character from the URI + * + * @return this returns true if a UTF-8 character is decoded + */ + private boolean unicode(int peek) { + if((peek & 0x80) == 0x00){ + return unicode(peek, 0); + } + if((peek & 0xe0) == 0xc0){ + return unicode(peek & 0x1f, 1); + } + if((peek & 0xf0) == 0xe0){ + return unicode(peek & 0x0f, 2); + } + if((peek & 0xf8) == 0xf0){ + return unicode(peek & 0x07, 3); + } + if((peek & 0xfc) == 0xf8){ + return unicode(peek & 0x03, 4); + } + if((peek & 0xfe) == 0xfc){ + return unicode(peek & 0x01, 5); + } + return false; + } + + /** + * This method will decode the specified amount of escaped + * characters from the URI and convert them into a single Java + * UCS-2 character. If there are not enough characters within + * the URI then this will return false and leave the URI alone. + *

+ * The number of characters left is determined from the first + * UTF-8 octet, as specified in RFC 2279, and because this is + * a URI there must that number of "%" HEX HEX + * sequences left. If successful the next character read is + * the UTF-8 sequence decoded into a native UCS-2 character. + * + * @param peek contains the bits read from the first UTF octet + * @param more this specifies the number of UTF octets left + * + * @return this returns true if a UTF-8 character is decoded + */ + private boolean unicode(int peek, int more) { + if(off + more * 3 >= count) { + return false; + } + return unicode(peek,more,off); + } + + /** + * This will decode the specified amount of trailing UTF-8 bits + * from the URI. The trailing bits are those following the first + * UTF-8 octet, which specifies the length, in octets, of the + * sequence. The trailing octets are if the form 10xxxxxx, for + * each of these octets only the last six bits are valid UCS + * bits. So a conversion is basically an accumulation of these. + *

+ * If at any point during the accumulation of the UTF-8 bits + * there is a parsing error, then parsing is aborted an false + * is returned, as a result the URI is left unchanged. + * + * @param peek bytes that have been accumulated from the URI + * @param more this specifies the number of UTF octets left + * @param pos this specifies the position the parsing begins + * + * @return this returns true if a UTF-8 character is decoded + */ + private boolean unicode(int peek, int more, int pos) { + while(more-- > 0) { + if(buf[pos] == '%'){ + int next = pos + 3; + int hex = peek(next); + + if((hex & 0xc0) == 0x80){ + peek = (peek<<6)|(hex&0x3f); + pos = next; + continue; + } + } + return false; + } + if(pos + 2 < count) { + off = pos + 2; + buf[off]= bits(peek); + } + return true; + } + + /** + * Defines behaviour for UCS-2 versus UCS-4 conversion from four + * octets. The UTF-8 encoding scheme enables UCS-4 characters to + * be encoded and decodeded. However, Java supports the 16-bit + * UCS-2 character set, and so the 32-bit UCS-4 character set is + * not compatable. This basically decides what to do with UCS-4. + * + * @param data up to four octets to be converted to UCS-2 format + * + * @return this returns a native UCS-2 character from the int + */ + private char bits(int data) { + return (char)data; + } + + /** + * This will return the escape expression specified from the URI + * as an integer value of the hexidecimal sequence. This does + * not make any changes to the buffer it simply checks to see if + * the characters at the position specified are an escaped set + * characters of the form "%" HEX HEX, if so, then + * it will convert that hexidecimal string in to an integer + * value, or -1 if the expression is not hexidecimal. + * + * @param pos this is the position the expression starts from + * + * @return the integer value of the hexidecimal expression + */ + private int peek(int pos) { + if(buf[pos] == '%'){ + if(count <= pos + 2) { + return -1; + } + char high = buf[pos + 1]; + char low = buf[pos + 2]; + + return convert(high, low); + } + return -1; + } + + /** + * This will convert the two hexidecimal characters to a real + * integer value, which is returned. This requires characters + * within the range of 'A' to 'F' and 'a' to 'f', and also + * the digits '0' to '9'. The characters encoded using the + * ISO-8859-1 encoding scheme, if the characters are not with + * in the range specified then this returns -1. + * + * @param high this is the high four bits within the integer + * @param low this is the low four bits within the integer + * + * @return this returns the indeger value of the conversion + */ + private int convert(char high, char low) { + int hex = 0x00; + + if(hex(high) && hex(low)){ + if('A' <= high && high <= 'F'){ + high -= 'A' - 'a'; + } + if(high >= 'a') { + hex ^= (high-'a')+10; + } else { + hex ^= high -'0'; + } + hex <<= 4; + + if('A' <= low && low <= 'F') { + low -= 'A' - 'a'; + } + if(low >= 'a') { + hex ^= (low-'a')+10; + } else { + hex ^= low-'0'; + } + return hex; + } + return -1; + } + + /** + * This is used to determine wheather a char is a hexidecimal + * char or not. A hexidecimal character is consdered + * to be a character within the range of 0 - 9 and + * between a - f and A - F. This will + * return true if the character is in this range. + * + * @param ch this is the character which is to be determined here + * + * @return true if the character given has a hexidecimal value + */ + private boolean hex(char ch) { + if(ch >= '0' && ch <= '9') { + return true; + } else if(ch >='a' && ch <= 'f') { + return true; + } else if(ch >= 'A' && ch <= 'F') { + return true; + } + return false; + } + + /** + * This is a character set defined by RFC 2396 it is used to + * determine the valididity of certain chars + * within a Uniform Resource Identifier. RFC 2396 defines + * an unreserved char as alphanum | mark. + * + * @param c the character value that is being checked + * + * @return true if the character has an unreserved value + */ + private boolean unreserved(char c){ + return mark(c) || alphanum(c); + } + + /** + * This is used to determine wheather or not a given unicode + * character is an alphabetic character or a digit character. + * That is withing the range 0 - 9 and between + * a - z it uses iso-8859-1 to + * compare the character. + * + * @param c the character value that is being checked + * + * @return true if the character has an alphanumeric value + */ + private boolean alphanum(char c){ + return digit(c) || alpha(c); + } + + /** + * This is used to determine wheather or not a given unicode + * character is an alphabetic character. This uses encoding + * iso-8859-1 to compare the characters. + * + * @param c the character value that is being checked + * + * @return true if the character has an alphabetic value + */ + private boolean alpha(char c){ + return (c <= 'z' && 'a' <= c) || + (c <= 'Z' && 'A' <= c); + } + + /** + * This is a character set defined by RFC 2396 it checks + * the valididity of cetain chars within a uniform resource + * identifier. The RFC 2396 defines a mark char as "-", + * "_", ".", "!", "~", "*", "'", "(", ")". + * + * @param c the character value that is being checked + * + * @return true if the character is a mark character + */ + private boolean mark(char c){ + switch(c){ + case '-': case '_': case '.': + case '!': case '~': case '*': + case '\'': case '(': case ')': + return true; + default: + return false; + } + } + + /** + * This is a character set defined by RFC 2396 it is used to check + * the valididity of cetain chars within a generic uniform resource + * identifier. The RFC 2396 defines a pchar char as unreserved or + * escaped or one of the following characters ":", "@", "=", + * "&", "+", "$", "," this will not check to see if the + * char is an escaped char, that is % HEX HEX. Because + * this takes 3 chars. + * + * @param c the character value that is being checked + * + * @return true if the character is a pchar character + */ + private boolean pchar(char c){ + switch(c){ + case '@': case '&': case '=': + case '+': case '$': case ',': + case ':': + return true; + default: + return unreserved(c); + } + } + + /** + * This is a character set defined by RFC 2396, it checks the + * valididity of certain chars in a uniform resource identifier. + * The RFC 2396 defines a reserved char as ";", "/", "?", + * ":", "@", "&", "=", "+", "$", ",". + * + * @param c the character value that is being checked + * + * @return true if the character is a reserved character + */ + private boolean reserved(char c){ + switch(c){ + case ';': case '/': case '?': + case '@': case '&': case ':': + case '=': case '+': case '$': + case ',': + return true; + default: + return false; + } + } + + /** + * This is used to convert this URI object into a String + * object. This will only convert the parts of the URI that exist, so + * the URI may not contain the domain or the query part and it will + * not contain the path parameters. If the URI contains all these + * parts then it will return somthing like + *

+    * scheme://host:port/path/path?querypart
+    * 
+ *

+ * It can return /path/path?querypart style relative + * URI's. If any of the parts are set to null then that part will be + * missing, for example if setDomain method is invoked + * with a null parameter then the domain and port will be missing + * from the resulting URI. If the path part is set to null using the + * setPath then the path will be /. An + * example URI with the path part of null would be + *

+    * scheme://host:port/?querypart
+    * 
+ * + * @return the URI with only the path part and the non-null optional + * parts of the uniform resource identifier + */ + public String toString() { + return (scheme.length() > 0 ? scheme +"://": "") + + (domain.length() > 0 ? domain + + (port > 0 ? ":"+port : "") : "")+ getPath() + + (param.size() > 0 ? param : "")+ + (query.length()>0?"?"+query :""); + } + + /** + * The ParameterMap is uses to store the parameters + * that are to be encoded in to the address. This will append all + * of the parameters to the end of the path. These can later be + * extracted by parsing the address. + * + * @author Niall Gallagher + */ + private class ParameterMap extends KeyMap { + + /** + * This will return the parameters encoded in such a way that + * it can be appended to the end of the path. These parameters + * can be added to the address such that they do not form a + * query parameter. Values such as session identifiers are + * often added as the path parameters to the address. + * + * @return this returns the representation of the parameters + */ + private String encode() { + StringBuilder text = new StringBuilder(); + + for(String name : param) { + String value = param.get(name); + + text.append(";"); + text.append(name); + + if(value != null) { + text.append("="); + text.append(value);; + } + } + return text.toString(); + } + + /** + * This will return the parameters encoded in such a way that + * it can be appended to the end of the path. These parameters + * can be added to the address such that they do not form a + * query parameter. Values such as session identifiers are + * often added as the path parameters to the address. + * + * @return this returns the representation of the parameters + */ + public String toString() { + return encode(); + } + } + + /** + * This is used as an alternative to the ParseBuffer + * for extracting tokens from the URI without allocating memory. + * This will basically mark out regions within the buffer which are + * used to represent the token. When the token value is required + * the region is used to create a String object. + */ + private class Token { + + /** + * This can be used to override the value for this token. + */ + public String value; + + /** + * This represents the start offset within the buffer. + */ + public int off; + + /** + * This represents the number of charters in the token. + */ + public int len; + + /** + * If the Token is to be reused this will clear + * all previous data. Clearing the buffer allows it to be + * reused if there is a new URI to be parsed. This ensures + * that a null is returned if the token length is zero. + */ + public void clear() { + value = null; + len = 0; + } + + /** + * This is used to determine the number of characters this + * token contains. This is used rather than accessing the + * length directly so that the value the token represents + * can be overridden easily without upsetting the token. + * + * @return this returns the number of characters this uses + */ + public int length() { + if(value == null){ + return len; + } + return value.length(); + } + + /** + * This method will convert the Token into it's + * String equivelant. This will firstly check + * to see if there is a value, for the string representation, + * if there is the value is returned, otherwise the region + * is converted into a String and returned. + * + * @return this returns a value representing the token + */ + public String toString() { + if(value != null) { + return value; + } + if(len > 0) { + value = new String(buf,off,len); + } + return value; + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentDispositionParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentDispositionParser.java new file mode 100644 index 0000000..b7fe6c2 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentDispositionParser.java @@ -0,0 +1,296 @@ +/* + * ContentDispositionParser.java February 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import org.simpleframework.common.parse.ParseBuffer; +import org.simpleframework.common.parse.Parser; +import org.simpleframework.http.ContentDisposition; + +/** + * The ContentDispositionParser object is used to represent + * a parser used to parse the Content-Disposition header. Its used when + * there is a multipart form upload to the server and allows the + * server to determine the individual part types. + * + * @author Niall Gallagher + */ +public class ContentDispositionParser extends Parser implements ContentDisposition { + + /** + * This is the buffer used to acquire values from the header. + */ + private ParseBuffer skip; + + /** + * This is used to capture the name of the file if it is provided. + */ + private ParseBuffer file; + + /** + * This is used to capture the name of the part if it is provided. + */ + private ParseBuffer name; + + /** + * This is used to determine if the disposition is a file or form. + */ + private boolean form; + + /** + * Constructor for the ContentDispositionParser object. + * This is used to create a parser that can parse a disposition + * header which is typically sent as part of a multipart upload. It + * can be used to determine the type of the upload. + */ + public ContentDispositionParser() { + this.file = new ParseBuffer(); + this.name = new ParseBuffer(); + this.skip = new ParseBuffer(); + } + + /** + * Constructor for the ContentDispositionParser object. + * This is used to create a parser that can parse a disposition header + * which is typically sent as part of a multipart upload. It can + * be used to determine the type of the upload. + * + * @param text this is the header value that is to be parsed + */ + public ContentDispositionParser(String text) { + this(); + parse(text); + } + + /** + * This method is used to acquire the file name of the part. This + * is used when the part represents a text parameter rather than + * a file. However, this can also be used with a file part. + * + * @return this returns the file name of the associated part + */ + public String getFileName() { + return file.toString(); + } + + /** + * This method is used to acquire the name of the part. Typically + * this is used when the part represents a text parameter rather + * than a file. However, this can also be used with a file part. + * + * @return this returns the name of the associated part + */ + public String getName() { + return name.toString(); + } + + /** + * This method is used to determine the type of a part. Typically + * a part is either a text parameter or a file. If this is true + * then the content represented by the associated part is a file. + * + * @return this returns true if the associated part is a file + */ + public boolean isFile() { + return !form || file.length() > 0; + } + + /** + * This will initialize the Parser when it is ready + * to parse a new String. This will reset the + * parser to a ready state. This method is invoked by the parser + * before the parse method is invoked, it is used to pack the + * contents of the header and clear any previous tokens used. + */ + protected void init() { + if(count > 0) { + pack(); + } + clear(); + } + /** + * This is used to clear all previously collected tokens. This + * allows the parser to be reused when there are multiple source + * strings to be parsed. Clearing of the tokens is performed + * when the parser is initialized. + */ + protected void clear() { + file.clear(); + name.clear(); + form = false; + off = 0; + } + + /** + * This is the method that should be implemented to read the + * buffer. This method will extract the type from the header and + * the tries to extract the optional parameters if they are in + * the header. The optional parts are the file name and name. + */ + protected void parse() { + type(); + parameters(); + } + + /** + * This is used to remove all whitespace characters from the + * String excluding the whitespace within literals. + * The definition of a literal can be found in RFC 2616. + *

+ * The definition of a literal for RFC 2616 is anything between 2 + * quotes but excuding quotes that are prefixed with the backward + * slash character. + */ + private void pack() { + char old = buf[0]; + int len = count; + int seek = 0; + int pos = 0; + + while(seek < len){ + char ch = buf[seek++]; + + if(ch == '"' && old != '\\'){ /* qd-text*/ + buf[pos++] = ch; + + while(seek < len){ + old = buf[seek-1]; + ch = buf[seek++]; + buf[pos++] = ch; + + if(ch =='"'&& old!='\\'){ /*qd-text*/ + break; + } + } + }else if(!space(ch)){ + old = buf[seek - 1]; + buf[pos++] = old; + } + } + count = pos; + } + + /** + * This is used to determine the type of the disposition header. This + * will allow the parser to determine it the header represents form + * data or a file upload. Once it determines the type of the upload + * header it sets an internal flag which can be used. + */ + private void type() { + if(skip("form-data")) { + form = true; + } else if(skip("file")) { + form = false; + } + } + + /** + * This will read the parameters from the header value. This will search + * for the filename parameter within the set of parameters + * which are given to the type. The filename param and the + * the name are tokenized by this method. + */ + private void parameters(){ + while(skip(";")){ + if(skip("filename=")){ + value(file); + } else { + if(skip("name=")) { + value(name); + } else { + parameter(); + } + } + } + } + + /** + * This will read the parameters from the header value. This will search + * for the filename parameter within the set of parameters + * which are given to the type. The filename param and the + * the name are tokenized by this method. + */ + private void parameter() { + name(); + off++; + value(skip); + } + + /** + * This will simply read all characters from the buffer before the first '=' + * character. This represents a parameter name (see RFC 2616 for token). The + * parameter name is not buffered it is simply read from the buffer. This will + * not cause an IndexOutOfBoundsException as each offset + * is checked before it is acccessed. + */ + private void name(){ + while(off < count){ + if(buf[off] =='='){ + break; + } + off++; + } + } + + /** + * This is used to read a parameters value from the buf. This will read all + * char's upto but excluding the first terminal char + * encountered from the off within the buf, or if the value is a literal + * it will read a literal from the buffer (literal is any data between + * quotes except if the quote is prefixed with a backward slash character). + * + * @param value this is the parse buffer to append the value to + */ + private void value(ParseBuffer value) { + if(quote(buf[off])) { + char quote = buf[off]; + + for(off++; off < count;) { + if(quote == buf[off]) { + if(buf[++off - 2] != '\\') { + break; + } + } + + value.append(buf[off++]); + } + } else { + while(off < count) { + if(buf[off] == ';') { + break; + } + + value.append(buf[off]); + off++; + } + } + } + + /** + * This method is used to determine if the specified character is a quote + * character. The quote character is typically used as a boundary for the + * values within the header. This accepts a single or double quote. + * + * @param ch the character to determine if it is a quotation + * + * @return true if the character provided is a quotation character + */ + private boolean quote(char ch) { + return ch == '\'' || ch == '"'; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentTypeParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentTypeParser.java new file mode 100644 index 0000000..f42c073 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentTypeParser.java @@ -0,0 +1,556 @@ +/* + * ContentTypeParser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import org.simpleframework.common.KeyMap; +import org.simpleframework.common.parse.ParseBuffer; +import org.simpleframework.common.parse.Parser; +import org.simpleframework.http.ContentType; + +/** + * This provides access to the MIME type parts, that is the primary + * type, the secondary type and an optional character set parameter. + * The charset parameter is one of many parameters that + * can be associated with a MIME type. This however this exposes this + * parameter with a typed method. + *

+ * The getCharset will return the character encoding the + * content type is encoded within. This allows the user of the content + * to decode it correctly. Other parameters can be acquired from this + * by simply providing the name of the parameter. + * + * @author Niall Gallagher + */ +public class ContentTypeParser extends Parser implements ContentType { + + /** + * Used to store the characters consumed for the secondary type. + */ + private ParseBuffer secondary; + + /** + * Used to store the characters consumed for the primary type. + */ + private ParseBuffer primary; + + /** + * Used to store the characters for the charset parameter. + */ + private ParseBuffer charset; + + /** + * Used to store the characters consumed for the type. + */ + private ParseBuffer type; + + /** + * Used to collect the name of a content type parameter. + */ + private ParseBuffer name; + + /** + * Used to collect the value of the content type parameter. + */ + private ParseBuffer value; + + /** + * Used to store the name value pairs of the parameters. + */ + private KeyMap map; + + /** + * The default constructor will create a ContentParser + * that contains no charset, primary or secondary. This can be used + * to extract the primary, secondary and the optional charset + * parameter by using the parser's parse(String) + * method. + */ + public ContentTypeParser(){ + this.secondary = new ParseBuffer(); + this.primary = new ParseBuffer(); + this.charset = new ParseBuffer(); + this.value = new ParseBuffer(); + this.type = new ParseBuffer(); + this.name = new ParseBuffer(); + this.map = new KeyMap(); + } + + /** + * This is primarily a convenience constructor. This will parse + * the String given to extract the MIME type. This + * could be achieved by calling the default no-arg constructor + * and then using the instance to invoke the parse + * method on that String. + * + * @param header String containing a MIME type value + */ + public ContentTypeParser(String header){ + this(); + parse(header); + } + + /** + * This method is used to get the primary and secondary parts + * joined together with a "/". This is typically how a content + * type is examined. Here convenience is most important, we can + * easily compare content types without any parameters. + * + * @return this returns the primary and secondary types + */ + public String getType() { + return type.toString(); + } + + /** + * This sets the primary type to whatever value is in the string + * provided is. If the string is null then this will contain a + * null string for the primary type of the parameter, which is + * likely invalid in most cases. + * + * @param value the type to set for the primary type of this + */ + public void setPrimary(String value) { + type.reset(value); + type.append('/'); + type.append(secondary); + primary.reset(value); + } + + /** + * This is used to retrieve the primary type of this MIME type. The + * primary type part within the MIME type defines the generic type. + * For example text/plain; charset=UTF-8. This will + * return the text value. If there is no primary type then this + * will return null otherwise the string value. + * + * @return the primary type part of this MIME type + */ + public String getPrimary() { + return primary.toString(); + } + + /** + * This sets the secondary type to whatever value is in the string + * provided is. If the string is null then this will contain a + * null string for the secondary type of the parameter, which is + * likely invalid in most cases. + * + * @param value the type to set for the primary type of this + */ + public void setSecondary(String value) { + type.reset(primary); + type.append('/'); + type.append(value); + secondary.reset(value); + } + + /** + * This is used to retrieve the secondary type of this MIME type. + * The secondary type part within the MIME type defines the generic + * type. For example text/html; charset=UTF-8. This + * will return the HTML value. If there is no secondary type then + * this will return null otherwise the string value. + * + * @return the primary type part of this MIME type + */ + public String getSecondary(){ + return secondary.toString(); + } + + /** + * This will set the charset to whatever value the + * string contains. If the string is null then this will not set + * the parameter to any value and the toString method + * will not contain any details of the parameter. + * + * @param enc parameter value to add to the MIME type + */ + public void setCharset(String enc) { + charset.reset(enc); + } + + /** + * This is used to retrieve the charset of this MIME + * type. This is a special parameter associated with the type, if + * the parameter is not contained within the type then this will + * return null, which typically means the default of ISO-8859-1. + * + * @return the value that this parameter contains + */ + public String getCharset() { + return charset.toString(); + } + + /** + * This is used to retrieve an arbitrary parameter from the MIME + * type header. This ensures that values for boundary + * or other such parameters are not lost when the header is parsed. + * This will return the value, unquoted if required, as a string. + * + * @param name this is the name of the parameter to be retrieved + * + * @return this is the value for the parameter, or null if empty + */ + public String getParameter(String name) { + return map.get(name); + } + + /** + * This will add a named parameter to the content type header. If + * a parameter of the specified name has already been added to the + * header then that value will be replaced by the new value given. + * Parameters such as the boundary as well as other + * common parameters can be set with this method. + * + * @param name this is the name of the parameter to be added + * @param value this is the value to associate with the name + */ + public void setParameter(String name, String value) { + map.put(name, value); + } + + /** + * This will initialize the parser when it is ready to parse + * a new String. This will reset the parser to a + * ready state. The init method is invoked by the parser when + * the Parser.parse method is invoked. + */ + protected void init(){ + if(count > 0) { + pack(); + } + clear(); + } + + /** + * This is used to clear all previously collected tokens. This + * allows the parser to be reused when there are multiple source + * strings to be parsed. Clearing of the tokens is performed + * when the parser is initialized. + */ + private void clear() { + primary.clear(); + secondary.clear(); + charset.clear(); + name.clear(); + value.clear(); + type.clear(); + map.clear(); + off = 0; + } + + /** + * Reads and parses the MIME type from the given String + * object. This uses the syntax defined by RFC 2616 for the media-type + * syntax. This parser is only concerned with one parameter, the + * charset parameter. The syntax for the media type is + *

+    * media-type = token "/" token *( ";" parameter )
+    * parameter = token | literal 
+    * 
+ */ + protected void parse(){ + primary(); + off++; + secondary(); + parameters(); + } + + /** + * This is used to remove all whitespace characters from the + * String excluding the whitespace within literals. + * The definition of a literal can be found in RFC 2616. + *

+ * The definition of a literal for RFC 2616 is anything between 2 + * quotes but excluding quotes that are prefixed with the backward + * slash character. + */ + private void pack() { + char old = buf[0]; + int len = count; + int seek = 0; + int pos = 0; + + while(seek < len){ + char ch = buf[seek++]; + + if(ch == '"' && old != '\\'){ /* qd-text*/ + buf[pos++] = ch; + + while(seek < len){ + old = buf[seek-1]; + ch = buf[seek++]; + buf[pos++] = ch; + + if(ch =='"'&& old!='\\'){ /*qd-text*/ + break; + } + } + }else if(!space(ch)){ + old = buf[seek - 1]; + buf[pos++] = old; + } + } + count = pos; + } + + /** + * This reads the type from the MIME type. This will fill the + * type ParseBuffer. This will read all chars + * upto but not including the first instance of a '/'. The type + * of a media-type as defined by RFC 2616 is + * type/subtype;param=val;param2=val. + */ + private void primary(){ + while(off < count){ + if(buf[off] =='/'){ + type.append('/'); + break; + } + type.append(buf[off]); + primary.append(buf[off]); + off++; + } + } + + /** + * This reads the subtype from the MIME type. This will fill the + * subtype ParseBuffer. This will read all chars + * upto but not including the first instance of a ';'. The subtype + * of a media-type as defined by RFC 2616 is + * type/subtype;param=val;param2=val. + */ + private void secondary(){ + while(off < count){ + if(buf[off] ==';'){ + break; + } + type.append(buf[off]); + secondary.append(buf[off]); + off++; + } + } + + /** + * This will read the parameters from the MIME type. This will search + * for the charset parameter within the set of parameters + * which are given to the type. The charset param is the + * only parameter that this parser will tokenize. + *

+ * This will remove any parameters that preceed the charset parameter. + * Once the charset is retrived the MIME type is considered + * to be parsed. + */ + private void parameters(){ + while(skip(";")){ + if(skip("charset=")){ + charset(); + break; + }else{ + parameter(); + insert(); + } + } + } + + /** + * This will add the name and value tokens to the parameters map. + * If any previous value of the given name has been inserted + * into the map then this will overwrite that value. This is + * used to ensure that the string value is inserted to the map. + */ + private void insert() { + insert(name, value); + name.clear(); + value.clear(); + } + + /** + * This will add the given name and value to the parameters map. + * If any previous value of the given name has been inserted + * into the map then this will overwrite that value. This is + * used to ensure that the string value is inserted to the map. + * + * @param name this is the name of the value to be inserted + * @param value this is the value of a that is to be inserted + */ + private void insert(ParseBuffer name, ParseBuffer value) { + map.put(name.toString(), value.toString()); + } + + /** + * This is a parameter as defined by RFC 2616. The parameter is added to a + * MIME type e.g. type/subtype;param=val etc. The parameter + * name and value are not stored. This is used to simply update the read + * offset past the parameter. The reason for reading the parameters is to + * search for the charset parameter which will indicate the + * encoding. + */ + private void parameter(){ + name(); + off++; /* = */ + value(); + } + + /** + * This will simply read all characters from the buffer before the first '=' + * character. This represents a parameter name (see RFC 2616 for token). The + * parameter name is not buffered it is simply read from the buffer. This will + * not cause an IndexOutOfBoundsException as each offset + * is checked before it is acccessed. + */ + private void name(){ + while(off < count){ + if(buf[off] =='='){ + break; + } + name.append(buf[off]); + off++; + } + } + + /** + * This is used to read a parameters value from the buf. This will read all + * char's upto but excluding the first terminal char + * encountered from the off within the buf, or if the value is a literal + * it will read a literal from the buffer (literal is any data between + * quotes except if the quote is prefixed with a backward slash character). + */ + private void value(){ + if(quote(buf[off])){ + for(off++; off < count;){ + if(quote(buf[off])){ + if(buf[++off-2]!='\\'){ + break; + } + } + value.append(buf[off++]); + } + }else{ + while(off < count){ + if(buf[off] ==';') { + break; + } + value.append(buf[off]); + off++; + } + } + } + + /** + * This method is used to determine if the specified character is a quote + * character. The quote character is typically used as a boundary for the + * values within the header. This accepts a single or double quote. + * + * @param ch the character to determine if it is a quotation + * + * @return true if the character provided is a quotation character + */ + private boolean quote(char ch) { + return ch == '\'' || ch == '"'; + } + + /** + * This is used to read the value from the charset param. + * This will fill the charset ParseBuffer and with + * the charset value. This will read a literal or a token as + * the charset value. If the charset is a literal + * then the quotes will be read as part of the charset. + */ + private void charset(){ + if(buf[off] == '"'){ + charset.append('"'); + for(off++; off < count;){ + charset.append(buf[off]); + if(buf[off++]=='"') + if(buf[off-2]!='\\'){ + break; + } + } + }else{ + while(off < count){ + if(buf[off]==';') { + break; + } + charset.append(buf[off]); + off++; + } + } + } + + /** + * This will return the value of the MIME type as a string. This + * will concatenate the primary and secondary type values and + * add the charset parameter to the type which will + * recreate the content type. + * + * @return this returns the string representation of the type + */ + private String encode() { + StringBuilder text = new StringBuilder(); + + if(primary != null) { + text.append(primary); + text.append("/"); + text.append(secondary); + } + if(charset.length() > 0) { + text.append("; charset="); + text.append(charset); + } + return encode(text); + } + + /** + * This will return the value of the MIME type as a string. This + * will concatenate the primary and secondary type values and + * add the charset parameter to the type which will + * recreate the content type. + * + * @param text this is the buffer to encode the parameters to + * + * @return this returns the string representation of the type + */ + private String encode(StringBuilder text) { + for(String name : map) { + String value = map.get(name); + + text.append("; "); + text.append(name); + + if(value != null) { + text.append("="); + text.append(value);; + } + } + return text.toString(); + } + + /** + * This will return the value of the MIME type as a string. This + * will concatenate the primary and secondary type values and + * add the charset parameter to the type which will + * recreate the content type. + * + * @return this returns the string representation of the type + */ + public String toString() { + return encode(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/CookieParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/CookieParser.java new file mode 100644 index 0000000..1d07c04 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/CookieParser.java @@ -0,0 +1,589 @@ +/* + * CookieParser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import org.simpleframework.common.parse.Parser; +import org.simpleframework.http.Cookie; + +import java.util.Iterator; + +/** + * CookieParser is used to parse the cookie header. The cookie header is + * one of the headers that is used by the HTTP state management mechanism. + * The Cookie header is the header that is sent from the client to the + * server in response to a Set-Cookie header. The syntax of the Cookie + * header as taken from RFC 2109, HTTP State Management Mechanism. + *

+ *
+ *  cookie          =       "Cookie:" cookie-version
+ *                          1*((";" | ",") cookie-value)
+ *  cookie-value    =       NAME "=" VALUE [";" path] [";" domain]
+ *  cookie-version  =       "$Version" "=" value
+ *  NAME            =       attr
+ *  VALUE           =       value
+ *  path            =       "$Path" "=" value
+ *  domain          =       "$Domain" "=" value
+ *
+ * 
+ * The cookie header may consist of several cookies. Each cookie can be + * extracted from the header by examining the it syntax of the cookie + * header. The syntax of the cookie header is defined in RFC 2109. + *

+ * Each cookie has a $Version attribute followed by multiple + * cookies. Each contains a name and a value, followed by an optional + * $Path and $Domain attribute. This will parse + * a given cookie header and return each cookie extracted as a + * Cookie object. + * + * @author Niall Gallagher + */ +public class CookieParser extends Parser implements Iterable { + + /** + * Determines when the Parser has finished. + */ + private boolean finished; + + /** + * Used so the Parser does not parse twice. + */ + private boolean parsed; + + /** + * Version of the Cookie being parsed. + */ + private int version; + + /** + * Used to store the name of the Cookie. + */ + private Token name; + + /** + * Used to store the value of the Cookie. + */ + private Token value; + + /** + * Used to store the $Path values. + */ + private Token path; + + /** + * Used to store the $Domain values. + */ + private Token domain; + + /** + * Create a CookieParser that contains no cookies. + * the instance will return false for the + * hasNext method. cookies may be parsed using + * this instance by using the parse method. + */ + public CookieParser(){ + this.path = new Token(); + this.domain = new Token(); + this.name = new Token(); + this.value = new Token(); + this.finished = true; + } + + /** + * This is primarily a convineance constructor. This will parse the + * String given to extract the cookies. This could be + * achived by calling the default no-arg constructor and then using + * the instance to invoke the parse method on that + * String. + * + * @param header a String containing a cookie value + */ + public CookieParser(String header){ + this(); + parse(header); + } + + /** + * Resets the cookie and the buffer variables for this + * CookieParser. It is used to set the + * state of the parser to start parsing a new cookie. + */ + protected void init() { + finished = false; + parsed =false; + version = 0; + off = 0; + version(); + } + + /** + * This will extract the next Cookie from the + * buffer. If all the characters in the buffer have already + * been examined then this method will simply do nothing. + * Otherwise this will parse the remainder of the buffer + * and (if it follows RFC 2109) produce a Cookie. + */ + protected void parse() { + if(!finished){ + cookie(); + parsed=true; + } + } + + /** + * This is used to skip an arbitrary String within the + * char buf. It checks the length of the String + * first to ensure that it will not go out of bounds. A comparison + * is then made with the buffers contents and the String + * if the reigon in the buffer matched the String then the + * offset within the buffer is increased by the String's + * length so that it has effectively skipped it. + *

+ * This skip method will ignore all of the whitespace text. + * This will also skip trailing spaces within the the input text and + * all spaces within the source text. For example if the input was + * the string "s omete xt" and the source was "some text to skip" then + * the result of a skip ignoring spaces would be "to skip" in the + * source string, as the trailing spaces are also eaten by this. + * + * @param text this is the String value to be skipped + * + * @return true if the String was skipped + */ + protected boolean skip(String text){ + int size = text.length(); + int seek = off; + int read = 0; + + if(off + size > count){ + return false; + } + while(read < size) { + char a = text.charAt(read); + char b = buf[seek]; + + if(space(b)){ + if(++seek >= count){ + return false; + } + }else if(space(a)){ + if(++read >= size) { + continue; + } + }else { + if(toLower(a) != toLower(b)){ + return false; + } + read++; + seek++; + } + } + for(off = seek; off < count; off++){ + if(!space(buf[off])) + break; + } + return true; + } + + /** + * This is used to acquire the cookie values from the provided + * the provided source text. This allows the cookie parser to be + * used within a for each loop to parse out the values of a + * cookie one by one so that they may be used or stored. + * + * @return this returns an iterator for extracting cookie value + */ + public Iterator iterator() { + return new Sequence(); + } + + /** + * This is used so that the collection of Cookies + * can be reiterated. This allows the collection to be reused. + * The reset method will invoke the super classes + * init method. This will reinitialize this + * Parser so the cookie will be reparsed. + */ + public void reset() { + init(); + parse(); + } + + /** + * Creates the Cookie from the token objects. It is + * assumed that the Cookie String has + * been parsed when this is called. This should only be used after + * the parse method has been called. + *

+ * If there is no $Domain or $Path + * within the Cookie String then the + * getDomain and getPath are null. + * + * @return the Cookie that was just parsed + */ + private Cookie getCookie() { + return getCookie(name.toString(), + value.toString()); + } + + /** + * Creates the Cookie from the token objects. It is + * assumed that the Cookie String has + * been parsed when this is called. This should only be used after + * the parse method has been called. + *

+ * If there is no $Domain or $Path + * within the Cookie String then the + * getDomain and getPath are null. + * + * @param name the name that the Cookie contains + * @param value the value that the Cookie contains + * + * @return the Cookie that was just parsed + */ + private Cookie getCookie(String name, String value) { + Cookie cookie = new Cookie(name, value, false); + + if(domain.len > 0) { + cookie.setDomain(domain.toString()); + } + if(path.len > 0) { + cookie.setPath(path.toString()); + } + cookie.setVersion(version); + return cookie; + } + + /** + * This is used to parse a Cookie from the buffer + * that contains the Cookie values. This will first + * try to remove any trailing value after the version/prev + * Cookie once this is removed it will extract the + * name/value pair from the Cookie. The name and + * value of the Cookie will be saved by the name + * and value tokens. + */ + private void cookie(){ + if(!skip(",")){ /* ,|; */ + skip(";"); + } + name(); + skip("="); /* = */ + value(); + } + + /** + * This initializes the name token and extracts the name of this + * Cookie. The offset and length of the name will be + * saved in the name token. This will read all char's + * upto but excluding the first '=' char encountered + * from the off within the buffer. + */ + private void name() { + name.off = off; + name.len = 0; + while(off < count){ + if(buf[off] == '='){ + break; + } + name.len++; + off++; + } + } + + /** + * Used to extract everything found after the NAME '=' + * within a Cookie. This extracts the Cookie + * value the $Path and $Domain attributes + * if they exist (i.e. $Path and $Domain + * are optional in a cookie see RFC 2109). + *

+ * The path method reads the terminal found before it as does the + * domain method that is ";$Path" is read as the first + * part of the path method. This is because if there is no path the + * parser should not read data it does not know belongs to a specific + * part of the Cookie. + */ + private void value() { + data(); + path(); + domain(); + } + + /** + * This initializes the value token and extracts the value of this + * Cookie. The offset and length of the value will be + * saved in the value token. This will read all char's + * upto but excluding the first terminal char encountered from the + * off within the buffer, or if the value is a literal it will read + * a literal from the buffer (literal is any data between quotes + * except if the quote is prefixed with a backward slash character + * that is '\'). + */ + private void data() { + value.off = off; + value.len = 0; + if(off < count && buf[off] == '"'){ + value.len++; + for(off++; off < count;){ + value.len++; + if(buf[off++]=='"') + if(buf[off-2]!='\\'){ + break; + } + } + value.len-=2; /* remove " */ + value.off++; /* remove " */ + }else { + while(off < count){ + if(terminal(buf[off])) + break; + value.len++; + off++; + } + } + } + + /** + * This initializes the path token and extracts the $Path + * of this Cookie. The offset and length of the path will + * be saved in the path token. This will read all char's + * up to but excluding the first terminal char encountered + * from the off within the buffer, or if the value is a + * literal it will read a literal from the buffer (literal is any data + * between quotes except if the quote is prefixed with a backward slash + * character, that is '\'). + *

+ * This reads the terminal before the $Path so that if + * there is no $Path for the Cookie then + * the character before it will not be read needlessly. + */ + private void path() { + path.len = 0; /* reset */ + if(skip(";$Path=")){ + path.off = off; + if(buf[off] == '"'){ + path.len++; + for(off++; off < count;){ + path.len++; + if(buf[off++]=='"') + if(buf[off-2]!='\\'){ + break; + } + } + path.len-=2; /* remove " */ + path.off++; /* remove " */ + }else{ + while(off < count){ + if(terminal(buf[off])) + break; + path.len++; + off++; + } + } + } + } + + /** + * Initializes the domain token and extracts the $Domain + * of this Cookie. The offset and length of the domain + * will be saved in the path token. This will read all characters up + * to but excluding the first terminal char encountered + * from the off within the buffer, or if the value is a literal it + * will read a literal from the buffer (literal is any data between + * quotes except if the quote is prefixed with a backward slash + * character, that is '\'). + *

+ * This reads the terminal before the $Domain so that + * if there is no $Domain for the Cookie + * then the character before it will not be read needlessly. + */ + private void domain(){ + domain.len = 0; /* reset */ + if(skip(";$Domain=")) { + domain.off = off; + if(buf[off] == '"'){ + domain.len++; + for(off++; off < count;){ + domain.len++; + if(buf[off++]=='"') + if(buf[off-2]!='\\'){ + break; + } + } + domain.len-=2; /* remove " */ + domain.off++; /* remove " */ + }else{ + while(off < count){ + if(terminal(buf[off])) + break; + domain.len++; + off++; + } + } + } + } + + /** + * This extracts the $Version of this Cookie. + * The version is parsed and converted into a decimal int from the digit + * characters that make up a version. + *

+ * This will read all digit char's up to but excluding the + * first non digit char that it encounters from the offset + * within the buffer, or if the value is a literal it will read a literal + * from the buffer (literal is any data between quotes except if the quote + * is prefixed with a backward slash character i.e. '\'). + */ + private void version(){ + if(skip("$Version=")) { + if(buf[off] == '"'){ + off++; + } + while(off < count){ + if(!digit(buf[off])){ + break; + } + version *= 10; + version += buf[off]; + version -= '0'; + off++; + } + if(buf[off] == '"'){ + off++; + } + }else{ + version = 1; + } + } + + /** + * This is used to determine if a given iso8859-1 character is + * a terminal character. That is either the ';' or ',' + * characters. Although the RFC 2109 says the terminal can be + * either a comma, it is not used by any browsers. + * + * @param ch the character that is to be compared + * + * @return true if this is a semicolon character + */ + private boolean terminal(char ch) { + return ch == ';'; + } + + /** + * This is used to represent an Iterator that will + * iterate over the available cookies within the provided source + * text. This allows the cookie parser to be used as an iterable + * with for each loops. Cookies can not be removed with this. + */ + private class Sequence implements Iterator { + + /** + * Extracts the next Cookie object from the string + * given. This will return null when there are no + * more cookies left in the String being parsed. + *

+ * To find out when there are no more cookies left use the + * hasNext method. This will only set the name, + * value, path, domain name version of the cookie + * because as of RFC 2109 these are the only attributes a + * Cookie may have, the path and domain are + * optional. + * + * @return an initialized Cookie object + */ + public Cookie next(){ + if(!hasNext()) { + return null; + } + parsed = false; + return getCookie(); + } + + + /** + * Determine whether or not there are any Cookies + * left in the String. This will attempt to extract + * another Cookie from the String and + * cache the result so the next method will produce + * this Cookie. If another Cookie cannot + * be parsed from the remainder of the String then + * this will return false otherwise it will return + * true. + * + * @return true if there are more cookies false otherwise + */ + public boolean hasNext(){ + if(finished) { + return false; + } + if(parsed) { + return true; + } + parse(); + + if(name.len <=0){ + finished = true; + return false; + } + return true; + + } + + /** + * This method is used to remove items from the iterator. This + * however performs no action as the act of parsing should not + * modify the underlying source text value so that it can be + * reset with the reset method and used again. + */ + public void remove() { + return; + } + } + + /** + * This is a token object that is used to store the offset and + * length of a region of chars in the CookieParser.buf + * array. The toString method of this token will + * produce the String value of the region it + * represents. + */ + private class Token { + + /** + * The numer of characters that were consumed by this token. + */ + public int len; + + /** + * The offset within the buffer that this token starts from. + */ + public int off; + + /** + * This converts region within the buffer to a String. + * This converts the region only if there is a sufficient length. + * + * @return the String value of the region + */ + public String toString(){ + return new String(buf,off,len); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/DateParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/DateParser.java new file mode 100644 index 0000000..7efea9c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/DateParser.java @@ -0,0 +1,642 @@ +/* + * DateParser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import static java.util.Calendar.DAY_OF_MONTH; +import static java.util.Calendar.DAY_OF_WEEK; +import static java.util.Calendar.HOUR_OF_DAY; +import static java.util.Calendar.MILLISECOND; +import static java.util.Calendar.MINUTE; +import static java.util.Calendar.MONTH; +import static java.util.Calendar.SECOND; +import static java.util.Calendar.YEAR; + +import java.util.Calendar; +import java.util.TimeZone; + +import org.simpleframework.common.parse.Parser; + +/** + * This is used to create a Parser for the HTTP date format. + * This will parse the 3 formats that are acceptable for the HTTP/1.1 date. + * The three formats that are acceptable for the HTTP-date are + *

+ * Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
+ * Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
+ * Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
+ * 
+ *

+ * This can also parse the date in ms as retrived from the System's + * System.currentTimeMillis method. This has a parse method for a + * long which will do the same as the parse(String). + * Once the date has been parsed there are two methods that allow the date + * to be represented, the toLong method converts the date to a + * long and the toString method will convert the date + * into a String. + *

+ * This produces the same string as the SimpleDateFormat.format + * using the pattern "EEE, dd MMM yyyy hh:mm:ss 'GMT'". This will + * however do the job faster as it does not take arbitrary inputs. + * + * @author Niall Gallagher + */ +public class DateParser extends Parser { + + /** + * Ensure that the time zone for dates if set to GMT. + */ + private static final TimeZone ZONE = TimeZone.getTimeZone("GMT"); + + /** + * Contains the possible days of the week for RFC 1123. + */ + private static final String WKDAYS[] = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }; + + /** + * Contains the possible days of the week for RFC 850. + */ + private static final String WEEKDAYS[] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }; + + /** + * Contains the possible months in the year for HTTP-date. + */ + private static final String MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + /** + * Used as an index into the months array to get the month. + */ + private int month; + + /** + * Represents the decimal value of the date such as 1977. + */ + private int year; + + /** + * Represents the decimal value of the date such as 18. + */ + private int day; + + /** + * Used as an index into the weekdays array to get the weekday. + */ + private int weekday; + + /** + * Represents the decimal value of the hour such as 24. + */ + private int hour; + + /** + * Represents the decimal value of the minute. + */ + private int mins; + + /** + * Represents the decimal value for the second. + */ + private int secs; + + /** + * The default constructor will create a parser that can parse + * Strings that contain dates in the form of RFC 1123, + * RFC 850 or asctime. If the dates that are to be parsed are not in + * the form of one of these date encodings the results of this + * parser will be random. + */ + public DateParser(){ + this.init(); + } + + /** + * This constructor will conveniently parse the long argument + * in the constructor. This can also be done by first calling the no-arg + * constructor and then using the parse method. + *

+ * This will then set this object to one that uses the RFC 1123 format + * for a date. + * + * @param date the date to be parsed + */ + public DateParser(long date){ + this(); + parse(date); + } + + /** This constructor will conveniently parse the String + * argument in the constructor. This can also be done by first calling + * the no-arg constructor and then using the parse method. + *

+ * This will then set this object to one that uses the RFC 1123 format + * for a date. + * + * @param date the date to be parsed + */ + public DateParser(String date) { + this(); + parse(date); + } + + /** + * This is used to extract the date from a long. If this + * method is given the value of the date as a long it will + * construct the RFC 1123 date as required by RFC 2616 sec 3.3. + *

+ * This saves time on parsing a String that is encoded in + * the HTTP-date format. The date given must be positive, if the date + * given is not a positive 'long' then the results + * of this method is random/unknown. + * + * @param date the date to be parsed + */ + public void parse(long date){ + Calendar calendar = Calendar.getInstance(ZONE); + calendar.setTimeInMillis(date); + + weekday = calendar.get(DAY_OF_WEEK); + year = calendar.get(YEAR); + month = calendar.get(MONTH); + day = calendar.get(DAY_OF_MONTH); + hour = calendar.get(HOUR_OF_DAY); + mins = calendar.get(MINUTE); + secs = calendar.get(SECOND); + month = month > 11 ? 11: month; + weekday = (weekday+5) % 7; + } + + /** + * Convenience method used to convert the specified HTTP date in to a + * long representing the time. This is used when a single method is + * required to convert a HTTP date format to a usable long value for + * use in creating Date objects. + * + * @param date the date specified in on of the HTTP date formats + * + * @return the date value as a long value in milliseconds + */ + public long convert(String date) { + parse(date); + return toLong(); + + } + + /** + * Convenience method used to convert the specified long date in to a + * HTTP date format. This is used when a single method is required to + * convert a long data value in milliseconds to a HTTP date value. + * + * @param date the date specified as a long of milliseconds + * + * @return the date represented in the HTTP date format RFC 1123 + */ + public String convert(long date) { + parse(date); + return toString(); + } + + /** + * This is used to reset the date and the buffer variables + * for this DateParser. Every in is set to the + * value of 0. + */ + protected void init() { + month = year = day = + weekday = hour = mins = + secs = off = 0; + } + + /** + * This is used to parse the contents of the buf. This + * checks the fourth char of the buffer to see what it contains. Invariably + * a date format belonging to RFC 1123 will have a ',' character in position 4, + * a date format belonging to asctime will have a ' ' character in position 4 + * and if neither of these characters are found at position 4 then it is + * assumed that the date is in the RFC 850 fromat, however it may not be. + */ + protected void parse(){ + if(buf.length<4)return; + if(buf[3]==','){ + rfc1123(); + }else if(buf[3]==' '){ + asctime(); + }else{ + rfc850(); + } + } + + /** + * This will parse a date that is in the form of an RFC 1123 date. This + * date format is the date format that is to be used with all applications + * that are HTTP/1.1 compliant. The RFC 1123 date format is + *

+    * rfc1123 = 'wkday "," SP date1 SP time SP GMT'. 
+    * date1 = '2DIGIT SP month SP 4DIGIT' and finally
+    * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'. 
+    * 
+ */ + private void rfc1123(){ + wkday(); + off+=2; + date1(); + off++; + time(); + } + + /** + * This will parse a date that is in the form of an RFC 850 date. This date + * format is the date format that is to be used with some applications that + * are HTTP/1.0 compliant. The RFC 1123 date format is + *
+    * rfc850 = 'weekday "," SP date2 SP time SP GMT'. 
+    * date2 = '2DIGIT "-" month "-" 2DIGIT' and finally
+    * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'. 
+    * 
+ */ + private void rfc850() { + weekday(); + off+=2; + date2(); + off++; + time(); + } + + /** + * This will parse a date that is in the form of an asctime date. This date + * format is the date format that is to be used with some applications that + * are HTTP/1.0 compliant. The RFC 1123 date format is + *
+    * asctime = 'weekday SP date3 SP time SP 4DIGIT'. 
+    * date3 = 'month SP (2DIGIT | (SP 1DIGIT))' and 
+    * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'. 
+    * 
+ */ + private void asctime(){ + wkday(); + off++; + date3(); + off++; + time(); + off++; + year4(); + } + + /** + * This is the date1 format of a date that is used by the RFC 1123 + * date format. This date is + *
+    * date1 = '2DIGIT SP month SP 4DIGIT'.
+    * example '02 Jun 1982'.
+    * 
+ */ + private void date1(){ + day(); + off++; + month(); + off++; + year4(); + } + + /** + * This is the date2 format of a date that is used by the RFC 850 + * date format. This date is + *
+    * date2 = '2DIGIT "-" month "-" 2DIGIT'
+    * example '02-Jun-82'.
+    * 
+ */ + private void date2(){ + day(); + off++; + month(); + off++; + year2(); + } + + /** + * This is the date3 format of a date that is used by the asctime + * date format. This date is + *
+    * date3 = 'month SP (2DIGIT | (SP 1DIGIT))' 
+    * example 'Jun  2'.
+    * 
+    */
+   private void date3(){
+      month();
+      off++;
+      day();
+   }
+
+   /** 
+    * This is used to parse a consecutive set of digit characters to create 
+    * the day of the week. This will tolerate a space on front of the digits 
+    * thiswill allow all date formats including asctime to use this to get 
+    * the day. This may parse more than 2 digits, however if there are more 
+    * than 2 digits the date format is incorrect anyway.
+    */
+   private void day(){
+      if(space(buf[off])){
+         off++;
+      }
+      while(off < count){
+         if(!digit(buf[off])){
+            break;
+         }
+         day *= 10;
+         day += buf[off];
+         day -= '0';
+         off++;
+      }    
+   }
+
+   /** 
+    * This is used to get the year from a set of digit characters. This is 
+    * used to parse years that are of the form of 2 digits (e.g 82) however 
+    * this will assume that any dates that are in 2 digit format are dates 
+    * for the 2000 th milleneum so 01 will be 2001.
+    * 

+ * This may parse more than 2 digits but if there are more than 2 digits + * in a row then the date format is incorrect anyway. + */ + private void year2(){ + int mill = 2000; /* milleneum */ + int cent = 0; /* century */ + while(off < count){ + if(!digit(buf[off])){ + break; + } + cent *= 10; + cent += buf[off]; + cent -= '0'; + off++; + } + year= mill+cent; /* result 4 digits*/ + } + + /** + * This is used to get the year from a set of digit characters. This + * is used to parse years that are of the form of 4 digits (e.g 1982). + *

+ * This may parse more than 4 digits but if there are more than 2 + * digits in a row then the date format is incorrect anyway. + */ + private void year4() { + while(off < count){ + if(!digit(buf[off])){ + break; + } + year *= 10; + year += buf[off]; + year -= '0'; + off++; + } + } + + /** + * This is used to parse the time for a HTTP-date. The time for a + * HTTP-date is in the form 00:00:00 that is + *

+    * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT' so this will
+    * read only a time of that form, although this will
+    * parse time = '2DIGIT CHAR 2DIGIT CHAR 2DIGIT'.
+    * 
+ */ + private void time(){ + hours(); + off++; + mins(); + off++; + secs(); + } + + /** + * This is used to initialize the hour. This will read a consecutive + * sequence of digit characters and convert them into a decimal number + * to represent the hour that this date represents. + *

+ * This may parse more than 2 digits but if there are more than 2 + * digits the date is already incorrect. + */ + private void hours(){ + while(off < count){ + if(!digit(buf[off])){ + break; + } + hour *= 10; + hour += buf[off]; + hour -= '0'; + off++; + } + } + + /** + * This is used to initialize the mins. This will read a consecutive + * sequence of digit characters and convert them into a decimal number + * to represent the mins that this date represents. + *

+ * This may parse more than 2 digits but if there are more than 2 + * digits the date is already incorrect. + */ + private void mins(){ + while(off < count){ + if(!digit(buf[off])){ + break; + } + mins *= 10; + mins += buf[off]; + mins -= '0'; + off++; + } + } + + /** + * This is used to initialize the secs. This will read a consecutive + * sequence of digit characters and convert them into a decimal + * number to represent the secs that this date represents. + *

+ * This may parse more than 2 digits but if there are more than 2 + * digits the date is already incorrect + */ + private void secs(){ + while(off < count){ + if(!digit(buf[off])){ + break; + } + secs *= 10; + secs += buf[off]; + secs -= '0'; + off++; + } + } + + /** + * This is used to read the week day of HTTP-date. The shorthand day + * (e.g Mon for Monday) is used by the RFC 1123 and asctime date formats. + * This will simply try to read each day from the buffer, when the day + * is read successfully then the index of that day is saved. + */ + private void wkday(){ + for(int i =0; i < WKDAYS.length;i++){ + if(skip(WKDAYS[i])){ + weekday = i; + return; + } + } + } + + /** + * This is used to read the week day of HTTP-date. This format is used + * by the RFC 850 date format. This will simply try to read each day from + * the buffer, when the day is read successfully then the index of that + * day is saved. + */ + private void weekday(){ + for(int i =0; i < WKDAYS.length;i++){ + if(skip(WEEKDAYS[i])){ + weekday = i; + return; + } + } + } + + /** + * This is used to read the month of HTTP-date. This will simply + * try to read each month from the buffer, when the month is read + * successfully then the index of that month is saved. + */ + private void month(){ + for(int i =0; i < MONTHS.length;i++){ + if(skip(MONTHS[i])){ + month = i; + return; + } + } + } + + /** + * This is used to append the date in RFC 1123 format to the given + * string builder. This will append the date and a trailing space + * character to the buffer. Dates like the following are appended. + *

+    * Tue, 02 Jun 1982 
+    * 
. + * For performance reasons a string builder is used. This avoids + * an unneeded synchronization caused by the string buffers. + * + * @param builder this is the builder to append the date to + */ + private void date(StringBuilder builder) { + builder.append(WKDAYS[weekday]); + builder.append(", "); + + if(day <= 9) { + builder.append('0'); + } + builder.append(day); + builder.append(' '); + builder.append(MONTHS[month]); + builder.append(' '); + builder.append(year); + builder.append(' '); + } + + /** + * This is used to append the time in RFC 1123 format to the given + * string builder. This will append the time and a trailing space + * character to the buffer. Times like the following are appended. + *
+    * 23:59:59
+    * 
. + * For performance reasons a string builder is used. This avoids + * an unneeded synchronization caused by the string buffers. + * + * @param builder this is the builder to write the time to + */ + private void time(StringBuilder builder) { + if(hour <= 9) { + builder.append('0'); + } + builder.append(hour); + builder.append(':'); + + if(mins <= 9) { + builder.append('0'); + } + builder.append(mins); + builder.append(':'); + + if(secs <= 9) { + builder.append('0'); + } + builder.append(secs); + builder.append(' '); + } + + /** + * This is used to append the time zone to the provided appender. + * For HTTP the dates should always be in GMT format. So this will + * simply append the "GMT" string to the end of the builder. + * + * @param builder this builder to append the time zone to + */ + private void zone(StringBuilder builder) { + builder.append("GMT"); + } + + /** + * This returns the date in as a long, given the exact + * time this will use the java.util.Date to parse this date + * into a long. The GregorianCalendar uses + * the method getTime which produces the Date + * object from this the getTime returns the long + * + * @return the date parsed as a long + */ + public long toLong() { + Calendar calendar = Calendar.getInstance(ZONE); /* GMT*/ + calendar.set(year,month, day, hour, mins, secs); + calendar.set(MILLISECOND, 0); + + return calendar.getTime().getTime(); + } + + /** + * This prints the date in the format of a RFC 1123 date. Example + *
+    * Tue, 02 Jun 1982 23:59:59 GMT
+    * 
. + * This uses a StringBuffer to accumulate the various + * Strings/ints to form the resulting date + * value. The resulting date value is the one required by RFC 2616. + *

+ * The HTTP date must be in the form of RFC 1123. The hours, minutes + * and seconds are appended with the 0 character if they are less than + * 9 i.e. if they do not have two digits. + * + * @return the date in RFC 1123 format + */ + public String toString(){ + StringBuilder builder = new StringBuilder(30); + + date(builder); + time(builder); + zone(builder); + + return builder.toString(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/LanguageParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/LanguageParser.java new file mode 100644 index 0000000..0a2d215 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/LanguageParser.java @@ -0,0 +1,156 @@ +/* + * LanguageParser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import java.util.List; +import java.util.Locale; + +/** + * LanguageParser is used to parse the HTTP Accept-Language + * header. This takes in an Accept-Language header and parses + * it according the RFC 2616 BNF for the Accept-Language header. + * This also has the ability to sequence the language tokens in terms of + * the most preferred and the least preferred. + *

+ * This uses the qvalues outlined by RFC 2616 to order the language tokens + * by preference. Typically the language tokens will not have qvalues with + * the language. However when a language tag has the qvalue parameter then + * this tag will be ordered based on the value of that parameter. A language + * tag without the qvalue parameter is considered to have a qvalue of 1 and + * is ordered accordingly. + * + * @author Niall Gallagher + */ +public class LanguageParser extends ListParser { + + /** + * This is used to create a LanguageParser for the + * Accept-Language HTTP header value. This will + * parse a set of language tokens and there parameters. The + * languages will be ordered on preference. This constructor + * will parse the value given using parse(String). + */ + public LanguageParser() { + super(); + } + + /** + * This is used to create a LanguageParser for the + * Accept-Language HTTP header value. This will + * parse a set of language tokens and there parameters. The + * languages will be ordered on preference. This constructor + * will parse the value given using parse(String). + * + * @param text value of a Accept-Language header + */ + public LanguageParser(String text) { + super(text); + } + + /** + * This is used to create a LanguageParser for the + * Accept-Language HTTP header value. This will + * parse a set of language tokens and there parameters. The + * languages will be ordered on preference. This constructor + * will parse the value given using parse(String). + * + * @param list value of a Accept-Language header + */ + public LanguageParser(List list) { + super(list); + } + + /** + * This creates a locale object using an offset and a length. + * The locale is created from the extracted token and the offset + * and length ensure that no leading or trailing whitespace are + * within the created locale object. + * + * @param text this is the text buffer to acquire the value from + * @param start the offset within the array to take characters + * @param len this is the number of characters within the token + */ + @Override + protected Locale create(char[] text, int start, int len){ + String language = language(text, start, len); + String country = country(text, start, len); + + return new Locale(language, country); + } + + /** + * This will extract the primary language tag from the header. + * This token is used to represent the language that will be + * available in the Locale object created. + * + * @param text this is the text buffer to acquire the value from + * @param start the offset within the array to take characters + * @param len this is the number of characters within the token + */ + private String language(char[] text, int start, int len) { + int mark = start; + int size = 0; + + while(start < len) { + char next = text[start]; + + if(terminal(next)) { + return new String(text, mark, size); + } + size++; + start++; + } + return new String(text, mark, len); + } + + /** + * This will extract the primary country tag from the header. + * This token is used to represent the country that will be + * available in the Locale object created. + * + * @param text this is the text buffer to acquire the value from + * @param start the offset within the array to take characters + * @param len this is the number of characters within the token + */ + private String country(char[] text, int start, int len) { + int size = len; + + while(start < len) { + if(text[start++] == '-') { + return new String(text, start, --size); + } + size--; + } + return ""; + } + + /** + * This is used to determine whether the character provided is + * a terminal character. The terminal token is the value that is + * used to separate the country from the language and also any + * character the marks the end of the language token. + * + * @param ch this is the character that is to be evaluated + * + * @return true if the character represents a terminal token + */ + private boolean terminal(char ch) { + return ch ==' ' || ch == '-' || ch == ';'; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/ListParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ListParser.java new file mode 100644 index 0000000..166a2aa --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ListParser.java @@ -0,0 +1,456 @@ +/* + * ListParser.java September 2003 + * + * Copyright (C) 2003, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import static java.lang.Long.MAX_VALUE; + +import java.util.ArrayList; +import java.util.List; +import java.util.PriorityQueue; + +import org.simpleframework.common.parse.Parser; + +/** + * The ListParser is used to extract a comma separated + * list of HTTP header values. This will extract values without + * any leading or trailing spaces, which enables the values to be + * used. Listing the values that appear in the header also requires + * that the values are ordered. This orders the values using the + * values that appear with any quality parameter associated with it. + * The quality value is a special parameter that often found in a + * comma separated value list to specify the client preference. + *

+ * 
+ *    image/gif, image/jpeg, text/html
+ *    image/gif;q=1.0, image/jpeg;q=0.8, image/png;  q=1.0,*;q=0.1
+ *    gzip;q=1.0, identity; q=0.5, *;q=0
+ *
+ * 
+ * The above lists taken from RFC 2616 provides an example of the + * common form comma separated values take. The first illustrates + * a simple comma delimited list, here the ordering of values is + * determined from left to right. The second and third list have + * quality values associated with them, these are used to specify + * a preference and thus order. + *

+ * Each value within a list has an implicit quality value of 1.0. + * If the value is explicitly set with a the "q" parameter, then + * the values can range from 1.0 to 0.001. This parser ensures + * that the order of values returned from the list + * method adheres to the optional quality parameters and ensures + * that the quality parameters a removed from the resulting text. + * + * @author Niall Gallagher + */ +public abstract class ListParser extends Parser { + + /** + * Provides a quick means of sorting the values extracted. + */ + private PriorityQueue order; + + /** + * Contains all the values extracted from the header(s). + */ + private List list; + + /** + * This is used as a working space to parse the value. + */ + private char[] text; + + /** + * The quality associated with an individual value. + */ + private long qvalue; + + /** + * Used to index into the write offset for the value. + */ + private int pos; + + /** + * This is used to determine whether to gather tokens. + */ + private boolean build; + + /** + * Constructor for the ListParser. This creates a + * parser with no initial parse data, if there are headers to + * be parsed then the parse(String) method or + * parse(List) method can be used. This will + * parse a delimited list according so RFC 2616 section 4.2. + */ + public ListParser(){ + this.order = new PriorityQueue(); + this.list = new ArrayList(); + this.text = new char[0]; + } + + /** + * Constructor for the ListParser. This creates a + * parser with the text supplied. This will parse the comma + * separated list according to RFC 2616 section 2.1 and 4.2. + * The tokens can be extracted using the list + * method, which will also sort and trim the tokens. + * + * @param text this is the comma separated list to be parsed + */ + public ListParser(String text) { + this(); + parse(text); + } + + /** + * Constructor for the ListParser. This creates a + * parser with the text supplied. This will parse the comma + * separated list according to RFC 2616 section 2.1 and 4.2. + * The tokens can be extracted using the list + * method, which will also sort and trim the tokens. + * + * @param list a list of comma separated lists to be parsed + */ + public ListParser(List list) { + this(); + parse(list); + } + + /** + * This allows multiple header values to be represented as one + * single comma separated list. RFC 2616 states that multiple + * message header fields with the same field name may be present + * in a message if and only if the entire field value for that + * header field is defined as a comma separated list. This means + * that if there are multiple header values with the same name + * they can be combined into a single comma separated list. + * + * @param list this is a list of header values to be combined + */ + public void parse(List list) { + for(String value : list) { + parse(value); + build = true; + } + build = false; + } + + /** + * This will build an ordered list of values extracted from the + * comma separated header value. This enables the most preferred + * token, to be taken from the first index of the array and the + * least preferred token to be taken from the last index. + * + * @return tokens parsed from the list ordered by preference + */ + public List list() { + return list; + } + + /** + * This is used to remove the String tokens from + * the priority queue and place those tokens in an array. The + * The String tokens are placed into the array + * in an ordered manner so that the most preferred token is + * inserted into the start of the list. + */ + private void build() { + while(!order.isEmpty()) { + Entry entry = order.remove(); + T value = entry.getValue(); + + list.add(value); + } + } + + /** + * This ensures that tokens are taken from the comma separated + * list as long as there bytes left to be examined within the + * source text. This also makes sure that the implicit qvalue + * is decreased each time a token is extracted from the list. + */ + protected void parse() { + while(off < count) { + clear(); + value(); + save(); + } + build(); + } + + /** + * Initializes the parser so that tokens can be extracted from + * the list. This creates a write buffer so that a if there is + * only one token as long as the source text, then that token + * can be accommodated, also this starts of the initial qvalue + * implicit to tokens within the list as the maximum long value. + *

+ * One thing that should be noted is that this will not empty + * the priority queue on each string parsed. This ensures that + * if there are multiple strings they can be parsed quickly + * and also contribute to the final result. + */ + protected void init(){ + if(text.length < count){ + text = new char[count]; + } + if(!build) { + list.clear(); + } + pos = off = 0; + order.clear(); + } + + /** + * This is used to return the parser to a semi-initialized state. + * After extracting a token from the list the buffer will have + * accumulated bytes, this ensures that bytes previously written + * to the buffer do not interfere with the next token extracted. + *

+ * This also ensures the implicit qvalue is reset to the maximum + * long value, so that the next token parsed without a qvalue + * will have the highest priority and be placed at the top of + * the list. This ensures order is always maintained. + */ + private void clear() { + qvalue = MAX_VALUE; + pos = 0; + } + + /** + * This method will extract a token from a comma separated list + * and write it to a buffer. This performs the extraction in such + * a way that it can tolerate literals, parameters, and quality + * value parameters. The only alterations made to the token by + * this method is the removal of quality values, that is, qvalue + * parameters which have the name "q". Below is an example of + * some of the lists that this can parse. + *

+    *
+    *    token; quantity=1;q=0.001, token; text="a, b, c, d";q=0
+    *    image/gif, , image/jpeg, image/png;q=0.8, *
+    *    token="\"a, b, c, d\", a, b, c, d", token="a";q=0.9,,
+    *    
+    * 
+ * This will only interpret a comma delimiter outside quotes of + * a literal. So if there are comma separated tokens that have + * quoted strings, then commas within those quoted strings will + * not upset the extraction of the token. Also escaped strings + * are tolerated according to RFC 2616 section 2. + */ + private void value() { + parse: while(off < count) { + if(buf[off++] == '"'){ /* "[t]ext" */ + text[pos++] = buf[off-1]; /* ["]text"*/ + while(++off < count){ /* "text"[] */ + if(buf[off -1] =='"'){ /* "text["] */ + if(buf[off -2] !='\\') + break; + } + text[pos++] = buf[off-1]; /* "tex[t]"*/ + } + } else if(buf[off -1] == ';'){ /* [;] q=0.1 */ + for(int seek = off; seek+1 < count;){/* ;[ ]q=0.1 */ + if(!space(buf[seek])){ /* ;[ ]q=0.1*/ + if(buf[seek] =='q'){ /* ; [q]=0.1*/ + if(buf[seek+1] =='='){ /* ; q[=]0.1*/ + off = seek; + qvalue(); + continue parse; + } + } + break; + } + seek++; + } + } + if(buf[off-1] ==','){ + break; + } + text[pos++] = buf[off-1]; + } + } + + /** + * This method will trim whitespace from the extracted token and + * store that token within the PriorityQueue. This + * ensures that the tokens parsed from the comma separated list + * can be used. Trimming the whitespace is something that will be + * done to the tokens so that they can be examined, so this + * ensures that the overhead of the String.trim + * method is not required to remove trailing or leading spaces. + * This also ensures that empty tokens are not saved. + */ + private void save() { + int size = pos; + int start = 0; + + while(size > 0){ + if(!space(text[size-1])){ + break; + } + size--; + } + while(start < pos){ + if(space(text[start])){ + start++; + size--; + }else { + break; + } + } + if(size > 0) { + T value = create(text, start, size); + + if(value != null) { + save(value); + } + } + } + + /** + * This stores the string in the PriorityQueue. If + * the qvalue extracted from the header value is less that 0.001 + * then this will not store the token. This ensures that client + * applications can specify tokens that are unacceptable to it. + * + * @param value this is the token to be enqueued into the queue + */ + private void save(T value) { + int size = order.size(); + + if(qvalue > 0) { + order.offer(new Entry(value, qvalue, size)); + } + } + + /** + * This is used to extract the qvalue parameter from the header. + * The qvalue parameter is identified by a parameter with the + * name "q" and a numeric floating point number. The number can + * be in the range of 0.000 to 1.000. The qvalue + * is parsed byte bit shifting a byte in to a value in to a + * long, this may cause problems with varying accuracy. + */ + private void qvalue() { + if(skip("q=")){ + char digit = 0; + + for(qvalue = 0; off < count;){ + if(buf[off] == '.'){ + off++; + continue; + } + if(!digit(buf[off])){ + break; + } + digit = buf[off]; + digit -= '0'; + qvalue |= digit; + qvalue <<= 4; + off++; + } + } + } + + /** + * This creates an value object using the range of characters + * that have been parsed as an item within the list of values. It + * is up to the implementation to create a value to insert in to + * the list. A null value will be ignored if returned. + * + * @param text this is the text buffer to acquire the value from + * @param start the offset within the array to take characters + * @param len this is the number of characters within the token + */ + protected abstract T create(char[] text, int start, int len); + + /** + * The Entry object provides a comparable object to + * insert in to a priority queue. This will sort the value using + * the quality value parameter parsed from the list. If there + * are values with the same quality value this this will sort + * the values by a secondary order parameter. + */ + private class Entry implements Comparable { + + /** + * This is the value that is represented by this entry. + */ + private final T value; + + /** + * This is the priority value that is used to sort entries. + */ + private final long priority; + + /** + * This is the secondary order value used to sort entries. + */ + private final int order; + + /** + * Constructor for the Entry object. This is used + * to create a comparable value that can be inserted in to a + * priority queue and extracted in order of the priority value. + * + * @param value this is the value that is represented by this + * @param priority this is the priority value for sorting + * @param order this is the secondary priority value used + */ + public Entry(T value, long priority, int order) { + this.priority = priority; + this.order = order; + this.value = value; + } + + /** + * This acquires the value represented by this entry. This is + * can be used to place the value within a list as it is taken + * from the priority queue. Acquiring the values in this way + * facilitates a priority ordered list of values. + * + * @return this returns the value represented by this + */ + public T getValue() { + return value; + } + + /** + * This is used to sort the entries within the priority queue + * using the provided priority of specified. If the entries + * have the same priority value then they are sorted using a + * secondary order value, which is the insertion index. + * + * @param entry this is the entry to be compared to + * + * @return this returns the result of the entry comparison + */ + public int compareTo(Entry entry) { + long value = entry.priority - priority; + + if(value > 0) { + return 1; + } + if(value < 0) { + return -1; + } + return order - entry.order; + } + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/PathParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/PathParser.java new file mode 100644 index 0000000..7055e4e --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/PathParser.java @@ -0,0 +1,726 @@ +/* + * PathParser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import org.simpleframework.common.parse.Parser; +import org.simpleframework.http.Path; + +/** + * This is used to parse a path given as part of a URI. This will read the + * path, normalize it, and break it up into its components. The normalization + * of the path is the conversion of the path given into it's actual path by + * removing the references to the parent directories and to the current dir. + *

+ * If the path that this represents is /usr/bin/../etc/./README + * then the actual path, normalized, is /usr/etc/README. Once + * the path has been normalized it is possible to acquire the segments as + * an array of strings, which allows simple manipulation of the path. + *

+ * Although RFC 2396 defines the path within a URI to have parameters this + * does not extract those parameters this will simply normalize the path and + * include the path parameters in the path. If the path is to be converted + * into a OS specific file system path that has the parameters extracted + * then the AddressParser should be used. + * + * @author Niall Gallagher + */ +public class PathParser extends Parser implements Path{ + + /** + * Used to store the individual path segments. + */ + private TokenList list; + + /** + * Used to store consumed name characters. + */ + private Token name; + + /** + * Used to store consumed file extension. + */ + private Token ext; + + /** + * Used to store the highest directory path. + */ + private Token dir; + + /** + * Used to store consumed normalized path name. + */ + private Token path; + + /** + * The default constructor will create a PathParser that + * contains no specifics. The instance will return null + * for all the get methods. The PathParser's get methods + * may be populated by using the parse method. + */ + public PathParser() { + this.list = new TokenList(); + this.ext = new Token(); + this.dir = new Token(); + this.path = new Token(); + this.name = new Token(); + } + + /** + * This is primarily a convineance constructor. This will parse the + * String given to extract the specifics. This could be + * achived by calling the default no-arg constructor and then using + * the instance to invoke the parse method on that + * String to extract the parts. + * + * @param path a String containing a path value + */ + public PathParser(String path){ + this(); + parse(path); + } + + /** + * This will parse the path in such a way that it ensures that at no + * stage there are trailing back references, using path normalization. + * The need to remove the back references is so that this + * PathParser will create the same String + * path given a set of paths that have different back references. For + * example the paths /path/../path and /path + * are the same path but different String's. + *

+ * This will NOT parse an immediate back reference as this signifies + * a path that cannot exist. So a path such as /../ will + * result in a null for all methods. Paths such as ../bin + * will not be allowed. + */ + protected void parse() { + normalize(); + path(); + segments(); + name(); + extension(); + } + + /** + * This will initialize the parser so that it is in a ready state. + * This allows the parser to be used to parse many paths. This will + * clear the parse buffer objects and reset the offset to point to + * the start of the char buffer. The count variable is reset by the + * Parser.parse method. + */ + protected void init() { + list.clear(); + ext.clear(); + dir.clear(); + name.clear(); + path.clear(); + off = 0; + } + + /** + * This will return the extension that the file name contains. + * For example a file name file.en_US.extension + * will produce an extension of extension. This + * will return null if the path contains no file extension. + * + * @return this will return the extension this path contains + */ + public String getExtension() { + return ext.toString(); + } + + /** + * This will return the full name of the file without the path. + * As regargs the definition of the path in RFC 2396 the name + * would be considered the last path segment. So if the path + * was /usr/README the name is README. + * Also for directorys the name of the directory in the last + * path segment is returned. This returns the name without any + * of the path parameters. As RFC 2396 defines the path to have + * path parameters after the path segments. + * + * @return this will return the name of the file in the path + */ + public String getName(){ + return name.toString(); + } + + /** + * This will return the normalized path. The normalized path is + * the path without any references to its parent or itself. So + * if the path to be parsed is /usr/../etc/./ the + * path is /etc/. If the path that this represents + * is a path with an immediate back reference then this will + * return null. This is the path with all its information even + * the parameter information if it was defined in the path. + * + * @return this returns the normalize path without + * ../ or ./ + */ + public String getPath() { + return path.toString(); + } + + /** + * This will return the normalized path from the specified path + * segment. This allows various path parts to be acquired in an + * efficient means what does not require copy operations of the + * use of substring invocations. Of particular + * interest is the extraction of context based paths. This is + * the path with all its information even the parameter + * information if it was defined in the path. + * + * @param from this is the segment offset to get the path for + * + * @return this returns the normalize path without + * ../ or ./ + */ + public String getPath(int from) { + return list.segment(from); + } + + /** + * This will return the normalized path from the specified path + * segment. This allows various path parts to be acquired in an + * efficient means what does not require copy operations of the + * use of substring invocations. Of particular + * interest is the extraction of context based paths. This is + * the path with all its information even the parameter + * information if it was defined in the path. + * + * @param from this is the segment offset to get the path for + * @param count this is the number of path segments to include + * + * @return this returns the normalize path without + * ../ or ./ + */ + public String getPath(int from, int count) { + return list.segment(from, count); + } + + /** + * This will return the highest directory that exists within + * the path. This is used to that files within the same path + * can be acquired. An example of that this would do given + * the path /pub/./bin/README would be to return + * the highest directory path /pub/bin/. The "/" + * character will allways be the last character in the path. + * + * @return this method will return the highest directory + */ + public String getDirectory(){ + return dir.toString(); + } + + /** + * This method is used to break the path into individual parts + * called segments, see RFC 2396. This can be used as an easy + * way to compare paths and to examine the directory tree that + * the path points to. For example, if an path was broken from + * the string /usr/bin/../etc then the segments + * returned would be usr and etc as + * the path is normalized before the segments are extracted. + * + * @return return all the path segments within the directory + */ + public String[] getSegments(){ + return list.list(); + } + + /** + * This will return the path as it is relative to the issued + * path. This in effect will chop the start of this path if + * it's start matches the highest directory of the given path + * as of getDirectory. This is useful if paths + * that are relative to a specific location are required. To + * illustrate what this method will do the following example + * is provided. If this object represented the path string + * /usr/share/rfc/rfc2396.txt and the issued + * path was /usr/share/text.txt then this will + * return the path string /rfc/rfc2396.txt. + * + * @param path the path prefix to acquire a relative path + * + * @return returns a path relative to the one it is given + * otherwize this method will return null + */ + public String getRelative(String path){ + return getRelative(new PathParser(path)); + } + + /** + * This is used by the getRelative(String) to + * normalize the path string and determine if it contains a + * highest directory which is shared with the path that is + * represented by this object. If the path has leading back + * references, such as ../, then the result of + * this is null. The returned path begins with a '/'. + * + * @param path the path prefix to acquire a relative path + * + * @return returns a path relative to the one it is given + * otherwize this method will return null + */ + private String getRelative(PathParser path){ + char[] text = path.buf; + int off = path.dir.off; + int len = path.dir.len; + + return getRelative(text, off, len); + } + + /** + * This will return the path as it is relative to the issued + * path. This in effect will chop the start of this path if + * it's start matches the highest directory of the given path + * as of getDirectory. This is useful if paths + * that are relative to a specific location are required. To + * illustrate what this method will do the following example + * is provided. If this object represented the path string + * /usr/share/rfc/rfc2396.txt and the issued + * path was /usr/share/text.txt then this will + * return the path string /rfc/rfc2396.txt. + * + * @param text the path prefix to acquire a relative path + * @param off this is the offset within the text to read + * @param len this is the number of characters in the path + * + * @return returns a path relative to the one it is given + * otherwize this method will return null + */ + private String getRelative(char[] text, int off, int len){ + if (len > path.len) { + return null; + } + int size = path.len - len + 1; /* '/' */ + int pos = path.off + len - 1; + + for(int i = 0; i < len; i++){ + if(text[off++] != buf[path.off+i]){ + return null; + } + } + if(pos < 0) { /* ../ */ + return null; + } + return new String(buf,pos,size); + } + + /** + * This will extract the path of the given String + * after it has been normalized. If the path can not be normalized + * then the count is set to -1 and the path cannot be extracted. + * When this happens then the path parameter is null. + */ + private void path() { + if(count > 0){ + path.len = count; + path.off = 0; + } + } + + /** + * This will simply read the characters from the end of the + * buffer until it encounters the first peroid character. When + * this is read it will store the file extension and remove the + * characters from the buffer. + */ + private void extension() { + int pos = off + count; /* index.html[]*/ + int len = 0; + + while(pos-1 >= off) { /* index.htm[l]*/ + if(buf[--pos]=='.'){ /* index[.]html*/ + ext.off = pos+1; + ext.len = len; + count = pos; + break; + } + len++; + } + } + + /** + * This wil extract each individual segment from the path and + * also extract the highest directory. The path segments are + * basically the strings delimited by the '/' character of a + * normalized path. As well as extracting the path segments + * this will also extract the directory of path, that is, the + * the path up to the last occurance of the '/' character. + */ + private void segments() { + int pos = count - 1; + int len = 1; + + if(count > 0){ + if(buf[pos] == '/'){ /* /pub/bin[/] */ + dir.len = pos+1; + dir.off = 0; + pos--; /* /pub/bi[n]/ */ + } + while(pos >= off){ + if(buf[pos] == '/'){ /* /pub[/]bin/*/ + if(dir.len == 0){ + dir.len = pos+1; /* [/] is 0*/ + dir.off = 0; + } + list.add(pos+1,len-1); + len = 0; + } + len++; + pos--; + } + } + } + + /** + * The normalization of the path is the conversion of the path + * given into it's actual path by removing the references to + * the parent directorys and to the current dir. So if the path + * given was /usr/bin/../etc/./README then the actual + * path, the normalized path, is /usr/etc/README. + *

+ * This method ensures the if there are an illegal number of back + * references that the path will be evaluated as empty. This can + * evaluate any path configuration, this includes any references + * like ../ or /.. within the path. + */ + private void normalize(){ + int size = count + off; + int pos = off; + + for(off = count = 0; pos < size; pos++) { + buf[count++] = buf[pos]; + + if(buf[pos] == '.') { /* //[.]/path/ */ + if(count -1 > 0) { /* /[/]./path/ */ + if(buf[count - 2] !='/') /* /[/]./path./ */ + continue; /* /path.[/] */ + } + if(pos + 2 > size){ /* /path/[.] */ + count--; + } else { + if(buf[pos + 1] =='/'){ /* /.[/]path */ + pos++;/* /[/]. */ + count--; /* /.[/]path */ + } + if(buf[pos] !='.'){ /* /.[/]path */ + continue; + } + if(pos + 2< size){ + if(buf[pos + 2]!='/') /* /..[p]ath */ + continue; /* /[.].path */ + } + if(count - 2 > 0) { + for(count -= 2; count - 1 > 0;){ /* /path[/]..*/ + if(buf[count - 1]=='/') { /* [/]path/..*/ + break; + } + count--; + } + }else { /* /../ */ + count = 0; + off = 0; + break; + } + pos += 2; /* /path/.[.]/ */ + } + } + } + } + + /** + * This will extract the full name of the file without the path. + * As regards the definition of the path in RFC 2396 the name + * would be considered the last path segment. So if the path + * was /usr/README the name is README. + * Also for directorys the name of the directory in the last + * path segment is returned. This returns the name without any + * of the path parameters. As RFC 2396 defines the path to have + * path parameters after the path segments. So the path for the + * directory "/usr/bin;param=value/;param=value" would result + * in the name "bin". If the path given was "/" then there will + * be nothing in the buffer because extract will + * have removed it. + */ + private void name(){ + int pos = count; + int len = 0; + + while(pos-- > off) { /* /usr/bin/;para[m] */ + if(buf[pos]==';'){ /* /usr/bin/[;]param */ + if(buf[pos-1]=='/'){ /* /usr/bin[/];param */ + pos--; /* /usr/bin[/];param */ + } + len = 0; /* /usr/bin[/]*/ + }else if(buf[pos]=='/'){ /* /usr[/]bin*/ + off = pos + 1; /* /usr/[b]in*/ + count = len; /* [b]in */ + break; + }else{ + len++; + } + } + name.len = count; + name.off = off; + } + + /** + * This will return the normalized path. The normalized path is + * the path without any references to its parent or itself. So + * if the path to be parsed is /usr/../etc/./ the + * path is /etc/. If the path that this represents + * is a path with an immediate back reference then this will + * return null. This is the path with all its information even + * the parameter information if it was defined in the path. + * + * @return this returns the normalize path without + * ../ or ./ + */ + public String toString(){ + return getPath(); + } + + /** + * This is used so that the PathParser can speed + * up the parsing of the data. Rather than using a buffer like + * a ParseBuffer or worse a StringBuffer + * this just keeps an index into the character array from the + * start and end of the token. Also this enables a cache to be + * kept so that a String does not need to be made + * again after the first time it is created. + */ + private class Token { + + /** + * Provides a quick retrieval of the token value. + */ + public String value; + + /** + * Offset within the buffer that the token starts. + */ + public int off; + + /** + * Length of the region that the token consumes. + */ + public int len; + + /** + * If the Token is to be reused this will clear + * all previous data. Clearing the buffer allows it to be + * reused if there is a new URI to be parsed. This ensures + * that a null is returned if the token length is zero. + */ + public void clear() { + value = null; + len = 0; + } + + /** + * This method will convert the Token into it's + * String equivelant. This will firstly check + * to see if there is a value, for the string representation, + * if there is the value is returned, otherwise the region + * is converted into a String and returned. + * + * @return this returns a value representing the token + */ + public String toString() { + if(value != null) { + return value; + } + if(len > 0) { + value = new String(buf,off,len); + } + return value; + } + } + + /** + * The TokenList class is used to store a list of + * tokens. This provides an add method which can + * be used to store an offset and length of a token within + * the buffer. Once the tokens have been added to they can be + * examined, in the order they were added, using the provided + * list method. This has a scalable capacity. + */ + private class TokenList { + + /** + * This is used to cache the segments that are created. + */ + private String[] cache; + + /** + * Contains the offsets and lengths of the tokens. + */ + private int[] list; + + /** + * Determines the write offset into the array. + */ + private int count; + + /** + * Constructor for the TokenList is used to + * create a scalable list to store tokens. The initial + * list is created with an array of sixteen ints, which + * is enough to store eight tokens. + */ + private TokenList(){ + list = new int[16]; + } + + /** + * This is used to acquire the path from the segment that + * is specified. This provides an efficient means to get + * the path without having to perform expensive copy of + * substring operations. + * + * @param from this is the path segment to get the path + * + * @return the string that is the path segment created + */ + public String segment(int from) { + int total = count / 2; + int left = total - from; + + return segment(from, left); + } + + /** + * This is used to acquire the path from the segment that + * is specified. This provides an efficient means to get + * the path without having to perform expensive copy of + * substring operations. + * + * @param from this is the path segment to get the path + * @param total this is the number of segments to use + * + * @return the string that is the path segment created + */ + public String segment(int from, int total) { + int last = list[0] + list[1] + 1; + + if(from + total < count / 2) { + last = offset(from + total); + } + int start = offset(from); + int length = last - start; + + return new String(buf, start-1, length); + } + + /** + * This is used to acquire the offset within the buffer + * of the specified segment. This allows a path to be + * created that is constructed from a given segment. + * + * @param segment this is the segment offset to use + * + * @return this returns the offset start for the segment + */ + private int offset(int segment) { + int last = count - 2; + int shift = segment * 2; + int index = last - shift; + + return list[index]; + } + + /** + * This is used to add a new token to the list. Tokens + * will be available from the list method in + * the order it was added, so the first to be added will + * at index zero and the last with be in the last index. + * + * @param off this is the read offset within the buffer + * @param len the number of characters within the token + */ + public void add(int off, int len){ + if(count+1 > list.length) { + resize(count *2); + } + list[count++] = off; + list[count++] = len; + } + + /** + * This is used to retrieve the list of tokens inserted + * to this list using the add method. The + * indexes of the tokens represents the order that the + * tokens were added to the list. + * + * @return returns an ordered list of token strings + */ + public String[] list(){ + if(cache == null) { + cache = build(); + } + return cache; + } + + /** + * This is used to retrieve the list of tokens inserted + * to this list using the add method. The + * indexes of the tokens represents the order that the + * tokens were added to the list. + * + * @return returns an ordered list of token strings + */ + private String[] build(){ + String[] value = new String[count/2]; + + for(int i =0, j = count/2; i< count; i+=2){ + int index = j - (i/2) - 1; + int off = list[i]; + int size = list[i + 1]; + + value[index] = new String(buf, off, size); + } + return value; + } + + /** + * This is used to clear all tokens previously stored + * in the list. This is required so that initialization + * of the parser with the init method can + * ensure that there are no tokens from previous data. + */ + public void clear(){ + cache =null; + count =0; + } + + /** + * Scales the internal array used should the number of + * tokens exceed the initial capacity. This will just + * copy across the ints used to represent the token. + * + * @param size length the capacity is to increase to + */ + private void resize(int size){ + int[] copy = new int[size]; + System.arraycopy(list,0,copy,0,count); + list = copy; + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/PrincipalParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/PrincipalParser.java new file mode 100644 index 0000000..52aeff8 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/PrincipalParser.java @@ -0,0 +1,362 @@ +/* + * PrincipalParser.java February 2001 + * + * Copyright (C) 2001, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import org.simpleframework.common.parse.ParseBuffer; +import org.simpleframework.common.parse.Parser; +import org.simpleframework.http.Principal; + +/** + * PrincipalParser is a parser class for the HTTP basic authorization + * header. It decodes the base64 encoding of the user and + * password pair. + *

+ * This follows the parsing tree of RFC 2617. The goal of this parser + * is to decode the base64 encoding of the user name and + * password. After the string has been decoded then the user name and + * password are extracted. This will only parse headers that are from + * the Basic authorization scheme. The format of the basic + * scheme can be found in RFC 2617 and is of the form + *

+ *  Basic SP base64-encoding.
+ * 
+ * + * @author Niall Gallagher + */ +public class PrincipalParser extends Parser implements Principal { + + /** + * Keeps the characters consumed for the password token. + */ + private ParseBuffer password; + + /** + * Keeps the characters consumed for the user name token. + */ + private ParseBuffer user; + + /** + * Keeps the bytes used for decoding base64. + */ + private byte[] four; + + /** + * Tracks the write offset for the buffer. + */ + private int write; + + /** + * Tracks the ready offset for the four buffer. + */ + private int ready; + + /** + * Tracks the read offset for the buffer. + */ + private int read; + + /** + * Creates a Parser for the basic authorization + * scheme. This allows headers that are of this scheme to be + * broken into its component parts i.e. user name and password. + */ + public PrincipalParser() { + this.password = new ParseBuffer(); + this.user = new ParseBuffer(); + this.four = new byte[4]; + } + + /** + * Creates a Parser for the basic authorization + * scheme. This allows headers that are of this scheme to be + * broken into its component parts i.e. user name and password. + * This constructor will parse the String given as + * the header. + * + * @param header this is a header value from the basic scheme + */ + public PrincipalParser(String header){ + this(); + parse(header); + } + + /** + * Gets the users password parsed from the Authorization + * header value. If there was not password parsed from the + * base64 value of the header this returns null + * + * @return the password for the user or null + */ + public String getPassword(){ + if(password.length() == 0){ + return null; + } + return password.toString(); + } + + /** + * Gets the users name from the Authorization header value. + * This will return null if there is no user + * name extracted from the base64 header value. + * + * @return this returns the name of the user + */ + public String getName(){ + if(user.length() == 0){ + return null; + } + return user.toString(); + } + + /** + * Used to parse the actual header data. This will attempt to + * read the "Basic" token from the set of characters given, if + * this is successful then the username and password is + * extracted. + */ + protected void parse(){ + if(skip("Basic ")){ + decode(); + userpass(); + } + } + + /** + * This will initialize the Parser when it is ready + * to parse a new String. This will reset the + * Parser to a ready state. The init method + * is invoked by the Parser when the parse + * method is invoked. + */ + protected void init() { + password.clear(); + user.clear(); + write = ready = + read = off = 0; + pack(); + } + + /** + * This is used to remove all whitespace characters from the + * String excluding the whitespace within literals. + * The definition of a literal can be found in RFC 2616. + *

+ * The definition of a literal for RFC 2616 is anything between 2 + * quotes but excuding quotes that are prefixed with the backward + * slash character. + */ + private void pack() { + int len = count; + int seek = 0; /* read */ + int pos = 0; /* write */ + char ch = 0; + + while(seek name : password pair that was given. This + * will take all data up to the first occurence of a + * ':' character as the users name and all data after the + * colon as the users password. + */ + private void userpass(){ + userid(); + off++; + password(); + } + + /** + * Extracts the user name from the buffer. This will read up to + * the first occurence of a colon, ':', character as the user + * name. For the BNF syntax of this see RFC 2617. + */ + private void userid(){ + while(off < count){ + char ch = buf[off]; + if(!text(ch) || ch ==':'){ + break; + } + user.append(ch); + off++; + } + + } + + /** + * Extracts the password from the buffer. This will all characters + * from the current offset to the first non text character as the + * password. For the BNF syntax of this see RFC 2617. + */ + private void password() { + while(off < count){ + char ch = buf[off]; + if(!text(ch)){ + break; + } + password.append(ch); + off++; + } + } + + /** + * This is used to remove decode the base64 encoding of + * the user name and password. This uses a standart base64 + * decoding scheme. + *

+ * For information on the decoding scheme used for base64 + * see the RFC 2045 on MIME, Multipurpose Internet Mail Extensions. + */ + private void decode() { + for(write = read = off; read + 3 < count;) { + while(ready < 4) { + int ch = translate(buf[read++]); + if(ch >= 0) { + four[ready++] = (byte)ch; + } + } + if(four[2] == 65) { + buf[write++] = first(four); + break; + } else if(four[3] == 65) { + buf[write++] = first(four); + buf[write++] = second(four); + break; + } else { + buf[write++] = first(four); + buf[write++] = second(four); + buf[write++] = third(four); + } + ready = 0; + } + count = write; + } + + /** + * This uses a basic translation from the byte character to the + * byte number. + *

+ * The table for translation the data can be found in RFC 2045 on + * MIME, Multipurpose Internet Mail Extensions. + * + * @param octet this is the octet ttat is to be translated + * + * @return this returns the translated octet + */ + private int translate(int octet) { + if(octet >= 'A' && octet <= 'Z') { + octet = octet - 'A'; + } else if(octet >= 'a' && octet <= 'z') { + octet = (octet - 'a') + 26; + } else if(octet >= '0' && octet <= '9') { + octet = (octet - '0') + 52; + } else if(octet == '+') { + octet = 62; + } else if(octet == '/') { + octet = 63; + } else if(octet == '=') { + octet = 65; + } else { + octet = -1; + } + return octet; + } + + /** + * This is used to extract the byte from the set of four + * bytes given. This method is used to isolate the correct + * bits that corrospond to an actual character withing the + * base64 data. + * + * @param four this is the four bytes that the character + * is to be extracted from + * + * @return this returns the character extracted + */ + private char first(byte[] four) { + return (char)(((four[0] & 0x3f) << 2) | ((four[1] & 0x30) >>> 4)); + } + + /** + * This is used to extract the byte from the set of four + * bytes given. This method is used to isolate the correct + * bits that corrospond to an actual character withing the + * base64 data. + * + * @param four this is the four bytes that the character + * is to be extracted from + * + * @return this returns the character extracted + + */ + private char second(byte[] four) { + return (char)(((four[1] & 0x0f) << 4) | ((four[2] &0x3c) >>> 2)); + } + + /** + * This is used to extract the byte from the set of four + * bytes given. This method is used to isolate the correct + * bits that corrospond to an actual character withing the + * base64 data. + * + * @param four this is the four bytes that the character + * is to be extracted from + * + * @return this returns the character extracted + */ + private char third(byte[] four) { + return (char)(((four[2] & 0x03) << 6) | (four[3] & 0x3f)); + } + + /** + * This is used to determine wheather or not a character is a + * TEXT character according to the HTTP specification, + * that is RFC 2616 specifies a TEXT character as one + * that is any octet except those less than 32 and not 127. + * + * @param c this is the character that is to be determined + * + * @return this returns true if the character is a TEXT + */ + private boolean text(char c){ + return c > 31 && c != 127 && c <= 0xffff; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/QueryParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/QueryParser.java new file mode 100644 index 0000000..56b6788 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/QueryParser.java @@ -0,0 +1,636 @@ +/* + * QueryParser.java December 2002 + * + * Copyright (C) 2002, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import org.simpleframework.common.parse.MapParser; +import org.simpleframework.http.Query; + +import java.net.URLEncoder; +import java.util.Set; + +/** + * The ParameterParser is used to parse data encoded in + * the application/x-www-form-urlencoded MIME type. It + * is also used to parse a query string from a HTTP URL, see RFC 2616. + * The parsed parameters are available through the various methods of + * the org.simpleframework.http.net.Query interface. The + * syntax of the parsed parameters is described below in BNF. + *

+ *
+ *    params  = *(pair [ "&" params])
+ *    pair    = name "=" value
+ *    name    = *(text | escaped)
+ *    value   = *(text | escaped)
+ *    escaped = % HEX HEX
+ *
+ * 
+ * This will consume all data found as a name or value, if the data + * is a "+" character then it is replaced with a space character. + * This regards only "=", "&", and "%" as having special values. + * The "=" character delimits the name from the value and the "&" + * delimits the name value pair. The "%" character represents the + * start of an escaped sequence, which consists of two hex digits. + * All escaped sequences are converted to its character value. + * + * @author Niall Gallagher + */ +public class QueryParser extends MapParser implements Query { + + /** + * Used to accumulate the characters for the parameter name. + */ + private Token name; + + /** + * Used to accumulate the characters for the parameter value. + */ + private Token value; + + /** + * Constructor for the ParameterParser. This creates + * an instance that can be use to parse HTML form data and URL + * query strings encoded as application/x-www-form-urlencoded. + * The parsed parameters are made available through the interface + * org.simpleframework.util.net.Query. + */ + public QueryParser(){ + this.name = new Token(); + this.value = new Token(); + } + + /** + * Constructor for the ParameterParser. This creates + * an instance that can be use to parse HTML form data and URL + * query strings encoded as application/x-www-form-urlencoded. + * The parsed parameters are made available through the interface + * org.simpleframework.util.net.Query. + * + * @param text this is the text to parse for the parameters + */ + public QueryParser(String text){ + this(); + parse(text); + } + + /** + * This extracts an integer parameter for the named value. If the + * named parameter does not exist this will return a zero value. + * If however the parameter exists but is not in the format of a + * decimal integer value then this will throw an exception. + * + * @param name the name of the parameter value to retrieve + * + * @return this returns the named parameter value as an integer + */ + public int getInteger(Object name) { + String value = get(name); + + if(value != null) { + return Integer.parseInt(value); + } + return 0; + } + + /** + * This extracts a float parameter for the named value. If the + * named parameter does not exist this will return a zero value. + * If however the parameter exists but is not in the format of a + * floating point number then this will throw an exception. + * + * @param name the name of the parameter value to retrieve + * + * @return this returns the named parameter value as a float + */ + public float getFloat(Object name) { + String value = get(name); + + if(value != null) { + return Float.parseFloat(value); + } + return 0.0f; + } + + /** + * This extracts a boolean parameter for the named value. If the + * named parameter does not exist this will return false otherwise + * the value is evaluated. If it is either true or + * false then those boolean values are returned. + * + * @param name the name of the parameter value to retrieve + * + * @return this returns the named parameter value as an float + */ + public boolean getBoolean(Object name) { + Boolean flag = Boolean.FALSE; + String value = get(name); + + if(value != null) { + flag = Boolean.valueOf(value); + } + return flag.booleanValue(); + } + + + /** + * This initializes the parser so that it can be used several + * times. This clears any previous parameters extracted. This + * ensures that when the next parse(String) is + * invoked the status of the Query is empty. + */ + protected void init(){ + all.clear(); + map.clear(); + name.len = 0; + value.len = 0; + off = 0; + } + + /** + * This performs the actual parsing of the parameter text. The + * parameters parsed from this are taken as "name=value" pairs. + * Multiple pairs within the text are separated by an "&". + * This will parse and insert all parameters into a hashtable. + */ + protected void parse() { + param(); + while(skip("&")){ + param(); + } + } + + /** + * This method adds the name and value to a map so that the next + * name and value can be collected. The name and value are added + * to the map as string objects. Once added to the map the + * Token objects are set to have zero length so they + * can be reused to collect further values. This will add the + * values to the map as an array of type string. This is done so + * that if there are multiple values that they can be stored. + */ + private void insert(){ + if(name.len > 0){ + insert(name,value); + } + name.len = 0; + value.len = 0; + } + + /** + * This will add the given name and value to the parameters map. + * If any previous value of the given name has been inserted + * into the map then this will overwrite that value. This is + * used to ensure that the string value is inserted to the map. + * + * @param name this is the name of the value to be inserted + * @param value this is the value of a that is to be inserted + */ + private void insert(Token name, Token value){ + put(name.toString(), value.toString()); + } + + /** + * This is an expression that is defined by RFC 2396 it is used + * in the definition of a segment expression. This is basically + * a list of chars with escaped sequences. + *

+ * This method has to ensure that no escaped chars go unchecked. + * This ensures that the read offset does not go out of bounds + * and consequently throw an out of bounds exception. + */ + private void param() { + name(); + if(skip("=")){ /* in case of error*/ + value(); + } + insert(); + } + + /** + * This extracts the name of the parameter from the character + * buffer. The name of a parameter is defined as a set of + * chars including escape sequences. This will extract the + * parameter name and buffer the chars. The name ends when a + * equals character, "=", is encountered. + */ + private void name(){ + int mark = off; + int pos = off; + + while(off < count){ + if(buf[off]=='%'){ /* escaped */ + escape(); + }else if(buf[off]=='=') { + break; + }else if(buf[off]=='+'){ + buf[off] = ' '; + } + buf[pos++] = buf[off++]; + } + name.len = pos - mark; + name.off = mark; + } + + /** + * This extracts a parameter value from a path segment. The + * parameter value consists of a sequence of chars and some + * escape sequences. The parameter value is buffered so that + * the name and values can be paired. The end of the value + * is determined as the end of the buffer or an ampersand. + */ + private void value(){ + int mark = off; + int pos = off; + + while(off < count){ + if(buf[off]=='%'){ /* escaped */ + escape(); + }else if(buf[off]=='+'){ + buf[off] = ' '; + }else if(buf[off]=='&'){ + break; + } + buf[pos++] = buf[off++]; + } + value.len = pos - mark; + value.off = mark; + } + + /** + * This converts an encountered escaped sequence, that is all + * embedded hexidecimal characters into a native UCS character + * value. This does not take any characters from the stream it + * just prepares the buffer with the correct byte. The escaped + * sequence within the URI will be interpreded as UTF-8. + *

+ * This will leave the next character to read from the buffer + * as the character encoded from the URI. If there is a fully + * valid escaped sequence, that is "%" HEX HEX. + * This decodes the escaped sequence using UTF-8 encoding, all + * encoded sequences should be in UCS-2 to fit in a Java char. + */ + private void escape() { + int peek = peek(off); + + if(!unicode(peek)) { + binary(peek); + } + } + + /** + * This method determines, using a peek character, whether the + * sequence of escaped characters within the URI is binary data. + * If the data within the escaped sequence is binary then this + * will ensure that the next character read from the URI is the + * binary octet. This is used strictly for backward compatible + * parsing of URI strings, binary data should never appear. + * + * @param peek this is the first escaped character from the URI + * + * @return currently this implementation always returns true + */ + private boolean binary(int peek) { + if(off + 2 < count) { + off += 2; + buf[off] =bits(peek); + } + return true; + } + + /** + * This method determines, using a peek character, whether the + * sequence of escaped characters within the URI is in UTF-8. If + * a UTF-8 character can be successfully decoded from the URI it + * will be the next character read from the buffer. This can + * check for both UCS-2 and UCS-4 characters. However, because + * the Java char can only hold UCS-2, the UCS-4 + * characters will have only the low order octets stored. + *

+ * The WWW Consortium provides a reference implementation of a + * UTF-8 decoding for Java, in this the low order octets in the + * UCS-4 sequence are used for the character. So, in the + * absence of a defined behaviour, the W3C behaviour is assumed. + * + * @param peek this is the first escaped character from the URI + * + * @return this returns true if a UTF-8 character is decoded + */ + private boolean unicode(int peek) { + if((peek & 0x80) == 0x00){ + return unicode(peek, 0); + } + if((peek & 0xe0) == 0xc0){ + return unicode(peek & 0x1f, 1); + } + if((peek & 0xf0) == 0xe0){ + return unicode(peek & 0x0f, 2); + } + if((peek & 0xf8) == 0xf0){ + return unicode(peek & 0x07, 3); + } + if((peek & 0xfc) == 0xf8){ + return unicode(peek & 0x03, 4); + } + if((peek & 0xfe) == 0xfc){ + return unicode(peek & 0x01, 5); + } + return false; + } + + /** + * This method will decode the specified amount of escaped + * characters from the URI and convert them into a single Java + * UCS-2 character. If there are not enough characters within + * the URI then this will return false and leave the URI alone. + *

+ * The number of characters left is determined from the first + * UTF-8 octet, as specified in RFC 2279, and because this is + * a URI there must that number of "%" HEX HEX + * sequences left. If successful the next character read is + * the UTF-8 sequence decoded into a native UCS-2 character. + * + * @param peek contains the bits read from the first UTF octet + * @param more this specifies the number of UTF octets left + * + * @return this returns true if a UTF-8 character is decoded + */ + private boolean unicode(int peek, int more) { + if(off + more * 3 >= count) { + return false; + } + return unicode(peek,more,off); + } + + /** + * This will decode the specified amount of trailing UTF-8 bits + * from the URI. The trailing bits are those following the first + * UTF-8 octet, which specifies the length, in octets, of the + * sequence. The trailing octets are of the form 10xxxxxx, for + * each of these octets only the last six bits are valid UCS + * bits. So a conversion is basically an accumulation of these. + *

+ * If at any point during the accumulation of the UTF-8 bits + * there is a parsing error, then parsing is aborted an false + * is returned, as a result the URI is left unchanged. + * + * @param peek bytes that have been accumulated fron the URI + * @param more this specifies the number of UTF octets left + * @param pos this specifies the position the parsing begins + * + * @return this returns true if a UTF-8 character is decoded + */ + private boolean unicode(int peek, int more, int pos) { + while(more-- > 0) { + if(buf[pos] == '%'){ + int next = pos + 3; + int hex = peek(next); + + if((hex & 0xc0) == 0x80){ + peek = (peek<<6)|(hex&0x3f); + pos = next; + continue; + } + } + return false; + } + if(pos + 2 < count) { + off = pos + 2; + buf[off]= bits(peek); + } + return true; + } + + /** + * Defines behaviour for UCS-2 versus UCS-4 conversion from four + * octets. The UTF-8 encoding scheme enables UCS-4 characters to + * be encoded and decodeded. However, Java supports the 16-bit + * UCS-2 character set, and so the 32-bit UCS-4 character set is + * not compatable. This basically decides what to do with UCS-4. + * + * @param data up to four octets to be converted to UCS-2 format + * + * @return this returns a native UCS-2 character from the int + */ + private char bits(int data) { + return (char)data; + } + + /** + * This will return the escape expression specified from the URI + * as an integer value of the hexadecimal sequence. This does + * not make any changes to the buffer it simply checks to see if + * the characters at the position specified are an escaped set + * characters of the form "%" HEX HEX, if so, then + * it will convert that hexadecimal string in to an integer + * value, or -1 if the expression is not hexadecimal. + * + * @param pos this is the position the expression starts from + * + * @return the integer value of the hexadecimal expression + */ + private int peek(int pos) { + if(buf[pos] == '%'){ + if(count <= pos + 2) { + return -1; + } + char high = buf[pos + 1]; + char low = buf[pos + 2]; + + return convert(high, low); + } + return -1; + } + + /** + * This will convert the two hexidecimal characters to a real + * integer value, which is returned. This requires characters + * within the range of 'A' to 'F' and 'a' to 'f', and also + * the digits '0' to '9'. The characters encoded using the + * ISO-8859-1 encoding scheme, if the characters are not with + * in the range specified then this returns -1. + * + * @param high this is the high four bits within the integer + * @param low this is the low four bits within the integer + * + * @return this returns the indeger value of the conversion + */ + private int convert(char high, char low) { + int hex = 0x00; + + if(hex(high) && hex(low)){ + if('A' <= high && high <= 'F'){ + high -= 'A' - 'a'; + } + if(high >= 'a') { + hex ^= (high-'a')+10; + } else { + hex ^= high -'0'; + } + hex <<= 4; + + if('A' <= low && low <= 'F') { + low -= 'A' - 'a'; + } + if(low >= 'a') { + hex ^= (low-'a')+10; + } else { + hex ^= low-'0'; + } + return hex; + } + return -1; + } + + /** + * This is used to determine whether a char is a hexadecimal + * char or not. A hexadecimal character is considered + * to be a character within the range of 0 - 9 and + * between a - f and A - F. This will + * return true if the character is in this range. + * + * @param ch this is the character which is to be determined here + * + * @return true if the character given has a hexadecimal value + */ + private boolean hex(char ch) { + if(ch >= '0' && ch <= '9') { + return true; + } else if(ch >='a' && ch <= 'f') { + return true; + } else if(ch >= 'A' && ch <= 'F') { + return true; + } + return false; + } + + /** + * This encode method will escape the text that + * is provided. This is used to that the parameter pairs can + * be encoded in such a way that it can be transferred over + * HTTP/1.1 using the ISO-8859-1 character set. + * + * @param text this is the text that is to be escaped + * + * @return the text with % HEX HEX UTF-8 escape sequences + */ + private String encode(String text) { + try { + return URLEncoder.encode(text, "UTF-8"); + }catch(Exception e){ + return text; + } + } + + /** + * This encode method will escape the name=value + * pair provided using the UTF-8 character set. This method + * will ensure that the parameters are encoded in such a way + * that they can be transferred via HTTP in ISO-8859-1. + * + * @param name this is the name of that is to be escaped + * @param value this is the value that is to be escaped + * + * @return the pair with % HEX HEX UTF-8 escape sequences + */ + private String encode(String name, String value) { + return encode(name) + "=" + encode(value); + } + + /** + * This toString method is used to compose an string + * in the application/x-www-form-urlencoded MIME type. + * This will encode the tokens specified in the Set. + * Each name=value pair acquired is converted into a UTF-8 escape + * sequence so that the parameters can be sent in the IS0-8859-1 + * format required via the HTTP/1.1 specification RFC 2616. + * + * @param set this is the set of parameters to be encoded + * + * @return returns a HTTP parameter encoding for the pairs + */ + public String toString(Set set) { + Object[] list = set.toArray(); + String text = ""; + + for(int i = 0; i < list.length; i++){ + String name = list[i].toString(); + String value = get(name); + + if(i > 0) { + text += "&"; + } + text += encode(name, value); + } + return text; + } + + /** + * This toString method is used to compose an string + * in the application/x-www-form-urlencoded MIME type. + * This will iterate over all tokens that have been added to this + * object, either during parsing, or during use of the instance. + * Each name=value pair acquired is converted into a UTF-8 escape + * sequence so that the parameters can be sent in the IS0-8859-1 + * format required via the HTTP/1.1 specification RFC 2616. + * + * @return returns a HTTP parameter encoding for the pairs + */ + public String toString() { + Set set = map.keySet(); + + if(map.size() > 0) { + return toString(set); + } + return ""; + } + + /** + * This is used to mark regions within the buffer that represent + * a valid token for either the name of a parameter or its value. + * This is used as an alternative to the ParseBuffer + * which requires memory to be allocated for storing the data + * read from the buffer. This requires only two integer values. + */ + private class Token { + + /** + * This represents the number of characters in the token. + */ + public int len; + + /** + * This represents the start offset within the buffer. + */ + public int off; + + /** + * In order to represent the Token as a value + * that can be used this converts it to a String. + * If the length of the token is less than or equal to zero + * this will return and empty string for the value. + * + * @return this returns a value representing the token + */ + public String toString() { + if(len <= 0) { + return ""; + } + return new String(buf,off,len); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/parse/ValueParser.java b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ValueParser.java new file mode 100644 index 0000000..99b16b9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/parse/ValueParser.java @@ -0,0 +1,108 @@ +/* + * ValueParser.java September 2003 + * + * Copyright (C) 2003, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.parse; + +import java.util.List; + +/** + * The ValueParser is used to extract a comma separated + * list of HTTP header values. This will extract values without + * any leading or trailing spaces, which enables the values to be + * used. Listing the values that appear in the header also requires + * that the values are ordered. This orders the values using the + * values that appear with any quality parameter associated with it. + * The quality value is a special parameter that often found in a + * comma separated value list to specify the client preference. + *

+ * 
+ *    image/gif, image/jpeg, text/html
+ *    image/gif;q=1.0, image/jpeg;q=0.8, image/png;  q=1.0,*;q=0.1
+ *    gzip;q=1.0, identity; q=0.5, *;q=0
+ *
+ * 
+ * The above lists taken from RFC 2616 provides an example of the + * common form comma separated values take. The first illustrates + * a simple comma delimited list, here the ordering of values is + * determined from left to right. The second and third list have + * quality values associated with them, these are used to specify + * a preference and thus order. + *

+ * Each value within a list has an implicit quality value of 1.0. + * If the value is explicitly set with a the "q" parameter, then + * the values can range from 1.0 to 0.001. This parser ensures + * that the order of values returned from the list + * method adheres to the optional quality parameters and ensures + * that the quality parameters a removed from the resulting text. + * + * @author Niall Gallagher + */ +public class ValueParser extends ListParser { + + /** + * Constructor for the ValueParser. This creates + * a parser with no initial parse data, if there are headers to + * be parsed then the parse(String) method or + * parse(List) method can be used. This will + * parse a delimited list according so RFC 2616 section 4.2. + */ + public ValueParser(){ + super(); + } + + /** + * Constructor for the ValueParser. This creates + * a parser with the text supplied. This will parse the comma + * separated list according to RFC 2616 section 2.1 and 4.2. + * The tokens can be extracted using the list + * method, which will also sort and trim the tokens. + * + * @param text this is the comma separated list to be parsed + */ + public ValueParser(String text) { + super(text); + } + + /** + * Constructor for the ValueParser. This creates + * a parser with the text supplied. This will parse the comma + * separated list according to RFC 2616 section 2.1 and 4.2. + * The tokens can be extracted using the list + * method, which will also sort and trim the tokens. + * + * @param list a list of comma separated lists to be parsed + */ + public ValueParser(List list) { + super(list); + } + + /** + * This creates a string object using an offset and a length. + * The string is created from the extracted token and the offset + * and length ensure that no leading or trailing whitespace are + * within the created string object. + * + * @param text this is the text buffer to acquire the value from + * @param start the offset within the buffer to take characters + * @param len this is the number of characters within the token + */ + @Override + protected String create(char[] text, int start, int len){ + return new String(text, start, len); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java new file mode 100644 index 0000000..bea3c63 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java @@ -0,0 +1,75 @@ +/* + * BinaryData.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The BinaryData object represents a binary payload for + * a WebScoket frame. This can be used to send any type of data. If + * however it is used to send text data then it is decoded as UTF-8. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public class BinaryData implements Data { + + /** + * This is used to convert the binary payload to text. + */ + private final DataConverter converter; + + /** + * This is the byte array that represents the binary payload. + */ + private final byte[] data; + + /** + * Constructor for the BinaryData object. It requires + * an array of binary data that will be send within a frame. + * + * @param data the byte array representing the frame payload + */ + public BinaryData(byte[] data) { + this.converter = new DataConverter(); + this.data = data; + } + + /** + * This returns the binary payload that is to be sent with a frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + public byte[] getBinary() { + return data; + } + + /** + * This returns the text payload that is to be sent with a frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + public String getText() { + return converter.convert(data); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java new file mode 100644 index 0000000..c64c605 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java @@ -0,0 +1,150 @@ +/* + * CloseCode.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The CloseCode enumerates the closure codes specified in + * RFC 6455. When closing an established connection an endpoint may + * indicate a reason for closure. The interpretation of this reason by + * an endpoint, and the action an endpoint should take given this reason, + * are left undefined by RFC 6455. The specification defines a set of + * status codes and specifies which ranges may be used by extensions, + * frameworks, and end applications. The status code and any associated + * textual message are optional components of a Close frame. + * + * @author niall.gallagher + */ +public enum CloseCode { + + /** + * Indicates the purpose for the connection has been fulfilled. + */ + NORMAL_CLOSURE(1000), + + /** + * Indicates that the server is going down or the client browsed away. + */ + GOING_AWAY(1001), + + /** + * Indicates the connection is terminating due to a protocol error. + */ + PROTOCOL_ERROR(1002), + + /** + * Indicates the connection received a data type it cannot accept. + */ + UNSUPPORTED_DATA(1003), + + /** + * According to RFC 6455 this has been reserved for future use. + */ + RESERVED(1004), + + /** + * Indicates that no status code was present and should not be used. + */ + NO_STATUS_CODE(1005), + + /** + * Indicates an abnormal closure and should not be used. + */ + ABNORMAL_CLOSURE(1006), + + /** + * Indicates that a payload was not consistent with the message type. + */ + INVALID_FRAME_DATA(1007), + + /** + * Indicates an endpoint received a message that violates its policy. + */ + POLICY_VIOLATION(1008), + + /** + * Indicates that a payload is too big to be processed. + */ + TOO_BIG(1009), + + /** + * Indicates that the server did not negotiate an extension properly. + */ + NO_EXTENSION(1010), + + /** + * Indicates an unexpected error within the server. + */ + INTERNAL_SERVER_ERROR(1011), + + /** + * Indicates a validation failure for TLS and should not be used. + */ + TLS_HANDSHAKE_FAILURE(1015); + + /** + * This is the actual integer value representing the code. + */ + public final int code; + + /** + * This is the high order byte for the closure code. + */ + public final int high; + + /** + * This is the low order byte for the closure code. + */ + public final int low; + + /** + * Constructor for the CloseCode object. This is used + * to create a closure code using one of the pre-defined values + * within RFC 6455. + * + * @param code this is the code that is to be used + */ + private CloseCode(int code) { + this.high = code & 0x0f; + this.low = code & 0xf0; + this.code = code; + } + + /** + * This is the data that represents the closure code. The array + * contains the high order byte and the low order byte as taken + * from the pre-defined closure code. + * + * @return a byte array representing the closure code + */ + public byte[] getData() { + return new byte[] { (byte)high, (byte)low }; + } + + + public static CloseCode resolveCode(int high, int low) { + for(CloseCode code : values()) { + if(code.high == high) { + if(code.low == low) { + return code; + } + } + } + return NO_STATUS_CODE; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java new file mode 100644 index 0000000..bb79830 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java @@ -0,0 +1,51 @@ +/* + * Data.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The Data interface represents a payload for a WebScoket + * frame. It can hold either binary data or text data. For performance + * binary frames are a better choice as all text frames need to be + * encoded as UTF-8 from the native UCS2 format. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public interface Data { + + /** + * This returns the binary payload that is to be sent with a frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + byte[] getBinary(); + + /** + * This returns the text payload that is to be sent with a frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + String getText(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java new file mode 100644 index 0000000..5713fd6 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java @@ -0,0 +1,111 @@ +/* + * DataConverter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The DataConverter object is used to convert binary data + * to text data and vice versa. According to RFC 6455 a particular text + * frame might include a partial UTF-8 sequence; however, the whole + * message MUST contain valid UTF-8. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public class DataConverter { + + /** + * This is the character encoding used to convert the text data. + */ + private final String charset; + + /** + * Constructor for the DataConverter object. By default + * this uses UTF-8 character encoding to convert text data as this + * is what is required for RFC 6455 section 5.6. + */ + public DataConverter() { + this("UTF-8"); + } + + /** + * Constructor for the DataConverter object. This can be + * used to specific a character encoding other than UTF-8. However it + * is not recommended as RFC 6455 section 5.6 suggests the frame must + * contain valid UTF-8 data. + * + * @param charset the character encoding to be used + */ + public DataConverter(String charset) { + this.charset = charset; + } + + /** + * This method is used to convert text using the character encoding + * specified when constructing the converter. Typically this will use + * UTF-8 as required by RFC 6455. + * + * @param text this is the string to convert to a byte array + * + * @return a byte array decoded using the specified encoding + */ + public byte[] convert(String text) { + try { + return text.getBytes(charset); + } catch(Exception e) { + throw new IllegalStateException("Could not encode text as " + charset, e); + } + } + + /** + * This method is used to convert data using the character encoding + * specified when constructing the converter. Typically this will use + * UTF-8 as required by RFC 6455. + * + * @param text this is the byte array to convert to a string + * + * @return a string encoded using the specified encoding + */ + public String convert(byte[] binary) { + try { + return new String(binary, charset); + } catch(Exception e) { + throw new IllegalStateException("Could not decode data as " + charset, e); + } + } + + /** + * This method is used to convert data using the character encoding + * specified when constructing the converter. Typically this will use + * UTF-8 as required by RFC 6455. + * + * @param text this is the byte array to convert to a string + * @param offset the is the offset to read the bytes from + * @param size this is the number of bytes to be used + * + * @return a string encoded using the specified encoding + */ + public String convert(byte[] binary, int offset, int size) { + try { + return new String(binary, offset, size, charset); + } catch(Exception e) { + throw new IllegalStateException("Could not decode data as " + charset, e); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java new file mode 100644 index 0000000..b51cd2b --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java @@ -0,0 +1,212 @@ +/* + * DataFrame.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The DataFrame object represents a frame as defined in + * RFC 6455. A frame is a very lightweight envelope used to send + * control information and either text or binary user data. Typically + * a frame will represent a single message however, it is possible + * to fragment a single frame up in to several frames. A fragmented + * frame has a specific FrameType indicating that it + * is a continuation frame. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.Data + */ +public class DataFrame implements Frame { + + /** + * This is the type used to determine the intent of the frame. + */ + private final FrameType type; + + /** + * This contains the payload to be sent with the frame. + */ + private final Data data; + + /** + * This determines if the frame is the last of a sequence. + */ + private final boolean last; + + /** + * Constructor for the DataFrame object. This is used + * to create a frame using the specified data and frame type. A + * zero payload is created using this constructor and is suitable + * only for specific control frames such as connection termination. + * + * @param type this is the frame type used for this instance + */ + public DataFrame(FrameType type) { + this(type, new byte[0]); + } + + /** + * Constructor for the DataFrame object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + */ + public DataFrame(FrameType type, byte[] data) { + this(type, data, true); + } + + /** + * Constructor for the DataFrame object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + * @param last true if this is not a fragment in a sequence + */ + public DataFrame(FrameType type, byte[] data, boolean last) { + this(type, new BinaryData(data), last); + } + + /** + * Constructor for the DataFrame object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + */ + public DataFrame(FrameType type, String text) { + this(type, text, true); + } + + /** + * Constructor for the DataFrame object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + * @param last true if this is not a fragment in a sequence + */ + public DataFrame(FrameType type, String text, boolean last) { + this(type, new TextData(text), last); + } + + /** + * Constructor for the DataFrame object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + */ + public DataFrame(FrameType type, Data data) { + this(type, data, true); + } + + /** + * Constructor for the DataFrame object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + * @param last true if this is not a fragment in a sequence + */ + public DataFrame(FrameType type, Data data, boolean last) { + this.data = data; + this.type = type; + this.last = last; + } + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + public boolean isFinal() { + return last; + } + + /** + * This returns the binary payload that is to be sent with the frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + public byte[] getBinary() { + return data.getBinary(); + } + + /** + * This returns the text payload that is to be sent with the frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + public String getText(){ + return data.getText(); + } + + /** + * This method is used to convert from one frame type to another. + * Converting a frame type is useful in scenarios such as when a + * ping needs to respond to a pong or when it is more convenient + * to send a text frame as binary. + * + * @param type this is the frame type to convert to + * + * @return a new frame using the specified frame type + */ + public Frame getFrame(FrameType type) { + return new DataFrame(type, data, last); + } + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + public FrameType getType(){ + return type; + } + + /** + * This returns the text payload that is to be sent with the frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + @Override + public String toString() { + return getText(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java new file mode 100644 index 0000000..7f5ad0f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java @@ -0,0 +1,85 @@ +/* + * Frame.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The Frame interface represents a frame as defined in + * RFC 6455. A frame is a very lightweight envelope used to send + * control information and either text or binary user data. Typically + * a frame will represent a single message however, it is possible + * to fragment a single frame up in to several frames. A fragmented + * frame has a specific FrameType indicating that it + * is a continuation frame. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public interface Frame { + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + boolean isFinal(); + + /** + * This returns the binary payload that is to be sent with the frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + byte[] getBinary(); + + /** + * This returns the text payload that is to be sent with the frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + String getText(); + + /** + * This method is used to convert from one frame type to another. + * Converting a frame type is useful in scenarios such as when a + * ping needs to respond to a pong or when it is more convenient + * to send a text frame as binary. + * + * @param type this is the frame type to convert to + * + * @return a new frame using the specified frame type + */ + Frame getFrame(FrameType type); + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + FrameType getType(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java new file mode 100644 index 0000000..bcacc43 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java @@ -0,0 +1,117 @@ +/* + * FrameChannel.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +import java.io.IOException; + +/** + * The FrameChannel represents a full duplex communication + * channel as defined by RFC 6455. Any instance of this will provide + * a means to perform asynchronous writes and reads to a remote client + * using a lightweight framing protocol. A frame is a finite length + * sequence of bytes that can hold either text or binary data. Also, + * control frames are used to perform heartbeat monitoring and closure. + *

+ * For convenience frames can be consumed from the socket via a + * callback to a registered listener. This avoids having to poll each + * socket for data and provides a asynchronous event driven model of + * communication, which greatly reduces overhead and complication. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameListener + * @see org.simpleframework.http.socket.Frame + */ +public interface FrameChannel { + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param data this is the data that is to be sent + */ + void send(byte[] data) throws IOException; + + /** + * This is used to send text to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param text this is the text that is to be sent + */ + void send(String text) throws IOException; + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param frame this is the frame that is to be sent + */ + void send(Frame frame) throws IOException; + + /** + * This is used to register a FrameListener to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + void register(FrameListener listener) throws IOException; + + /** + * This is used to remove a FrameListener from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + void remove(FrameListener listener) throws IOException; + + /** + * This is used to close the connection with a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + * + * @param reason the reason for closing the connection + */ + void close(Reason reason) throws IOException; + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + */ + void close() throws IOException; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java new file mode 100644 index 0000000..6892e9c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java @@ -0,0 +1,64 @@ +/* + * FrameListener.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The FrameListener is used to listen for incoming frames + * on a WebSocket. Any number of listeners can listen on + * a single web socket and it will receive all incoming events. For + * consistency this interface is modelled on the WebSocket API as + * defined by W3C Candidate Recommendation as of 20 September 2012. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +public interface FrameListener { + + /** + * This is called when a new frame arrives on the WebSocket. It + * will receive control frames as well as binary and text user + * frames. Control frames should not be acted on or responded + * to as they are provided for informational purposes only. + * + * @param session this is the associated session + * @param frame this is the frame that has been received + */ + void onFrame(Session session, Frame frame); + + /** + * This is called when an error occurs on the WebSocket. After + * an error the connection it is closed with an opcode indicating + * an internal server error. + * + * @param session this is the associated session + * @param frame this is the exception that has been thrown + */ + void onError(Session session, Exception cause); + + /** + * This is called when the connection is closed from the other + * side. Typically a frame with an opcode of close is sent + * before the close callback is issued. + * + * @param session this is the associated session + * @param reason this is the reason the connection was closed + */ + void onClose(Session session, Reason reason); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java new file mode 100644 index 0000000..8237701 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java @@ -0,0 +1,142 @@ +/* + * FrameType.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The FrameType represents the set of opcodes defined + * in RFC 6455. The base framing protocol uses a opcode to define the + * interpretation of the payload data for the frame. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.Frame + */ +public enum FrameType { + + /** + * A continuation frame identifies a fragment from a larger message. + */ + CONTINUATION(0x00), + + /** + * A text frame identifies a message that contains UTF-8 text data. + */ + TEXT(0x01), + + /** + * A binary frame identifies a message that contains binary data. + */ + BINARY(0x02), + + /** + * A close frame identifies a frame used to terminate a connection. + */ + CLOSE(0x08), + + /** + * A ping frame is a heartbeat used to determine connection health. + */ + PING(0x09), + + /** + * A pong frame is sent is sent in response to a ping frame. + */ + PONG(0x0a); + + /** + * This is the integer value for the opcode. + */ + public final int code; + + /** + * Constructor for the Frame type enumeration. This is + * given the opcode that is used to identify a specific frame type. + * + * @param code this is the opcode representing the frame type + */ + private FrameType(int code) { + this.code = code; + } + + /** + * This is used to determine if a frame is a text frame. It can be + * useful to know if a frame is a user based frame as it reduces + * the need to convert from or to certain character sets. + * + * @return this returns true if the frame represents a text frame + */ + public boolean isText() { + return this == TEXT; + } + + /** + * This is used to determine if a frame is a close frame. A close + * frame contains an optional payload, which if present contains + * an error code in network byte order in the first two bytes, + * followed by an optional UTF-8 text reason of the closure. + * + * @return this returns true if the frame represents a close frame + */ + public boolean isClose() { + return this == CLOSE; + } + + /** + * This is used to determine if a frame is a pong frame. A pong + * frame is sent in response to a ping and is used to determine if + * a WebSocket connection is still active and healthy. + * + * @return this returns true if the frame represents a pong frame + */ + public boolean isPong() { + return this == PONG; + } + + /** + * This is used to determine if a frame is a ping frame. A ping + * frame is sent to check if a WebSocket connection is still healthy. + * A connection is determined healthy if it responds with a pong + * frame is a reasonable length of time. + * + * @return this returns true if the frame represents a ping frame + */ + public boolean isPing() { + return this == PING; + } + + /** + * This is used to acquire the frame type given an opcode. If no + * frame type can be determined from the opcode provided then this + * will return a null value. + * + * @param octet this is the octet representing the opcode + * + * @return this returns the frame type from the opcode + */ + public static FrameType resolveType(int octet) { + int value = octet & 0xff; + + for(FrameType code : values()) { + if(code.code == value) { + return code; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java new file mode 100644 index 0000000..c7438e5 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java @@ -0,0 +1,97 @@ +/* + * Reason.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The Reason object is used to hold a textual reason + * for connection closure and an RFC 6455 defined code. When a + * connection is to be closed a control frame with an opcode of + * close is sent with the text reason, if one is provided. + * + * @author Niall Gallagher + */ +public class Reason { + + /** + * This is the close code to be sent with a control frame. + */ + private final CloseCode code; + + /** + * This is the textual description of the close reason. + */ + private final String text; + + /** + * Constructor for the Reason object. This is used + * to create a reason and a textual description of that reason + * to be delivered as a control frame. + * + * @param code this is the code to be sent with the frame + */ + public Reason(CloseCode code) { + this(code, null); + } + + /** + * Constructor for the Reason object. This is used + * to create a reason and a textual description of that reason + * to be delivered as a control frame. + * + * @param code this is the code to be sent with the frame + * @param text this is textual description of the close reason + */ + public Reason(CloseCode code, String text) { + this.code = code; + this.text = text; + } + + /** + * This is used to get the RFC 6455 code describing the type + * of close event. It is the code that should be used by + * applications to determine why the connection was terminated. + * + * @return returns the close code for the connection + */ + public CloseCode getCode() { + return code; + } + + /** + * This is used to get the textual description for the closure. + * In many scenarios there will be no textual reason as it is + * an optional attribute. + * + * @return this returns the description for the closure + */ + public String getText() { + return text; + } + + /** + * This is used to provide a textual representation of the reason. + * For consistency this will only return the enumerated value for + * the close code, or if none exists a "null" text string. + * + * @return this returns a string representation of the reason + */ + public String toString() { + return String.valueOf(code); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java new file mode 100644 index 0000000..7c9a7db --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java @@ -0,0 +1,91 @@ +/* + * Session.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +import java.util.Map; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The Session object represents a simple WebSocket session + * that contains the connection handshake details and the actual socket. + * In order to determine how the session should be interacted with the + * protocol is conveniently exposed, however all attributes of the + * original HTTP request are available. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +public interface Session { + + /** + * This can be used to retrieve the response attributes. These can + * be used to keep state with the response when it is passed to + * other systems for processing. Attributes act as a convenient + * model for storing objects associated with the response. This + * also inherits attributes associated with the client connection. + * + * @return the attributes of that have been set on the request + */ + Map getAttributes(); + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the attribute Map + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param key this is the key of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + Object getAttribute(Object key); + + /** + * Provides a FrameChannel that can be used to communicate + * with the connected client. Communication is full duplex and also + * asynchronous through the use of a FrameListener that + * can be registered with the channel. + * + * @return a web socket for full duplex communication + */ + FrameChannel getChannel(); + + /** + * Provides the Request used to initiate the session. + * This is useful in establishing the identity of the user, acquiring + * an security information and also for determining the request path + * that was used, which be used to establish context. + * + * @return the request used to initiate the session + */ + Request getRequest(); + + /** + * Provides the Response used to establish the session + * with the remote client. This is useful in establishing the protocol + * used to create the session and also for determining various other + * useful contextual information. + * + * @return the response used to establish the session + */ + Response getResponse(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java new file mode 100644 index 0000000..24ee97d --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java @@ -0,0 +1,75 @@ +/* + * TextData.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The TextData object represents a text payload for + * a WebScoket frame. This can be used to send any type of data. If + * however it is used to send binary data then it is encoded as UTF-8. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public class TextData implements Data { + + /** + * This is used to convert the text payload to a byte array. + */ + private final DataConverter converter; + + /** + * This is the text string representing a frame payload. + */ + private final String data; + + /** + * Constructor for the TextData object. It requires + * an text string that will be sent as UTF-8 within a frame. + * + * @param data the text string representing the frame payload + */ + public TextData(String data) { + this.converter = new DataConverter(); + this.data = data; + } + + /** + * This returns the binary payload that is to be sent with a frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + public byte[] getBinary() { + return converter.convert(data); + } + + /** + * This returns the text payload that is to be sent with a frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + public String getText() { + return data; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java new file mode 100644 index 0000000..2fe2521 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java @@ -0,0 +1,127 @@ +/* + * AcceptToken.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_KEY; + +import java.io.IOException; +import java.security.MessageDigest; + +import org.simpleframework.common.encode.Base64Encoder; +import org.simpleframework.http.Request; + +/** + * The AcceptToken is used to create a unique token based + * on a random key sent by the client. This is used to prove that the + * handshake was received, the server has to take two pieces of + * information and combine them to form a response. The first piece + * of information comes from the Sec-WebSocket-Key header + * field in the client handshake, the second is the globally unique + * identifier 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. Both + * are concatenated and an SHA-1 has is generated and used in the + * session initiating response. + * + * @author Niall Gallagher + */ +class AcceptToken { + + /** + * This is the globally unique identifier used in the handshake. + */ + private static final byte[] MAGIC = { + '2', '5', '8', 'E', 'A', 'F', 'A', '5', '-', + 'E', '9', '1', '4', '-', '4', '7', 'D', 'A', + '-', '9', '5', 'C', 'A', '-', 'C', '5', 'A', + 'B', '0', 'D', 'C', '8', '5', 'B', '1', '1' }; + + /** + * This is used to generate the SHA-1 has from the user key. + */ + private final MessageDigest digest; + + /** + * This is the original request used to initiate the session. + */ + private final Request request; + + /** + * This is the character encoding to decode the key with. + */ + private final String charset; + + /** + * Constructor for the AcceptToken object. This is + * to create an object that can generate a token from the client + * key available from the Sec-WebSocket-Key header. + * + * @param request this is the session initiating request + */ + public AcceptToken(Request request) throws Exception { + this(request, "SHA-1"); + } + + /** + * Constructor for the AcceptToken object. This is + * to create an object that can generate a token from the client + * key available from the Sec-WebSocket-Key header. + * + * @param request this is the session initiating request + * @param algorithm the algorithm used to create the token + */ + public AcceptToken(Request request, String algorithm) throws Exception { + this(request, algorithm, "UTF-8"); + } + + /** + * Constructor for the AcceptToken object. This is + * to create an object that can generate a token from the client + * key available from the Sec-WebSocket-Key header. + * + * @param request this is the session initiating request + * @param algorithm the algorithm used to create the token + * @param charset the encoding used to decode the client key + */ + public AcceptToken(Request request, String algorithm, String charset) throws Exception { + this.digest = MessageDigest.getInstance(algorithm); + this.request = request; + this.charset = charset; + } + + /** + * This is used to create the required accept token for the session + * initiating response. The resulting token is a SHA-1 digest of + * the Sec-WebSocket-Key a globally unique identifier + * defined in RFC 6455 all encoded in base64. + * + * @return the accept token for the session initiating response + */ + public String create() throws IOException { + String value = request.getValue(SEC_WEBSOCKET_KEY); + byte[] data = value.getBytes(charset); + + if (data.length > 0) { + digest.update(data); + digest.update(MAGIC); + } + byte[] digested = digest.digest(); + char[] text = Base64Encoder.encode(digested); + + return new String(text); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java new file mode 100644 index 0000000..0c09063 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java @@ -0,0 +1,107 @@ +/* + * DirectRouter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_PROTOCOL; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The DirectRouter object is used to create a router + * that uses a single service. Typically this is used by simpler + * servers that wish to expose a single sub-protocol to clients. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public class DirectRouter implements Router { + + /** + * The service used by this router instance. + */ + private final Service service; + + /** + * The protocol used or null if none was specified. + */ + private final String protocol; + + /** + * Constructor for the DirectRouter object. This + * is used to create an object that will select a single service. + * Creating an instance with this constructor means that the + * protocol header will not be set. + * + * @param service this is the service used by this instance + * @param protocol the protocol used by this router or null + */ + public DirectRouter(Service service) { + this(service, null); + } + + /** + * Constructor for the DirectRouter object. This + * is used to create an object that will select a single service. + * If the protocol specified is null then the response to the + * session initiation will contain null for the protocol header. + * + * @param service this is the service used by this instance + * @param protocol the protocol used by this router or null + */ + public DirectRouter(Service service, String protocol) { + this.protocol = protocol; + this.service = service; + } + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this will return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + public Service route(Request request, Response response) { + String token = request.getValue(UPGRADE); + + if(token != null) { + if(token.equalsIgnoreCase(WEBSOCKET)) { + String version = request.getValue(SEC_WEBSOCKET_VERSION); + + if(version != null) { + response.setValue(SEC_WEBSOCKET_VERSION, version); + } + if(protocol != null) { + response.setValue(SEC_WEBSOCKET_PROTOCOL, protocol); + } + return service; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java new file mode 100644 index 0000000..6ab224a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java @@ -0,0 +1,118 @@ +/* + * FrameBuilder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.util.Arrays; + +import org.simpleframework.http.socket.DataConverter; +import org.simpleframework.http.socket.DataFrame; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameType; + +/** + * The FrameBuilder object is used to create an object + * that interprets a frame header to produce frame objects. For + * efficiency this converts binary data to the native frame data + * type, which avoids memory churn. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +class FrameBuilder { + + /** + * This converts binary data to a UTF-8 string for text frames. + */ + private final DataConverter converter; + + /** + * This is used to determine the type of frames to create. + */ + private final FrameHeader header; + + /** + * Constructor for the FrameBuilder object. This acts + * as a factory for frame objects by using the provided header to + * determine the frame type to be created. + * + * @param header the header used to determine the frame type + */ + public FrameBuilder(FrameHeader header) { + this.converter = new DataConverter(); + this.header = header; + } + + /** + * This is used to create a frame object to represent the data that + * has been consumed. The frame created will contain either a copy of + * the provided byte buffer or a text string encoded in UTF-8. To + * avoid memory churn this method should be used sparingly. + * + * @return this returns a frame created from the consumed bytes + */ + public Frame create(byte[] data, int count) { + FrameType type = header.getType(); + + if(type.isText()) { + return createText(data, count); + } + return createBinary(data, count); + } + + /** + * This is used to create a frame object from the provided data. + * The resulting frame will contain a UTF-8 encoding of the data + * to ensure that data conversion needs to be performed only once. + * + * @param data this is the data to convert to a new frame + * @param count this is the number of bytes in the frame + * + * @return a new frame containing the text + */ + private Frame createText(byte[] data, int count) { + FrameType type = header.getType(); + String text = converter.convert(data, 0, count); + + if(header.isFinal()) { + return new DataFrame(type, text, true); + } + return new DataFrame(type, text, false); + } + + /** + * This is used to create a frame object from the provided data. + * The resulting frame will contain a copy of the data to ensure + * that the frame is immutable. + * + * @param data this is the data to convert to a new frame + * @param count this is the number of bytes in the frame + * + * @return a new frame containing a copy of the provided data + */ + private Frame createBinary(byte[] data, int count) { + FrameType type = header.getType(); + byte[] copy = Arrays.copyOf(data, count); + + if(header.isFinal()) { + return new DataFrame(type, copy, true); + } + return new DataFrame(type, copy, false); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java new file mode 100644 index 0000000..8987620 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java @@ -0,0 +1,179 @@ +/* + * FrameCollector.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; + +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; + +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.reactor.Operation; +import org.simpleframework.transport.reactor.Reactor; +import org.simpleframework.transport.trace.Trace; + +/** + * The FrameCollector operation is used to collect frames + * from a channel and dispatch them to a FrameListener. + * To ensure that stale connections do not linger any connection that + * does not send a control ping or pong frame within two minutes will + * be terminated and the close control frame will be sent. + * + * @author Niall Gallagher + */ +class FrameCollector implements Operation { + + /** + * This decodes the frame bytes from the channel and processes it. + */ + private final FrameProcessor processor; + + /** + * This is the cursor used to maintain a stream seek position. + */ + private final ByteCursor cursor; + + /** + * This is the underlying channel for this frame collector. + */ + private final Channel channel; + + /** + * This is the reactor used to schedule this operation for reads. + */ + private final Reactor reactor; + + /** + * This is the tracer that is used to trace the frame collection. + */ + private final Trace trace; + + /** + * Constructor for the FrameCollector object. This is + * used to create a collector that will process and dispatch web + * socket frames as defined by RFC 6455. + * + * @param encoder this is the encoder used to send messages + * @param session this is the web socket session + * @param channel this is the underlying TCP communication channel + * @param reactor this is the reactor used for read notifications + */ + public FrameCollector(FrameEncoder encoder, Session session, Request request, Reactor reactor) { + this.processor = new FrameProcessor(encoder, session, request); + this.channel = request.getChannel(); + this.cursor = channel.getCursor(); + this.trace = channel.getTrace(); + this.reactor = reactor; + } + + /** + * This is used to acquire the trace object that is associated + * with the operation. A trace object is used to collection details + * on what operations are being performed. For instance it may + * contain information relating to I/O events or errors. + * + * @return this returns the trace associated with this operation + */ + public Trace getTrace() { + return trace; + } + + /** + * This is the channel associated with this collector. This is used + * to register for notification of read events. If at any time the + * remote endpoint is closed then this will cause the collector + * to perform a final execution before closing. + * + * @return this returns the selectable TCP channel + */ + public SelectableChannel getChannel() { + return channel.getSocket(); + } + + /** + * This is used to register a FrameListener to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public void register(FrameListener listener) { + processor.register(listener); + } + + /** + * This is used to remove a FrameListener from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public void remove(FrameListener listener) { + processor.remove(listener); + } + + /** + * This is used to execute the collection operation. Collection is + * done by reading the frame header from the incoming data, once + * consumed the remainder of the frame is collected until such + * time as it has been fully consumed. When consumed it will be + * dispatched to the registered frame listeners. + */ + public void run() { + try { + processor.process(); + + if(cursor.isOpen()) { + reactor.process(this, SelectionKey.OP_READ); + } else { + processor.close(); + } + } catch(Exception cause) { + trace.trace(ERROR, cause); + + try { + processor.failure(cause); + } catch(Exception fatal) { + trace.trace(ERROR, fatal); + } finally { + channel.close(); + } + } + } + + /** + * This is called when a read operation has timed out. To ensure + * that stale channels do not remain registered they are cleared + * out with this method and a close frame is sent if possible. + */ + public void cancel() { + try{ + processor.close(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java new file mode 100644 index 0000000..b904130 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java @@ -0,0 +1,214 @@ +/* + * FrameConnection.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.NORMAL_CLOSURE; +import static org.simpleframework.http.socket.service.ServiceEvent.OPEN_SOCKET; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; +import org.simpleframework.http.socket.FrameChannel; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.reactor.Reactor; +import org.simpleframework.transport.trace.Trace; + +/** + * The FrameConnection represents a connection that can + * send and receivd WebSocket frames. Any instance of this will provide + * a means to perform asynchronous writes and reads to a remote client + * using a lightweight framing protocol. A frame is a finite length + * sequence of bytes that can hold either text or binary data. Also, + * control frames are used to perform heartbeat monitoring and closure. + *

+ * For convenience frames can be consumed from the socket via a + * callback to a registered listener. This avoids having to poll each + * socket for data and provides a asynchronous event driven model of + * communication, which greatly reduces overhead and complication. + * + * @author Niall Gallagher + */ +class FrameConnection implements FrameChannel { + + /** + * The collector is used to collect frames from the TCP channel. + */ + private final FrameCollector operation; + + /** + * This encoder is used to encode data as RFC 6455 frames. + */ + private final FrameEncoder encoder; + + /** + * This is the sender used to send frames over the channel. + */ + private final ByteWriter writer; + + /** + * This is the session object that has a synchronized channel. + */ + private final Session session; + + /** + * This is the underlying TCP channel that frames are sent over. + */ + private final Channel channel; + + /** + * The reason that is sent if at any time the channel is closed. + */ + private final Reason reason; + + /** + * This is used to trace all events that occur on the channel. + */ + private final Trace trace; + + /** + * Constructor for the FrameConnection object. This is used + * to create a channel that can read and write frames over a TCP + * channel. For asynchronous read and dispatch operations this will + * produce an operation to collect and process RFC 6455 frames. + * + * @param request this is the initiating request for the WebSocket + * @param response this is the initiating response for the WebSocket + * @param reactor this is the reactor used to process frames + */ + public FrameConnection(Request request, Response response, Reactor reactor) { + this.encoder = new FrameEncoder(request); + this.session = new ServiceSession(this, request, response); + this.operation = new FrameCollector(encoder, session, request, reactor); + this.reason = new Reason(NORMAL_CLOSURE); + this.channel = request.getChannel(); + this.writer = channel.getWriter(); + this.trace = channel.getTrace(); + } + + /** + * This is used to open the channel and begin consuming frames. This + * will also return the session that contains the details for the + * created WebSocket such as the initiating request and response as + * well as the FrameChannel object. + * + * @return the session associated with the WebSocket + */ + public Session open() throws IOException { + trace.trace(OPEN_SOCKET); + operation.run(); + return session; + } + + /** + * This is used to register a FrameListener to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public void register(FrameListener listener) throws IOException { + operation.register(listener); + } + + /** + * This is used to remove a FrameListener from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public void remove(FrameListener listener) throws IOException { + operation.remove(listener); + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param data this is the data that is to be sent + */ + public void send(byte[] data) throws IOException { + encoder.encode(data); + } + + /** + * This is used to send text to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param text this is the text that is to be sent + */ + public void send(String text) throws IOException { + encoder.encode(text); + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param frame this is the frame that is to be sent + */ + public void send(Frame frame) throws IOException { + encoder.encode(frame); + } + + /** + * This is used to close the connection with a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + * + * @param reason the reason for closing the connection + */ + public void close(Reason reason) throws IOException { + encoder.encode(reason); + writer.close(); + } + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + */ + public void close() throws IOException { + encoder.encode(reason); + writer.close(); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java new file mode 100644 index 0000000..579d6ef --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java @@ -0,0 +1,162 @@ +/* + * FrameConsumer.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.transport.ByteCursor; + +/** + * The FrameConsumer object is used to read a WebSocket + * frame as defined by RFC 6455. This is a state machine that can read + * the data one byte at a time until the entire frame has been consumed. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameCollector + */ +class FrameConsumer { + + /** + * This is used to consume the header part of the frame. + */ + private FrameHeaderConsumer header; + + /** + * This is used to interpret the header and create a frame. + */ + private FrameBuilder builder; + + /** + * This is used to buffer the bytes that form the frame. + */ + private byte[] buffer; + + /** + * This is a count of the payload bytes currently consumed. + */ + private int count; + + /** + * Constructor for the FrameConsumer object. This is + * used to create a consumer to read the bytes that form the frame + * from an underlying TCP connection. Internally a buffer is created + * to allow bytes to be consumed and collected in chunks. + */ + public FrameConsumer() { + this.header = new FrameHeaderConsumer(); + this.builder = new FrameBuilder(header); + this.buffer = new byte[2048]; + } + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + public FrameType getType() { + return header.getType(); + } + + /** + * This is used to create a frame object to represent the data that + * has been consumed. The frame created will make a copy of the + * internal byte buffer so this method should be used sparingly. + * + * @return this returns a frame created from the consumed bytes + */ + public Frame getFrame() { + return builder.create(buffer, count); + } + + /** + * This consumes frame bytes using the provided cursor. The consumer + * acts as a state machine by consuming the data as that data + * becomes available, this allows it to consume data asynchronously + * and dispatch once the whole frame has been consumed. + * + * @param cursor the cursor to consume the frame data from + */ + public void consume(ByteCursor cursor) throws IOException { + while (cursor.isReady()) { + if(!header.isFinished()) { + header.consume(cursor); + } + if(header.isFinished()) { + int length = header.getLength(); + + if(count <= length) { + if(buffer.length < length) { + buffer = new byte[length]; + } + if(count < length) { + int size = cursor.read(buffer, count, length - count); + + if(size == -1) { + throw new IOException("Could only read " + count + " of length " + length); + } + count += size; + } + if(count == length) { + if(header.isMasked()) { + byte[] mask = header.getMask(); + + for (int i = 0; i < count; i++) { + buffer[i] ^= mask[i % 4]; + } + } + break; + } + } + } + } + } + + /** + * This is used to determine if the collector has finished. If it + * is not finished the collector will be registered to listen for + * an I/O interrupt to read further bytes of the frame. + * + * @return true if the collector has finished consuming + */ + public boolean isFinished() { + if(header.isFinished()) { + int length = header.getLength(); + + if(count == length) { + return true; + } + } + return false; + } + + /** + * This resets the collector to its original state so that it can + * be reused. Reusing the collector has obvious benefits as it will + * reduce the amount of memory churn for the server. + */ + public void clear() { + header.clear(); + count = 0; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java new file mode 100644 index 0000000..1a99d30 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java @@ -0,0 +1,229 @@ +/* + * FrameEncoder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.FrameType.BINARY; +import static org.simpleframework.http.socket.FrameType.CLOSE; +import static org.simpleframework.http.socket.FrameType.TEXT; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_FRAME; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.CloseCode; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The FrameEncoder is used to encode data as frames as + * defined by RFC 6455. This can encode binary, and text frames as + * well as control frames. All frames generated are written to the + * underlying channel but are not flushed so that multiple frames + * can be buffered before the final flush is made. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConnection + */ +class FrameEncoder { + + /** + * This is the underlying sender used to send the frames. + */ + private final OutputBarrier barrier; + + /** + * This is the TCP channel the frames are delivered over. + */ + private final Channel channel; + + /** + * This is used to trace the traffic on the channel. + */ + private final Trace trace; + + /** + * This is the charset used to encode the text frames with. + */ + private final String charset; + + /** + * Constructor for the FrameEncoder object. This is + * used to create an encoder to sending frames over the provided + * channel. Frames send remain unflushed so they can be batched + * on a single output buffer. + * + * @param request contains the opening handshake information + */ + public FrameEncoder(Request request) { + this(request, "UTF-8"); + } + + /** + * Constructor for the FrameEncoder object. This is + * used to create an encoder to sending frames over the provided + * channel. Frames send remain unflushed so they can be batched + * on a single output buffer. + * + * @param request contains the opening handshake information + * @param charset this is the character encoding to encode with + */ + public FrameEncoder(Request request, String charset) { + this.barrier = new OutputBarrier(request, 5000); + this.channel = request.getChannel(); + this.trace = channel.getTrace(); + this.charset = charset; + } + + /** + * This is used to encode the provided data as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param text this is the data used to encode the frame + * + * @return the size of the generated frame including the header + */ + public int encode(String text) throws IOException { + byte[] data = text.getBytes(charset); + return encode(TEXT, data, true); + } + + /** + * This is used to encode the provided data as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param data this is the data used to encode the frame + * + * @return the size of the generated frame including the header + */ + public int encode(byte[] data) throws IOException { + return encode(BINARY, data, true); + } + + /** + * This is used to encode the provided data as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. A close frame with + * a reason is similar to a text frame with the exception that the + * first two bytes of the frame payload contains the close code as + * a two byte integer in network byte order. The body of the close + * frame may contain UTF-8 encoded data with a reason, the + * interpretation of which is not defined by RFC 6455. + * + * @param reason this is the data used to encode the frame + * + * @return the size of the generated frame including the header + */ + public int encode(Reason reason) throws IOException { + CloseCode code = reason.getCode(); + String text = reason.getText(); + byte[] header = code.getData(); + + if(text != null) { + byte[] data = text.getBytes(charset); + byte[] message = new byte[data.length + 2]; + + message[0] = header[0]; + message[1] = header[1]; + + for(int i = 0; i < data.length; i++) { + message[i + 2] = data[i]; + } + return encode(CLOSE, message, true); + } + return encode(CLOSE, header, true); + } + + /** + * This is used to encode the provided frame as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param frame this is frame that is to be send over the channel + * + * @return the size of the generated frame including the header + */ + public int encode(Frame frame) throws IOException { + FrameType code = frame.getType(); + byte[] data = frame.getBinary(); + boolean last = frame.isFinal(); + + return encode(code, data, last); + } + + /** + * This is used to encode the provided frame as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param type this is the type of frame that is to be encoded + * @param data this is the data used to create the frame + * @param last determines if the is the last frame in a sequence + * + * @return the size of the generated frame including the header + */ + private int encode(FrameType type, byte[] data, boolean last) throws IOException { + byte[] header = new byte[10]; + long length = data.length; + int count = 0; + + if (last) { + header[0] |= 1 << 7; + } + header[0] |= type.code % 128; + + if (length <= 125) { + header[1] = (byte) length; + count = 2; + } else if (length >= 126 && length <= 65535) { + header[1] = (byte) 126; + header[2] = (byte) ((length >>> 8) & 0xff); + header[3] = (byte) (length & 0xff); + count = 4; + } else { + header[1] = (byte) 127; + header[2] = (byte) ((length >>> 56) & 0xff); + header[3] = (byte) ((length >>> 48) & 0xff); + header[4] = (byte) ((length >>> 40) & 0xff); + header[5] = (byte) ((length >>> 32) & 0xff); + header[6] = (byte) ((length >>> 24) & 0xff); + header[7] = (byte) ((length >>> 16) & 0xff); + header[8] = (byte) ((length >>> 8) & 0xff); + header[9] = (byte) (length & 0xff); + count = 10; + } + byte[] reply = new byte[count + data.length]; + + for (int i = 0; i < count; i++) { + reply[i] = header[i]; + } + for (int i = 0; i < length; i++) { + reply[i + count] = data[i]; + } + trace.trace(WRITE_FRAME, type); + barrier.send(reply); + + return reply.length; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java new file mode 100644 index 0000000..a246451 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java @@ -0,0 +1,80 @@ +/* + * FrameHeader.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.socket.FrameType; + +/** + * The FrameHeader represents the variable length header + * used for a WebSocket frame. It is used to determine the number of + * bytes that need to be consumed to successfully process a frame + * from the connected client. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +interface FrameHeader { + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + FrameType getType(); + + /** + * This provides the client mask send with the request. The mask is + * a 32 bit value that is used as an XOR bitmask of the client + * payload. Masking applies only in the client to server direction. + * + * @return this returns the 32 bit mask used for this frame + */ + byte[] getMask(); + + /** + * This provides the length of the payload within the frame. It + * is used to determine how much data to consume from the underlying + * TCP stream in order to recreate the frame to dispatch. + * + * @return the number of bytes used in the frame + */ + int getLength(); + + /** + * This is used to determine if the frame is masked. All client + * frames should be masked according to RFC 6455. If masked the + * payload will have its contents bitmasked with a 32 bit value. + * + * @return this returns true if the payload has been masked + */ + boolean isMasked(); + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + boolean isFinal(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java new file mode 100644 index 0000000..d651ea9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java @@ -0,0 +1,235 @@ +/* + * FrameHeaderConsumer.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.transport.ByteCursor; + +/** + * The FrameHeaderConsumer is used to consume frames from + * a connected TCP channel. This is a state machine that can consume + * the data one byte at a time until the entire header has been consumed. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +class FrameHeaderConsumer implements FrameHeader { + + /** + * This is the frame type which represents the opcode. + */ + private FrameType type; + + /** + * If header consumed was from a client frame the data is masked. + */ + private boolean masked; + + /** + * Determines if this frame is part of a larger sequence. + */ + private boolean last; + + /** + * This is the mask that is used to obfuscate client frames. + */ + private byte[] mask; + + /** + * This is the octet that is used to read one byte at a time. + */ + private byte[] octet; + + /** + * Required number of bytes within the frame header. + */ + private int required; + + /** + * This represents the length of the frame payload. + */ + private int length; + + /** + * This determines the count of the mask bytes read. + */ + private int count; + + /** + * Constructor for the FrameHeaderConsumer object. This + * is used to create a consumer to read the bytes that form the + * frame header from an underlying TCP connection. + */ + public FrameHeaderConsumer() { + this.octet = new byte[1]; + this.mask = new byte[4]; + this.length = -1; + } + + /** + * This provides the length of the payload within the frame. It + * is used to determine how much data to consume from the underlying + * TCP stream in order to recreate the frame to dispatch. + * + * @return the number of bytes used in the frame + */ + public int getLength() { + return length; + } + + /** + * This provides the client mask send with the request. The mask is + * a 32 bit value that is used as an XOR bitmask of the client + * payload. Masking applies only in the client to server direction. + * + * @return this returns the 32 bit mask used for this frame + */ + public byte[] getMask() { + return mask; + } + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + public FrameType getType() { + return type; + } + + /** + * This is used to determine if the frame is masked. All client + * frames should be masked according to RFC 6455. If masked the + * payload will have its contents bitmasked with a 32 bit value. + * + * @return this returns true if the payload has been masked + */ + public boolean isMasked() { + return masked; + } + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + public boolean isFinal() { + return last; + } + + /** + * This consumes frame bytes using the provided cursor. The consumer + * acts as a state machine by consuming the data as that data + * becomes available, this allows it to consume data asynchronously + * and dispatch once the whole frame has been consumed. + * + * @param cursor the cursor to consume the frame data from + */ + public void consume(ByteCursor cursor) throws IOException { + if (cursor.isReady()) { + if (type == null) { + int count = cursor.read(octet); + + if (count <= 0) { + throw new IOException("Ready cursor produced no data"); + } + type = FrameType.resolveType(octet[0] & 0x0f); + + if(type == null) { + throw new IOException("Frame type code not supported"); + } + last = (octet[0] & 0x80) != 0; + } else { + if (length < 0) { + int count = cursor.read(octet); + + if (count <= 0) { + throw new IOException("Ready cursor produced no data"); + } + masked = (octet[0] & 0x80) != 0; + length = (octet[0] & 0x7F); + + if (length == 0x7F) { // 8 byte extended payload length + required = 8; + length = 0; + } else if (length == 0x7E) { // 2 bytes extended payload length + required = 2; + length = 0; + } + } else if (required > 0) { + int count = cursor.read(octet); + + if (count == -1) { + throw new IOException("Could not read length"); + } + length |= (octet[0] & 0xFF) << (8 * --required); + } else { + if (masked && count < mask.length) { + int size = cursor.read(mask, count, mask.length - count); + + if (size == -1) { + throw new IOException("Could not read mask"); + } + count += size; + } + } + } + } + } + + /** + * This is used to determine if the collector has finished. If it + * is not finished the collector will be registered to listen for + * an I/O intrrupt to read further bytes of the frame. + * + * @return true if the collector has finished consuming + */ + public boolean isFinished() { + if(type != null) { + if(length >= 0 && required == 0) { + if(masked) { + return count == mask.length; + } + return true; + } + } + return false; + } + + /** + * This resets the collector to its original state so that it can + * be reused. Reusing the collector has obvious benefits as it will + * reduce the amount of memory churn for the server. + */ + public void clear() { + type = null; + length = -1; + required = 0; + masked = false; + count = 0; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java new file mode 100644 index 0000000..c7528d4 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java @@ -0,0 +1,255 @@ +/* + * FrameProcessor.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.NORMAL_CLOSURE; +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; +import static org.simpleframework.http.socket.service.ServiceEvent.READ_FRAME; +import static org.simpleframework.http.socket.service.ServiceEvent.READ_PING; +import static org.simpleframework.http.socket.service.ServiceEvent.READ_PONG; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_PONG; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.trace.Trace; + +/** + * The FrameProcessor object is used to process incoming + * data and dispatch that data as WebSocket frames. Dispatching of the + * frames is done by making a callback to FrameListener + * objects registered. In addition to frames this will also notify of + * any errors that occur or on connection closure. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +class FrameProcessor { + + /** + * This is the set of listeners to dispatch frames to. + */ + private final Set listeners; + + /** + * This is used to extract the reason description from a frame. + */ + private final ReasonExtractor extractor; + + /** + * This is used to consume the frames from the underling channel. + */ + private final FrameConsumer consumer; + + /** + * This is the encoder that is used to send control messages. + */ + private final FrameEncoder encoder; + + /** + * This is used to determine if a close notification was sent. + */ + private final AtomicBoolean closed; + + /** + * This is the cursor used to maintain a read seek position. + */ + private final ByteCursor cursor; + + /** + * This is the session associated with the WebSocket connection. + */ + private final Session session; + + /** + * This is the underlying TCP channel this reads frames from. + */ + private final Channel channel; + + /** + * This is the reason message used for a normal closure. + */ + private final Reason normal; + + /** + * This is used to trace the events that occur on the channel. + */ + private final Trace trace; + + /** + * Constructor for the FrameProcessor object. This is + * used to create a processor that can consume and dispatch frames + * as defined by RFC 6455 to a set of registered listeners. + * + * @param encoder this is the encoder used to send control frames + * @param session this is the session associated with the channel + * @param channel this is the channel to read frames from + */ + public FrameProcessor(FrameEncoder encoder, Session session, Request request) { + this.listeners = new CopyOnWriteArraySet(); + this.normal = new Reason(NORMAL_CLOSURE); + this.extractor = new ReasonExtractor(); + this.consumer = new FrameConsumer(); + this.closed = new AtomicBoolean(); + this.channel = request.getChannel(); + this.cursor = channel.getCursor(); + this.trace = channel.getTrace(); + this.encoder = encoder; + this.session = session; + } + + /** + * This is used to register a FrameListener to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public void register(FrameListener listener) { + listeners.add(listener); + } + + /** + * This is used to remove a FrameListener from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public void remove(FrameListener listener) { + listeners.remove(listener); + } + + /** + * This is used to process frames consumed from the underlying TCP + * connection. It will respond to control frames such as pings and + * will also handle close frames. Each frame, regardless of its + * type will be dispatched to any FrameListener objects + * that are registered with the processor. If an a close frame is + * received it will echo that close frame, with the same close code + * and back to the sender as suggested by RFC 6455 section 5.5.1. + */ + public void process() throws IOException { + if(cursor.isReady()) { + consumer.consume(cursor); + + if(consumer.isFinished()) { + Frame frame = consumer.getFrame(); + FrameType type = frame.getType(); + + trace.trace(READ_FRAME, type); + + if(type.isPong()) { + trace.trace(READ_PONG); + } + if(type.isPing()){ + Frame response = frame.getFrame(FrameType.PONG); + + trace.trace(READ_PING); + encoder.encode(response); + trace.trace(WRITE_PONG); + } + for(FrameListener listener : listeners) { + listener.onFrame(session, frame); + } + if(type.isClose()){ + Reason reason = extractor.extract(frame); + + if(reason != null) { + close(reason); + } else { + close(); + } + } + consumer.clear(); + } + } + } + + /** + * This is used to report failures back to the client. Any I/O + * or frame processing exception is reported back to all of the + * registered listeners so that they can take action. The + * underlying TCP connection is closed after any failure. + * + * @param reason this is the cause of the failure + */ + public void failure(Exception reason) throws IOException { + if(!closed.getAndSet(true)) { + for(FrameListener listener : listeners) { + try { + listener.onError(session, reason); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } + } + } + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. All registered listeners will be + * notified of the close event. + * + * @param reason this is the reason for the connection closure + */ + public void close(Reason reason) throws IOException{ + if(!closed.getAndSet(true)) { + for(FrameListener listener : listeners) { + try { + listener.onClose(session, reason); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } + } + } + + /** + * This is used to close the connection when it has not responded + * to any activity for a configured period of time. It may be + * possible to send up a control frame, however if the TCP channel + * is closed this will just notify the listeners. + */ + public void close() throws IOException{ + if(!closed.getAndSet(true)) { + try { + for(FrameListener listener : listeners) { + listener.onClose(session, normal); + } + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java new file mode 100644 index 0000000..3da2635 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java @@ -0,0 +1,99 @@ +/* + * OutputBarrier.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.io.IOException; +import java.util.concurrent.locks.ReentrantLock; + +import org.simpleframework.http.Request; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; + +/** + * The OutputBarrier is used to ensure that control + * frames and data frames do not get sent at the same time. Sending + * both at the same time could lead to the status checking thread + * being blocked and this could eventually exhaust the thread pool. + * + * @author Niall Gallagher + */ +class OutputBarrier { + + /** + * This is used to check if there is an operation in progress. + */ + private final ReentrantLock lock; + + /** + * This is the underlying sender used to send the frames. + */ + private final ByteWriter writer; + + /** + * This is the TCP channel the frames are delivered over. + */ + private final Channel channel; + + /** + * This is the length of time to wait before failing to lock. + */ + private final long duration; + + /** + * Constructor for the OutputBarrier object. This + * is used to ensure that if there is currently a blocking write + * in place that the SessionChecker will not end up + * being blocked if it attempts to send a control frame. + * + * @param request this is the request to get the TCP channel from + * @param duration this is the length of time to wait for the lock + */ + public OutputBarrier(Request request, long duration) { + this.lock = new ReentrantLock(); + this.channel = request.getChannel(); + this.writer = channel.getWriter(); + this.duration = duration; + } + + /** + * This method is used to send all frames. It is important that + * a lock is used to protect this so that if there is an attempt + * to send out a control frame while the connection is blocked + * there is an exception thrown. + * + * @param frame this is the frame to send over the TCP channel + */ + public void send(byte[] frame) throws IOException { + try { + if(!lock.tryLock(duration, MILLISECONDS)) { + throw new IOException("Transport lock could not be acquired"); + } + try { + writer.write(frame); + writer.flush(); // less throughput, better latency + } finally { + lock.unlock(); + } + } catch(Exception e) { + throw new IOException("Error writing to transport", e); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java new file mode 100644 index 0000000..9deb66a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java @@ -0,0 +1,111 @@ +/* + * PathRouter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_PROTOCOL; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.simpleframework.http.Path; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The PathRouter is used when there are multiple + * services that can be used. Each service is selected based on the + * path sent in the initiating request. If a match cannot be made + * based on the request then a default service us chosen. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public class PathRouter implements Router { + + /** + * This is the set of services that can be selected. + */ + private final Map registry; + + /** + * This is the default service chosen if there is no match. + */ + private final Service primary; + + /** + * Constructor for the PathRouter object. This is used + * to create a router using a selection of services that can be + * selected using the path provided in the initiating request. + * + * @param registry this is the registry of available services + * @param primary this is the default service to use + */ + public PathRouter(Map registry, Service primary) throws IOException { + this.registry = registry; + this.primary = primary; + } + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this will return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + public Service route(Request request, Response response) { + String token = request.getValue(UPGRADE); + + if(token != null) { + if(token.equalsIgnoreCase(WEBSOCKET)) { + List protocols = request.getValues(SEC_WEBSOCKET_PROTOCOL); + String version = request.getValue(SEC_WEBSOCKET_VERSION); + Path path = request.getPath(); + String normal = path.getPath(); + + if(version != null) { + response.setValue(SEC_WEBSOCKET_VERSION, version); + } + for(String protocol : protocols) { + String original = response.getValue(SEC_WEBSOCKET_PROTOCOL); + + if(original == null) { + response.setValue(SEC_WEBSOCKET_PROTOCOL, protocol); + } + } + Service service = registry.get(normal); + + if(service != null) { + return service; + } + return primary; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java new file mode 100644 index 0000000..54060c9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java @@ -0,0 +1,105 @@ +/* + * ProtocolRouter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_PROTOCOL; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The ProtocolRouter is used when there are multiple + * services that can be used. Each service is selected based on the + * protocol sent in the initiating request. If a match cannot be + * made based on the request then a default service us chosen. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public class ProtocolRouter implements Router { + + /** + * This is the set of services that can be selected. + */ + private final Map registry; + + /** + * This is the default service chosen if there is no match. + */ + private final Service primary; + + /** + * Constructor for the ProtocolRouter object. This is + * used to create a router using a selection of services that can + * be selected using the Sec-WebSocket-Protocol header + * sent in the initiating request by the client. + * + * @param registry this is the registry of available services + * @param primary this is the default service to use + */ + public ProtocolRouter(Map registry, Service primary) throws IOException { + this.registry = registry; + this.primary = primary; + } + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this will return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + public Service route(Request request, Response response) { + String token = request.getValue(UPGRADE); + + if(token != null) { + if(token.equalsIgnoreCase(WEBSOCKET)) { + List protocols = request.getValues(SEC_WEBSOCKET_PROTOCOL); + String version = request.getValue(SEC_WEBSOCKET_VERSION); + + if(version != null) { + response.setValue(SEC_WEBSOCKET_VERSION, version); + } + for(String protocol : protocols) { + Service service = registry.get(protocol); + + if(service != null) { + response.setValue(SEC_WEBSOCKET_PROTOCOL, protocol); + return service; + } + } + return primary; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java new file mode 100644 index 0000000..fb6ce88 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java @@ -0,0 +1,114 @@ +/* + * ReasonExtractor.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.NO_STATUS_CODE; + +import org.simpleframework.http.socket.CloseCode; +import org.simpleframework.http.socket.DataConverter; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.Reason; + +/** + * The ReasonExtractor object is used to extract the close + * reason from a frame payload. If their is no close reason this will + * return a Reason with just the close code. Finally in + * the event of a botched frame being sent with no close code then the + * close code 1005 is used to indicate no reason. + * + * @author Niall Gallagher + */ +class ReasonExtractor { + + /** + * This is the data converter object used to convert data. + */ + private final DataConverter converter; + + /** + * Constructor for the ReasonExtractor object. This + * is used to create an extractor for close code and the close + * reason descriptions. All descriptions are decoded using the + * UTF-8 character encoding. + */ + public ReasonExtractor() { + this.converter = new DataConverter(); + } + + /** + * This is used to extract a reason from the provided frame. The + * close reason is taken from the first two bytes of the frame + * payload and the UTF-8 string that follows is the description. + * + * @param frame this is the frame to extract the reason from + * + * @return a reason containing the close code and reason + */ + public Reason extract(Frame frame) { + byte[] data = frame.getBinary(); + + if(data.length > 0) { + CloseCode code = extractCode(data); + String text = extractText(data); + + return new Reason(code, text); + } + return new Reason(NO_STATUS_CODE); + } + + /** + * This method is used to extract the UTF-8 description from the + * frame payload. If there are only two bytes within the payload + * then this will return null for the reason. + * + * @param data the frame payload to extract the description from + * + * @return returns the description within the payload + */ + private String extractText(byte[] data) { + int length = data.length - 2; + + if(length > 0) { + return converter.convert(data, 2, length); + } + return null; + } + + /** + * This method is used to extract the close code. The close code + * is an two byte integer in network byte order at the start + * of the close frame payload. This code is required by RFC 6455 + * however if not code is available code 1005 is returned. + * + * @param data the frame payload to extract the description from + * + * @return returns the description within the payload + */ + private CloseCode extractCode(byte[] data) { + int length = data.length; + + if(length > 0) { + int high = data[0]; + int low = data[1]; + + return CloseCode.resolveCode(high, low); + } + return NO_STATUS_CODE; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java new file mode 100644 index 0000000..7e47dc3 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java @@ -0,0 +1,137 @@ +/* + * RequestValidator.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.CONNECTION; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_KEY; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import java.util.List; + +import org.simpleframework.http.Request; + +/** + * The RequestValidator object is used to ensure requests + * for confirm to RFC 6455 section 4.2.1. The client opening handshake + * must consist of several parts, including a version of 13 referring + * to RFC 6455, a WebSocket key, and the required HTTP connection + * details. If any of these are missing the server is obliged to + * respond with a HTTP 400 response indicating a bad request. + * + * @author Niall Gallagher + */ +class RequestValidator { + + /** + * This is the request forming the client part of the handshake. + */ + private final Request request; + + /** + * This is the version referring to the required client version. + */ + private final String version; + + /** + * Constructor for the RequestValidator object. This + * is used to create a plain vanilla validator that uses version + * 13 as dictated by RFC 6455 section 4.2.1. + * + * @param request this is the handshake request from the client + */ + public RequestValidator(Request request) { + this(request, "13"); + } + + /** + * Constructor for the RequestValidator object. This + * is used to create a plain vanilla validator that uses version + * 13 as dictated by RFC 6455 section 4.2.1. + * + * @param request this is the handshake request from the client + * @param version a version other than 13 if desired + */ + public RequestValidator(Request request, String version) { + this.request = request; + this.version = version; + } + + /** + * This is used to determine if the client handshake request had + * all the required headers as dictated by RFC 6455 section 4.2.1. + * If the request does not contain any of these parts then this + * will return false, indicating a HTTP 400 response should be + * sent to the client. + * + * @return true if the request was a valid handshake + */ + public boolean isValid() { + if(!isProtocol()) { + return false; + } + if(!isUpgrade()) { + return false; + } + return true; + } + + /** + * This is used to determine if the request is a valid WebSocket + * handshake of the correct version. This also checks to see if + * the request contained the required handshake token. + * + * @return this returns true if the request is a valid handshake + */ + private boolean isProtocol() { + String protocol = request.getValue(SEC_WEBSOCKET_VERSION); + String token = request.getValue(SEC_WEBSOCKET_KEY); + + if(token != null) { + return version.equals(protocol); + } + return false; + } + + /** + * Here we check to ensure that there is a HTTP connection header + * with the required upgrade token. The upgrade token may be + * one of many, so all must be checked. Finally to ensure that + * the upgrade is for a WebSocket the upgrade header is checked. + * + * @return this returns true if the request is an upgrade + */ + private boolean isUpgrade() { + List tokens = request.getValues(CONNECTION); + + for(String token : tokens) { + if(token.equalsIgnoreCase(UPGRADE)) { + String upgrade = request.getValue(UPGRADE); + + if(upgrade != null) { + return upgrade.equalsIgnoreCase(WEBSOCKET); + } + return false; + } + } + return false; + } + +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java new file mode 100644 index 0000000..0ba780e --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java @@ -0,0 +1,159 @@ +/* + * ResponseBuilder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.CLOSE; +import static org.simpleframework.http.Protocol.CONNECTION; +import static org.simpleframework.http.Protocol.DATE; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_ACCEPT; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_HEADER; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.Status; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +/** + * The ResponseBuilder object is used to build a response + * to a WebSocket handshake. In order for a successful handshake to + * complete a HTTP request must have a version of 13 referring + * to RFC 6455, a WebSocket key, and the required HTTP connection + * details. If any of these are missing the server is obliged to + * respond with a HTTP 400 response indicating a bad request. + * + * @author Niall Gallagher + */ +class ResponseBuilder { + + /** + * This is used to validate the initiating WebSocket request. + */ + private final RequestValidator validator; + + /** + * This is the accept token generated for the request. + */ + private final AcceptToken token; + + /** + * This is the sender used to send the WebSocket response. + */ + private final ByteWriter writer; + + /** + * This is the response to the WebSocket handshake. + */ + private final Response response; + + /** + * This is the underlying TCP channel for the request. + */ + private final Channel channel; + + /** + * This is used to trace the activity for the handshake. + */ + private final Trace trace; + + /** + * Constructor for the ResponseBuilder object. In order + * to process the WebSocket handshake this requires the original + * request and the response as well as the underlying TCP channel + * which forms the basis of the WebSocket connection. + * + * @param request this is the request that initiated the handshake + * @param response this is the response for the handshake + */ + public ResponseBuilder(Request request, Response response) throws Exception { + this.validator = new RequestValidator(request); + this.token = new AcceptToken(request); + this.channel = request.getChannel(); + this.writer = channel.getWriter(); + this.trace = channel.getTrace(); + this.response = response; + } + + /** + * This is used to determine if the client handshake request had + * all the required headers as dictated by RFC 6455 section 4.2.1. + * If the request does not contain any of these parts then this + * will return false, indicating a HTTP 400 response is sent to + * the client, otherwise a HTTP 101 response is sent. + */ + public void commit() throws IOException { + if(validator.isValid()) { + accept(); + } else { + reject(); + } + } + + /** + * This is used to respond to the client with a HTTP 400 response + * indicating the WebSocket handshake failed. No response body is + * sent with the rejection message and the underlying TCP channel + * is closed to prevent further use of the connection. + */ + private void reject() throws IOException { + long time = System.currentTimeMillis(); + + response.setStatus(Status.BAD_REQUEST); + response.setValue(CONNECTION, CLOSE); + response.setDate(DATE, time); + + String header = response.toString(); + byte[] message = header.getBytes("UTF-8"); + + trace.trace(WRITE_HEADER, header); + writer.write(message); + writer.flush(); + writer.close(); + } + + /** + * This is used to respond to the client with a HTTP 101 response + * to indicate that the WebSocket handshake succeeeded. Once this + * response has been sent all traffic between the client and + * server will be with WebSocket frames as defined by RFC 6455. + */ + private void accept() throws IOException { + long time = System.currentTimeMillis(); + String accept = token.create(); + + response.setStatus(Status.SWITCHING_PROTOCOLS); + response.setDescription(UPGRADE); + response.setValue(CONNECTION, UPGRADE); + response.setDate(DATE, time); + response.setValue(SEC_WEBSOCKET_ACCEPT, accept); + response.setValue(UPGRADE, WEBSOCKET); + + String header = response.toString(); + byte[] message = header.getBytes("UTF-8"); + + trace.trace(WRITE_HEADER, header); + writer.write(message); + writer.flush(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java new file mode 100644 index 0000000..3b466f5 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java @@ -0,0 +1,59 @@ +/* + * Router.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The Router interface represents a means of routing + * a session initiating request to the correct service. Typically + * a service is chosen based on the sub-protocol provided in the + * initiating request, however it can be chosen on any criteria + * available in the request. An initiating request must contain + * a Connection header with the websocket + * token according to RFC 6455 section 4.2.1. If the request does + * not contain this token it is treated as a normal request and + * a Service will not be resolved. + *

+ * If a service has been successfully chosen from the initiating + * request the the value of Sec-WebSocket-Protocol will + * contain either the chosen protocol if a match was made with the + * initiating request or null to indicate a default choice. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public interface Router { + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this must return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + Service route(Request request, Response response); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java new file mode 100644 index 0000000..3b018a9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java @@ -0,0 +1,109 @@ +/* + * RouterContainer.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.Container; + +/** + * The RouterContainer is used to route requests that + * satisfy a WebSocket opening handshake to a specific service. Each + * request intercepted by this Container implementation + * is examined for opening handshake criteria as specified by RFC 6455, + * and if it contains the required information it is router to a + * specific service using a Router implementation. If the + * request does not contain the required criteria it is handled by + * an internal container delegate. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.Router + */ +public class RouterContainer implements Container { + + /** + * This is the service dispatcher used to dispatch requests. + */ + private final ServiceDispatcher dispatcher; + + /** + * This is the container used to handle traditional requests. + */ + private final Container container; + + /** + * This is the router used to select specific services. + */ + private final Router router; + + /** + * Constructor for the RouterContainer object. This + * requires a container to delegate traditional requests to and + * a Router implementation which can be used to + * select a service to dispatch a WebSocket session to. + * + * @param container this is the container to delegate to + * @param router this is the router used to select services + * @param threads this contains the number of threads to use + */ + public RouterContainer(Container container, Router router, int threads) throws IOException { + this(container, router, threads, 10000); + } + + /** + * Constructor for the RouterContainer object. This + * requires a container to delegate traditional requests to and + * a Router implementation which can be used to + * select a service to dispatch a WebSocket session to. + * + * @param container this is the container to delegate to + * @param router this is the router used to select services + * @param threads this contains the number of threads to use + * @param ping this is the frequency to send ping frames with + */ + public RouterContainer(Container container, Router router, int threads, long ping) throws IOException { + this.dispatcher = new ServiceDispatcher(router, threads, ping); + this.container = container; + this.router = router; + } + + /** + * This method is used to create a dispatch a Session to + * a specific service selected by a router. If the session initiating + * handshake fails for any reason this will close the underlying TCP + * connection and send a HTTP 400 response back to the client. All + * traditional requests that do not represent an WebSocket opening + * handshake are dispatched to the internal container. + * + * @param req the request that contains the client HTTP message + * @param resp the response used to deliver the server response + */ + public void handle(Request req, Response resp) { + Service service = router.route(req, resp); + + if(service != null) { + dispatcher.dispatch(req, resp); + } else { + container.handle(req, resp); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java new file mode 100644 index 0000000..d95c01f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java @@ -0,0 +1,44 @@ +/* + * Service.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.socket.Session; + +/** + * The Service interface represents a service that can be + * used to communicate with the WebSocket protocol defined in RFC 6455. + * Typically a service will implement a sub-protocol negotiated from + * the initiating HTTP request. The service should be considered a + * hand off point rather than an place to implement business logic. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +public interface Service { + + /** + * This method connects a new session with a service implementation. + * Connecting a session with a service in this way should not block + * as it could cause starvation of the servicing thread pool. + * + * @param session the new session to connect to the service + */ + void connect(Session session); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java new file mode 100644 index 0000000..ad5325c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java @@ -0,0 +1,149 @@ +/* + * ServiceChannel.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.FrameChannel; + +/** + * The ServiceChannel represents a full duplex communication + * channel as defined by RFC 6455. Any instance of this will provide + * a means to perform asynchronous writes and reads to a remote client + * using a lightweight framing protocol. A frame is a finite length + * sequence of bytes that can hold either text or binary data. Also, + * control frames are used to perform heartbeat monitoring and closure. + *

+ * For convenience frames can be consumed from the socket via a + * callback to a registered listener. This avoids having to poll each + * socket for data and provides a asynchronous event driven model of + * communication, which greatly reduces overhead and complication. + * + * @author Niall Gallagher + */ +class ServiceChannel implements FrameChannel { + + /** + * This is the internal channel for full duplex communication. + */ + private final FrameChannel channel; + + /** + * Constructor for the ServiceChannel object. This is + * used to create a channel that is given to the application. This + * is synchronized so only one frame can be dispatched at a time. + * + * @param channel this is the channel to delegate to + */ + public ServiceChannel(FrameChannel channel) { + this.channel = channel; + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param data this is the data that is to be sent + */ + public synchronized void send(byte[] data) throws IOException { + channel.send(data); + } + + /** + * This is used to send text to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param text this is the text that is to be sent + */ + public synchronized void send(String text) throws IOException { + channel.send(text); + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param frame this is the frame that is to be sent + */ + public synchronized void send(Frame frame) throws IOException { + channel.send(frame); + } + + /** + * This is used to register a FrameListener to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public synchronized void register(FrameListener listener) throws IOException { + channel.register(listener); + } + + /** + * This is used to remove a FrameListener from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public synchronized void remove(FrameListener listener) throws IOException { + channel.remove(listener); + } + + /** + * This is used to close the connection with a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + * + * @param reason the reason for closing the connection + */ + public synchronized void close(Reason reason) throws IOException { + channel.close(reason); + } + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + */ + public void close() throws IOException { + channel.close(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java new file mode 100644 index 0000000..509495f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java @@ -0,0 +1,101 @@ +/* + * ServiceDispatcher.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.common.thread.ConcurrentScheduler; +import org.simpleframework.common.thread.Scheduler; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.transport.reactor.ExecutorReactor; +import org.simpleframework.transport.reactor.Reactor; + +/** + * The ServiceDispatcher object is used to perform the + * opening handshake for a WebSocket session. Once the session has been + * established it is connected to a Service where frames + * can be sent and received. If for any reason the handshake fails + * this will terminated the connection with a HTTP 400 response. + * + * @author Niall Gallagher + */ +class ServiceDispatcher { + + /** + * This is the session dispatcher used to dispatch the session. + */ + private final SessionDispatcher dispatcher; + + /** + * This is used to build the sessions from the handshake request. + */ + private final SessionBuilder builder; + + /** + * This is used asynchronously read frames from the TCP channel. + */ + private final Scheduler scheduler; + + /** + * This is used to notify of read events on the TCP channel. + */ + private final Reactor reactor; + + /** + * Constructor for the ServiceDispatcher object. The + * dispatcher created will dispatch WebSocket sessions to a service + * using the provided Router instance. + * + * @param router this is the router used to select a service + * @param threads this is the number of threads to use + */ + public ServiceDispatcher(Router router, int threads) throws IOException { + this(router, threads, 10000); + } + + /** + * Constructor for the ServiceDispatcher object. The + * dispatcher created will dispatch WebSocket sessions to a service + * using the provided Router instance. + * + * @param router this is the router used to select a service + * @param threads this is the number of threads to use + * @param ping this is the frequency used to send ping frames + */ + public ServiceDispatcher(Router router, int threads, long ping) throws IOException { + this.scheduler = new ConcurrentScheduler(FrameCollector.class, threads); + this.reactor = new ExecutorReactor(scheduler); + this.builder = new SessionBuilder(scheduler, reactor, ping); + this.dispatcher = new SessionDispatcher(builder, router); + } + + /** + * This method is used to create a dispatch a Session to + * a specific service selected by a router. If the session initiating + * handshake fails for any reason this will close the underlying TCP + * connection and send a HTTP 400 response back to the client. + * + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public void dispatch(Request request, Response response) { + dispatcher.dispatch(request, response); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java new file mode 100644 index 0000000..a5d0079 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java @@ -0,0 +1,97 @@ +/* + * ServiceEvent.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +/** + * The ServiceEvent enumeration contains the events that + * are dispatched processing a WebSocket. To see how a WebSocket is + * behaving and to gather performance statistics the service events + * can be intercepted using a custom TraceAnalyzer object. + * + * @author Niall Gallagher + * + * @see org.simpleframework.transport.trace.TraceAnalyzer + */ +public enum ServiceEvent { + + /** + * This event is dispatched when a WebSocket is connected. + */ + OPEN_SOCKET, + + /** + * This event is dispatched when a WebSocket is dispatched. + */ + DISPATCH_SOCKET, + + /** + * This event is dispatched when a WebSocket channel is closed. + */ + TERMINATE_SOCKET, + + /** + * This event is dispatched when the response handshake is sent. + */ + WRITE_HEADER, + + /** + * This event is dispatched when the WebSocket receives a ping. + */ + READ_PING, + + /** + * This event is dispatched when a ping is sent over a WebSocket. + */ + WRITE_PING, + + /** + * This event is dispatched when the WebSocket receives a pong. + */ + READ_PONG, + + /** + * This event is dispatched when a pong is sent over a WebSocket. + */ + WRITE_PONG, + + /** + * This event is dispatched when a frame is read from a WebSocket. + */ + READ_FRAME, + + /** + * This event is dispatched when a frame is sent over a WebSocket. + */ + WRITE_FRAME, + + /** + * This indicates that there has been no response to a ping. + */ + PING_EXPIRED, + + /** + * This indicates that there has been no response to a ping. + */ + PONG_RECEIVED, + + /** + * This event is dispatched when an error occurs with a WebSocket. + */ + ERROR; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java new file mode 100644 index 0000000..b8fc083 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java @@ -0,0 +1,139 @@ +/* + * ServiceSession.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.util.Map; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.FrameChannel; +import org.simpleframework.http.socket.Session; + +/** + * The ServiceSession represents a simple WebSocket session + * that contains the connection handshake details and the actual socket. + * In order to determine how the session should be interacted with the + * protocol is conveniently exposed, however all attributes of the + * original HTTP request are available. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +class ServiceSession implements Session { + + /** + * The WebSocket used for asynchronous full duplex communication. + */ + private final FrameChannel channel; + + /** + * This is the initiating response associated with the session. + */ + private final Response response; + + /** + * This is the initiating request associated with the session. + */ + private final Request request; + + /** + * This is the bag of attributes used by this session. + */ + private final Map attributes; + + /** + * Constructor for the ServiceSession object. This is used + * to create the session that will be used by a Service to + * send and receive WebSocket frames. + * + * @param channel this is the actual WebSocket for the session + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public ServiceSession(FrameChannel channel, Request request, Response response) { + this.channel = new ServiceChannel(channel); + this.attributes = request.getAttributes(); + this.response = response; + this.request = request; + } + + /** + * This can be used to retrieve the response attributes. These can + * be used to keep state with the response when it is passed to + * other systems for processing. Attributes act as a convenient + * model for storing objects associated with the response. This + * also inherits attributes associated with the client connection. + * + * @return the attributes of that have been set on the request + */ + public Map getAttributes() { + return attributes; + } + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the attribute Map + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param key this is the key of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + public Object getAttribute(Object key) { + return attributes.get(key); + } + + /** + * Provides a WebSocket that can be used to communicate + * with the connected client. Communication is full duplex and also + * asynchronous through the use of a FrameListener that + * can be registered with the socket. + * + * @return a web socket for full duplex communication + */ + public FrameChannel getChannel() { + return channel; + } + + /** + * Provides the Request used to initiate the session. + * This is useful in establishing the identity of the user, acquiring + * an security information and also for determining the request path + * that was used, which be used to establish context. + * + * @return the request used to initiate the session + */ + public Request getRequest() { + return request; + } + + /** + * Provides the Response used to establish the session + * with the remote client. This is useful in establishing the protocol + * used to create the session and also for determining various other + * useful contextual information. + * + * @return the response used to establish the session + */ + public Response getResponse() { + return response; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java new file mode 100644 index 0000000..59864ee --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java @@ -0,0 +1,93 @@ +/* + * SessionBuilder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.common.thread.Scheduler; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.reactor.Reactor; + +/** + * The SessionBuilder object is used to create sessions + * for connected WebSockets. Before the session is created a response + * is sent back to the connected client. If for some reason the session + * is not valid or does not conform to the requirements of RFC 6455 + * then a HTTP 400 response code is sent and the TCP channel is closed. + * + * @author Niall Gallagher + */ +class SessionBuilder { + + /** + * This is the scheduler that is used to ping WebSocket sessions. + */ + private final Scheduler scheduler; + + /** + * This is the reactor used to register for I/O notifications. + */ + private final Reactor reactor; + + /** + * This is the frequency the server should send out ping frames. + */ + private final long ping; + + /** + * Constructor for the SessionBuilder object. This is + * used to create sessions using the request and response associated + * with the WebSocket opening handshake. + * + * @param scheduler this is the shared thread pool used for pinging + * @param reactor this is used to check for I/O notifications + * @param ping this is the frequency to send out ping frames + */ + public SessionBuilder(Scheduler scheduler, Reactor reactor, long ping) { + this.scheduler = scheduler; + this.reactor = reactor; + this.ping = ping; + } + + /** + * This is used to create a WebSocket session. If at any point there + * is an error creating the session the underlying TCP connection is + * closed and a Session is returned regardless. + * + * @param request this is the request associated with this session + * @param response this is the response associated with this session + * + * @return this returns the session associated with the WebSocket + */ + public Session create(Request request, Response response) throws Exception { + FrameConnection connection = new FrameConnection(request, response, reactor); + ResponseBuilder builder = new ResponseBuilder(request, response); + StatusChecker checker = new StatusChecker(connection, request, scheduler, ping); + + try { + builder.commit(); + checker.start(); + } catch(Exception e) { + throw new IOException("Could not send response", e); + } + return connection.open(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java new file mode 100644 index 0000000..6543be0 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java @@ -0,0 +1,111 @@ +/* + * SessionDispatcher.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.service.ServiceEvent.DISPATCH_SOCKET; +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; +import static org.simpleframework.http.socket.service.ServiceEvent.TERMINATE_SOCKET; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The SessionDispatcher object is used to perform the + * opening handshake for a WebSocket session. Once the session has been + * established it is connected to a Service where frames + * can be sent and received. If for any reason the handshake fails + * this will terminated the connection with a HTTP 400 response. + * + * @author Niall Gallagher + */ +class SessionDispatcher { + + /** + * This is used to create the session for the WebSocket. + */ + private final SessionBuilder builder; + + /** + * This is used to select the service to dispatch to. + */ + private final Router router; + + /** + * Constructor for the SessionDispatcher object. The + * dispatcher created will dispatch WebSocket sessions to a service + * using the provided Router instance. + * + * @param builder this is used to build the WebSocket session + * @param router this is used to select the service + */ + public SessionDispatcher(SessionBuilder builder, Router router) { + this.builder = builder; + this.router = router; + } + + /** + * This method is used to create a dispatch a Session to + * a specific service selected by a router. If the session initiating + * handshake fails for any reason this will close the underlying TCP + * connection and send a HTTP 400 response back to the client. + * + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public void dispatch(Request request, Response response) { + Channel channel = request.getChannel(); + Trace trace = channel.getTrace(); + + try { + Service service = router.route(request, response); + Session session = builder.create(request, response); + + trace.trace(DISPATCH_SOCKET); + service.connect(session); + } catch(Exception cause) { + trace.trace(ERROR, cause); + terminate(request, response); + } + } + + /** + * This method is used to terminate the connection and commit the + * response. Terminating the session before it has been dispatched + * is done when there is a protocol or an unexpected I/O error with + * the underlying TCP channel. + * + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public void terminate(Request request, Response response) { + Channel channel = request.getChannel(); + Trace trace = channel.getTrace(); + + try { + response.close(); + channel.close(); + trace.trace(TERMINATE_SOCKET); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java new file mode 100644 index 0000000..1f4f0d7 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java @@ -0,0 +1,220 @@ +/* + * StatusChecker.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.INTERNAL_SERVER_ERROR; +import static org.simpleframework.http.socket.CloseCode.NORMAL_CLOSURE; +import static org.simpleframework.http.socket.FrameType.PING; +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; +import static org.simpleframework.http.socket.service.ServiceEvent.PING_EXPIRED; +import static org.simpleframework.http.socket.service.ServiceEvent.PONG_RECEIVED; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_PING; + +import java.util.concurrent.atomic.AtomicLong; + +import org.simpleframework.common.thread.Scheduler; +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.DataFrame; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The StatusChecker object is used to perform health + * checks on connected sessions. Health is determined using the ping + * pong protocol defined in RFC 6455. The ping pong protocol requires + * that any endpoint must respond to a ping control frame with a pong + * control frame containing the same payload. This session checker + * will send out out ping controls frames and wait for a pong frame. + * If it does not receive a pong frame after a configured expiry time + * then it will close the associated session. + * + * @author Niall Gallagher + */ +class StatusChecker implements Runnable{ + + /** + * This is used to perform the monitoring of the sessions. + */ + private final StatusResultListener listener; + + /** + * This is the WebSocket this this pinger will be monitoring. + */ + private final FrameConnection connection; + + /** + * This is the shared scheduler used to execute this checker. + */ + private final Scheduler scheduler; + + /** + * This is a count of the number of unacknowledged ping frames. + */ + private final AtomicLong counter; + + /** + * This is the underling TCP channel that is being checked. + */ + private final Channel channel; + + /** + * The only reason for a close is for an unexpected error. + */ + private final Reason normal; + + /** + * The only reason for a close is for an unexpected error. + */ + private final Reason error; + + /** + * This is used to trace various events for this pinger. + */ + private final Trace trace; + + /** + * This is the frame that contains the ping to send. + */ + private final Frame frame; + + /** + * This is the frequency with which the checker should run. + */ + private final long frequency; + + /** + * Constructor for the StatusChecker object. This + * is used to create a pinger that will send out ping frames at + * a specified interval. If a session does not respond within + * three times the duration of the ping the connection is reset. + * + * @param connection this is the WebSocket to send the frames + * @param request this is the associated request + * @param scheduler this is the scheduler used to execute this + * @param frequency this is the frequency with which to ping + */ + public StatusChecker(FrameConnection connection, Request request, Scheduler scheduler, long frequency) { + this.listener = new StatusResultListener(this); + this.error = new Reason(INTERNAL_SERVER_ERROR); + this.normal = new Reason(NORMAL_CLOSURE); + this.frame = new DataFrame(PING); + this.counter = new AtomicLong(); + this.channel = request.getChannel(); + this.trace = channel.getTrace(); + this.connection = connection; + this.scheduler = scheduler; + this.frequency = frequency; + } + + /** + * This is used to kick of the status checking. Here an initial + * ping is sent over the socket and the task is then scheduled to + * check the result after the frequency period has expired. If + * this method fails for any reason the TCP channel is closed. + */ + public void start() { + try { + connection.register(listener); + trace.trace(WRITE_PING); + connection.send(frame); + counter.getAndIncrement(); + scheduler.execute(this, frequency); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * This method is used to check to see if a session has expired. + * If there have been three unacknowledged ping events then this + * will force a closure of the WebSocket connection. This is done + * to ensure only healthy connections are maintained within the + * server, also RFC 6455 recommends using the ping pong protocol. + */ + public void run() { + long count = counter.get(); + + try { + if(count < 3) { + trace.trace(WRITE_PING); + connection.send(frame); + counter.getAndIncrement(); + scheduler.execute(this, frequency); // schedule the next one + } else { + trace.trace(PING_EXPIRED); + connection.close(normal); + } + } catch (Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * If the connection gets a response to its ping message then this + * will reset the internal counter. This ensure that the connection + * does not time out. If after three pings there is not response + * from the other side then the connection will be terminated. + */ + public void refresh() { + try { + trace.trace(PONG_RECEIVED); + counter.set(0); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * This is used to close the session and send a 1011 close code + * to the client indicating an internal server error. Closing + * of the session in this manner only occurs if there is an + * expiry of the session or an I/O error, both of which are + * unexpected and violate the behaviour as defined in RFC 6455. + */ + public void failure() { + try { + connection.close(error); + channel.close(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * This is used to close the session and send a 1000 close code + * to the client indicating a normal closure. This will be called + * when there is a close notification dispatched to the status + * listener. Typically here a graceful closure is best. + */ + public void close() { + try { + connection.close(normal); + channel.close(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } +} \ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java new file mode 100644 index 0000000..2b2a049 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java @@ -0,0 +1,93 @@ +/* + * StatusResultListener.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; + +/** + * The StatusResultListener is used to listen for responses + * to ping frames sent out by the server. A response to the ping frame + * is a pong frame. When a pong is received it allows the session to + * be scheduled to receive another ping. + * + * @author Niall Gallagher + */ +class StatusResultListener implements FrameListener { + + /** + * This is used to ping sessions to check for health. + */ + private final StatusChecker checker; + + /** + * Constructor for the StatusResultListener object. + * This requires the session health checker that performs the pings + * so that it can reschedule the session for multiple pings if + * the connection responds with a pong. + * + * @param checker this is the session health checker + */ + public StatusResultListener(StatusChecker checker) { + this.checker = checker; + } + + /** + * This is called when a new frame arrives on the WebSocket. If + * the frame is a pong then this will reschedule the the session + * to receive another ping frame. + * + * @param session this is the associated session + * @param frame this is the frame that has been received + */ + public void onFrame(Session session, Frame frame) { + FrameType type = frame.getType(); + + if(type.isPong()) { + checker.refresh(); + } + } + + /** + * This is called when there is an error with the connection. + * When called the session is removed from the checker and no + * more ping frames are sent. + * + * @param session this is the associated session + * @param cause this is the cause of the error + */ + public void onError(Session session, Exception cause) { + checker.failure(); + } + + /** + * This is called when the connection is closed from the other + * side. When called the session is removed from the checker + * and no more ping frames are sent. + * + * @param session this is the associated session + * @param reason this is the reason the connection was closed + */ + public void onClose(Session session, Reason reason) { + checker.close(); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/ConnectionTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/ConnectionTest.java new file mode 100644 index 0000000..9047de5 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/ConnectionTest.java @@ -0,0 +1,267 @@ +package org.simpleframework.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URL; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.CountDownLatch; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.FileAllocator; +import org.simpleframework.common.thread.ConcurrentExecutor; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.Container; +import org.simpleframework.http.core.ContainerTransportProcessor; +import org.simpleframework.http.core.ThreadDumper; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.TransportSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.Socket; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; + +public class ConnectionTest extends TestCase { + + private static final int PING_TEST_PORT = 12366; + + public void testSocketPing() throws Exception { + // for(int i = 0; i < 10; i++) { + // System.err.printf("Ping [%s]%n", i); + // testPing(PING_TEST_PORT, "Hello World!", true, 2); + // } + } + + public void testURLPing() throws Exception { + for(int i = 0; i < 20; i++) { + System.err.printf("Ping [%s]%n", i); + testPing(PING_TEST_PORT, "Hello World!", false, 10); + } + } + + public void testMixPing() throws Exception { + //for(int i = 0; i < 50; i+=2) { + // System.err.printf("Ping [%s]%n", i); + // testPing(PING_TEST_PORT, "Hello World!", true, 2); + // System.err.printf("Ping [%s]%n", i+1); + // testPing(PING_TEST_PORT, "Hello World!", false, 10); + //} + } + + private void testPing(int port, String message, boolean socket, int count) throws Exception { + PingServer server = new PingServer(PING_TEST_PORT, message); + Pinger pinger = new Pinger(PING_TEST_PORT, socket, count); + + server.start(); + List list = pinger.execute(); + + for(int i = 0; i < count; i++) { // at least 20 + String result = list.get(i); + + assertNotNull(result); + assertEquals(result, message); + } + server.stop(); + pinger.validate(); + pinger.stop(); // wait for it all to finish + } + + private static class DebugServer implements SocketProcessor { + + private SocketProcessor server; + + public DebugServer(SocketProcessor server) { + this.server = server; + } + + public void process(Socket socket) throws IOException { + System.err.println("Connect..."); + server.process(socket); + } + + public void stop() throws IOException { + System.err.println("Stop..."); + server.stop(); + } + } + + private static class PingServer implements Container { + + private final Connection connection; + private final SocketAddress address; + private final String message; + + public PingServer(int port, String message) throws Exception { + Allocator allocator = new FileAllocator(); + TransportProcessor processor = new ContainerTransportProcessor(this, allocator, 5); + SocketProcessor server = new TransportSocketProcessor(processor); + DebugServer debug = new DebugServer(server); + + this.connection = new SocketConnection(debug); + this.address = new InetSocketAddress(port); + this.message = message; + } + + public void start() throws Exception { + try { + System.err.println("Starting..."); + connection.connect(address); + }finally { + System.err.println("Started..."); + } + } + + public void stop() throws Exception { + connection.close(); + } + + public void handle(Request req, Response resp) { + try { + System.err.println(req); + PrintStream out = resp.getPrintStream(1024); + + resp.setValue("Content-Type", "text/plain"); + out.print(message); + out.close(); + }catch(Exception e) { + e.printStackTrace(); + } + } + } + + private static class Pinger implements Runnable { + + private final int count; + private final int port; + private final boolean socket; + private final CountDownLatch latch; + private final CountDownLatch stop; + private final ConcurrentExecutor executor; + private final ThreadDumper dumper; + private final List list; + private final List sockets; + + public Pinger(int port, boolean socket, int count) throws Exception { + this.executor = new ConcurrentExecutor(Pinger.class, count); + this.list = new Vector(count); + this.sockets = new Vector(count); + this.latch = new CountDownLatch(count); + this.stop = new CountDownLatch(count + count); + this.dumper = new ThreadDumper(); + this.port = port; + this.socket = socket; + this.count = count; + } + + public List execute() throws Exception { + dumper.start(); + + for(int i = 0; i < count; i++) { + executor.execute(this); + } + latch.await(); + + // Overrun with pings to ensure they close + if(socket) { + for(int i = 0; i < count; i++) { + executor.execute(this); + } + } + return list; + } + + public void validate() throws Exception { + if(socket) { + for(java.net.Socket socket : sockets) { + if(socket.getInputStream().read() != -1) { + throw new IOException("Connection not closed"); + } else { + System.err.println("Socket is closed"); + } + } + } + } + + public void stop() throws Exception { + executor.stop(); + + if(socket) { + stop.await(); // wait for all excess pings to finish + } + dumper.kill(); + } + + private String ping() throws Exception { + if(socket) { + return pingWithSocket(); + } + return pingWithURL(); + } + + public void run() { + try { + String result = ping(); + + list.add(result); + latch.countDown(); + }catch(Throwable e){ + System.err.println(e); + } finally { + stop.countDown(); // account for excess pings + } + } + + /** + * This works as it opens a socket and sends the request. + * This will split using the CRLF and CRLF ending. + * + * @return the response body + * + * @throws Exception if the socket can not connect + */ + private String pingWithSocket() throws Exception { + java.net.Socket socket = new java.net.Socket("localhost", port); + OutputStream out = socket.getOutputStream(); + out.write( + ("GET / HTTP/1.1\r\n" + + "Host: localhost\r\n"+ + "\r\n").getBytes()); + out.flush(); + InputStream in = socket.getInputStream(); + byte[] block = new byte[1024]; + int count = in.read(block); + String result = new String(block, 0, count); + String parts[] = result.split("\r\n\r\n"); + + if(!result.startsWith("HTTP")) { + throw new IOException("Header is not valid"); + } + sockets.add(socket); + return parts[1]; + } + + /** + * Use the standard URL tool to get the content. + * + * @return the response body + * + * @throws Exception if a connection can not be made. + */ + private String pingWithURL() throws Exception { + URL target = new URL("http://localhost:"+ port+"/"); + InputStream in = target.openStream(); + byte[] block = new byte[1024]; + int count = in.read(block); + String result = new String(block, 0, count); + + return result; + } + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/CookieTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/CookieTest.java new file mode 100644 index 0000000..9fd4441 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/CookieTest.java @@ -0,0 +1,63 @@ +package org.simpleframework.http; + +import junit.framework.TestCase; + +public class CookieTest extends TestCase { + + public void testCookies() throws Exception { + Cookie cookie = new Cookie("JSESSIONID", "XXX"); + + cookie.setExpiry(10); + cookie.setPath("/path"); + + System.err.println(cookie); + + assertTrue(cookie.toString().contains("max-age=10")); + assertTrue(cookie.toString().matches(".*expires=\\w\\w\\w, \\d\\d-\\w\\w\\w-\\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d GMT;.*")); + } + + public void testCookieWithoutExpiry() throws Exception { + Cookie cookie = new Cookie("JSESSIONID", "XXX"); + + cookie.setPath("/path"); + + System.err.println(cookie); + + assertFalse(cookie.toString().contains("max-age=10")); + assertFalse(cookie.toString().matches(".*expires=\\w\\w\\w, \\d\\d \\w\\w\\w \\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d GMT;.*")); + } + + public void testSecureCookies() throws Exception { + Cookie cookie = new Cookie("JSESSIONID", "XXX"); + + cookie.setExpiry(10); + cookie.setPath("/path"); + cookie.setSecure(true); + + System.err.println(cookie); + + assertTrue(cookie.toString().contains("max-age=10")); + assertTrue(cookie.toString().matches(".*expires=\\w\\w\\w, \\d\\d-\\w\\w\\w-\\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d GMT;.*")); + + cookie.setExpiry(10); + cookie.setPath("/path"); + cookie.setSecure(false); + cookie.setProtected(true); + + System.err.println(cookie); + + assertTrue(cookie.toString().contains("max-age=10")); + assertTrue(cookie.toString().matches(".*expires=\\w\\w\\w, \\d\\d-\\w\\w\\w-\\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d GMT;.*")); + + cookie.setExpiry(10); + cookie.setPath("/path"); + cookie.setSecure(true); + cookie.setProtected(true); + + System.err.println(cookie); + + assertTrue(cookie.toString().contains("max-age=10")); + assertTrue(cookie.toString().matches(".*expires=\\w\\w\\w, \\d\\d-\\w\\w\\w-\\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d GMT;.*")); + + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/Debug.java b/simple/simple-http/src/test/java/org/simpleframework/http/Debug.java new file mode 100644 index 0000000..b916494 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/Debug.java @@ -0,0 +1,11 @@ +package org.simpleframework.http; + +public class Debug { + public void log(String text, Object... list) { + System.out.printf(text, list); + } + + public void logln(String text, Object... list) { + System.out.printf(text + "%n", list); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/MockRenegotiationServer.java b/simple/simple-http/src/test/java/org/simpleframework/http/MockRenegotiationServer.java new file mode 100644 index 0000000..76bfb6e --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/MockRenegotiationServer.java @@ -0,0 +1,434 @@ +package org.simpleframework.http; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SocketChannel; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.SocketFactory; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509TrustManager; +import javax.security.cert.X509Certificate; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.FileAllocator; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.Client.AnonymousTrustManager; +import org.simpleframework.http.core.Container; +import org.simpleframework.http.core.ContainerTransportProcessor; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.CertificateChallenge; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.TransportSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.Socket; +import org.simpleframework.transport.Transport; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; +import org.simpleframework.transport.trace.TraceAnalyzer; +import org.simpleframework.transport.trace.Trace; + +public class MockRenegotiationServer implements Container { + + private final ConfigurableCertificateServer server; + private final Connection connection; + private final SocketAddress address; + private final SSLContext context; + private final TraceAnalyzer agent; + + public static void main(String[] list) throws Exception { + System.err.println("Starting renegotiation test....."); + //System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true"); + //System.setProperty("sun.security.ssl.allowLegacyHelloMessages", "true"); + //File file = new File("C:\\work\\development\\async_http\\yieldbroker-proxy-site\\etc\\www.yieldbroker.com.pfx"); + File file = new File("/Users/niallg/Work/development/yieldbroker/proxy/yieldbroker-proxy-site/certificate/www.yieldbroker.com.pfx"); + //File file = new File("C:\\work\\development\\async_http\\yieldbroker-proxy-trading\\etc\\uat.yieldbroker.com.pfx"); + KeyStoreReader reader = new KeyStoreReader(KeyStoreType.PKCS12, file, "p", "p"); + SecureSocketContext context = new SecureSocketContext(reader, SecureProtocol.TLS); + SSLContext sslContext = context.getContext(); + MockRenegotiationServer server = new MockRenegotiationServer(sslContext, false, 10001); + server.start(); + } + + public MockRenegotiationServer(SSLContext context, boolean certRequired, int port) throws IOException { + Allocator allocator = new FileAllocator(); + ContainerTransportProcessor processor = new ContainerTransportProcessor(this, allocator, 4); + TransportGrabber grabber = new TransportGrabber(processor); + TransportSocketProcessor processorServer = new TransportSocketProcessor(grabber); + + this.server = new ConfigurableCertificateServer(processorServer, certRequired); + this.agent = new ConsoleAgent(); + this.connection = new SocketConnection(server, agent); + this.address = new InetSocketAddress(port); + this.context = context; + } + + public void handle(final Request req, final Response resp) { + boolean challengeForCertificate = false; + + try { + final PrintStream out = resp.getPrintStream(); + String normal = req.getPath().getPath(); + + if(normal.indexOf(".ico") == -1) { + SSLEngine engine = (SSLEngine)req.getAttribute(SSLEngine.class); + if(normal.startsWith("/niall/cert")) { + SocketChannel channel = ((Transport)req.getAttribute(Transport.class)).getChannel(); + System.err.println("NEW REQUEST ("+System.identityHashCode(engine)+"): " + req); + + + try { + resp.setContentType("text/plain"); + resp.setValue("Connection", "keep-alive"); + String certificateInfo = null; + + + try { + X509Certificate[] list = req.getClientCertificate().getChain(); + StringBuilder builder = new StringBuilder(); + for(X509Certificate cert : list) { + X509Certificate x509 = (X509Certificate)cert; + builder.append(x509); + } + certificateInfo = builder.toString(); + } catch(Exception e) { + e.printStackTrace(); + certificateInfo = e.getMessage(); + challengeForCertificate = true; + + // http://stackoverflow.com/questions/14281628/ssl-renegotiation-with-client-certificate-causes-server-buffer-overflow + // Perhaps an expect 100 continue does something here????? + if(challengeForCertificate) { + Certificate certificate = req.getClientCertificate(); + CertificateChallenge challenge = certificate.getChallenge(); + + Future future = challenge.challenge(new Runnable() { + public void run() { + System.err.println("FINISHED THE CHALLENGE!!!"); + } + }); + Certificate futureCert = future.get(10, TimeUnit.SECONDS); + + if(futureCert == null) { + System.err.println("FAILED TO GET CERT!!!!"); + } else { + System.err.println("**** GOT THE CERT"); + } + + String text= "Challenge finished without cert"; + try { + X509Certificate[] list = req.getClientCertificate().getChain(); + StringBuilder builder = new StringBuilder(); + for(X509Certificate x509 : list) { + builder.append(x509); + } + text = builder.toString(); + } catch(Exception ex) { + e.printStackTrace(); + } + out.print(text); + out.flush(); + try { + resp.close(); + } catch(Exception ex){ + e.printStackTrace(); + } + } + } + // Thread.sleep(10000); + if(!challengeForCertificate) { + try { + X509Certificate[] list = req.getClientCertificate().getChain(); + StringBuilder builder = new StringBuilder(); + for(X509Certificate cert : list) { + X509Certificate x509 = (X509Certificate)cert; + builder.append(x509); + } + certificateInfo = builder.toString(); + } catch(Exception e) { + e.printStackTrace(); + } + out.print(certificateInfo); + out.flush(); + resp.close(); + } + + + } finally { + if(!challengeForCertificate) { + System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!("+System.identityHashCode(engine)+")!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WORKING"); + } + } + } else { + resp.setStatus(org.simpleframework.http.Status.NOT_FOUND); + resp.setValue("Connection", "keep-alive"); + resp.setValue("Content-Type", "text/plain"); + out.println("Ok normal request with NO renegotiation " + req); + } + } else { + resp.setStatus(org.simpleframework.http.Status.NOT_FOUND); + resp.setValue("Connection", "keep-alive"); + resp.setValue("Content-Type", "text/plain"); + out.println("fuck off!!"); + } + } catch(Exception e) { + e.printStackTrace(); + } finally { + try { + if(!challengeForCertificate) { + resp.close(); + } else { + System.err.println("NOT DOING ANYTHING WITH THE REQUEST, AS A CHALLENGE IS UNDERWAY challengeForCertificate="+challengeForCertificate+" path="+req); + } + } catch(Exception ex) { + ex.printStackTrace(); + } + + } + } + + public void start() throws IOException { + connection.connect(address, context); + } + + public void stop() throws IOException { + connection.close(); + } + + private static class ConsoleAgent implements TraceAnalyzer { + + private final Map map; + private final AtomicInteger count; + + public ConsoleAgent() { + this.map = new ConcurrentHashMap(); + this.count = new AtomicInteger(); + } + + public Trace attach(SelectableChannel channel) { + if(map.containsKey(channel)) { + throw new IllegalStateException("Can't attach twice"); + } + final int counter = count.getAndIncrement(); + map.put(channel, counter); + + return new Trace() { + + public void trace(Object event) { + trace(event, ""); + } + + public void trace(Object event, Object value) { + if(value instanceof Throwable) { + StringWriter writer = new StringWriter(); + PrintWriter out = new PrintWriter(writer); + ((Exception)value).printStackTrace(out); + out.flush(); + value = writer.toString(); + } + if(value != null && !String.valueOf(value).isEmpty()) { + System.err.printf("(%s) [%s] %s: %s%n", Thread.currentThread().getName(), counter, event, value); + } else { + System.err.printf("(%s) [%s] %s%n", Thread.currentThread().getName(), counter, event); + } + } + }; + } + + public void stop() { + System.err.println("Stop agent"); + } + } + + public static class TransportGrabber implements TransportProcessor { + + private TransportProcessor processor; + + public TransportGrabber(TransportProcessor processor) { + this.processor = processor; + } + + public void process(Transport transport) throws IOException { + transport.getAttributes().put(Transport.class, transport); + processor.process(transport); + + } + + public void stop() throws IOException { + processor.stop(); + } + + } + + public static class ConfigurableCertificateServer implements SocketProcessor { + + private SocketProcessor server; + private boolean certRequired; + + public ConfigurableCertificateServer(SocketProcessor server) { + this(server, false); + } + + public ConfigurableCertificateServer(SocketProcessor server, boolean certRequired) { + this.certRequired = certRequired; + this.server = server; + } + + public void setCertRequired(boolean certRequired) { + this.certRequired = certRequired; + } + + public void process(Socket socket) throws IOException { + SSLEngine engine = socket.getEngine(); + socket.getAttributes().put(SSLEngine.class, engine); + if(certRequired) { + socket.getEngine().setNeedClientAuth(true); + } + server.process(socket); + } + + public void stop() throws IOException { + System.err.println("stop"); + } + } + + + public enum KeyStoreType { + JKS("JKS", "SunX509"), + PKCS12("PKCS12", "SunX509"); + + private final String algorithm; + private final String type; + + private KeyStoreType(String type, String algorithm) { + this.algorithm = algorithm; + this.type = type; + } + + public String getType() { + return type; + } + + public KeyStore getKeyStore() throws KeyStoreException { + return KeyStore.getInstance(type); + } + + public KeyManagerFactory getKeyManagerFactory() throws NoSuchAlgorithmException { + return KeyManagerFactory.getInstance(algorithm); + } + } + + private static class KeyStoreManager { + + private final KeyStoreType keyStoreType; + + public KeyStoreManager(KeyStoreType keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public KeyManager[] getKeyManagers(InputStream keyStoreSource, String keyStorePassword, String keyManagerPassword) throws Exception { + KeyStore keyStore = keyStoreType.getKeyStore(); + KeyManagerFactory keyManagerFactory = keyStoreType.getKeyManagerFactory(); + + keyStore.load(keyStoreSource, keyManagerPassword.toCharArray()); + keyManagerFactory.init(keyStore, keyManagerPassword.toCharArray()); + + return keyManagerFactory.getKeyManagers(); + } + + } + + private static class KeyStoreReader { + + private final KeyStoreManager keyStoreManager; + private final String keyManagerPassword; + private final String keyStorePassword; + private final File keyStore; + + public KeyStoreReader(KeyStoreType keyStoreType, File keyStore, String keyStorePassword, String keyManagerPassword) { + this.keyStoreManager = new KeyStoreManager(keyStoreType); + this.keyManagerPassword = keyManagerPassword; + this.keyStorePassword = keyStorePassword; + this.keyStore = keyStore; + } + + public KeyManager[] getKeyManagers() throws Exception { + InputStream storeSource = new FileInputStream(keyStore); + + try { + return keyStoreManager.getKeyManagers(storeSource, keyStorePassword, keyManagerPassword); + } finally { + storeSource.close(); + } + } + } + + public enum SecureProtocol { + DEFAULT("Default"), + SSL("SSL"), + TLS("TLS"); + + private final String protocol; + + private SecureProtocol(String protocol) { + this.protocol = protocol; + } + + public SSLContext getContext() throws NoSuchAlgorithmException { + return SSLContext.getInstance(protocol); + } + } + + private static class SecureSocketContext { + + private final X509TrustManager trustManager; + private final X509TrustManager[] trustManagers; + private final KeyStoreReader keyStoreReader; + private final SecureProtocol secureProtocol; + + public SecureSocketContext(KeyStoreReader keyStoreReader, SecureProtocol secureProtocol) { + this.trustManager = new AnonymousTrustManager(); + this.trustManagers = new X509TrustManager[]{trustManager}; + this.keyStoreReader = keyStoreReader; + this.secureProtocol = secureProtocol; + } + + public SSLContext getContext() throws Exception { + KeyManager[] keyManagers = keyStoreReader.getKeyManagers(); + SSLContext secureContext = secureProtocol.getContext(); + + secureContext.init(keyManagers, trustManagers, null); + + return secureContext; + } + + public SocketFactory getSocketFactory() throws Exception { + KeyManager[] keyManagers = keyStoreReader.getKeyManagers(); + SSLContext secureContext = secureProtocol.getContext(); + + secureContext.init(keyManagers, trustManagers, null); + + return secureContext.getSocketFactory(); + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/MockSocket.java b/simple/simple-http/src/test/java/org/simpleframework/http/MockSocket.java new file mode 100644 index 0000000..be11f59 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/MockSocket.java @@ -0,0 +1,45 @@ + +package org.simpleframework.http; + +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.SSLEngine; + +import org.simpleframework.transport.Socket; +import org.simpleframework.transport.trace.Trace; + +public class MockSocket implements Socket { + + private SocketChannel socket; + private SSLEngine engine; + private Map map; + + public MockSocket(SocketChannel socket) { + this(socket, null); + } + + public MockSocket(SocketChannel socket, SSLEngine engine) { + this.map = new HashMap(); + this.engine = engine; + this.socket = socket; + } + + public SSLEngine getEngine() { + return engine; + } + + public SocketChannel getChannel() { + return socket; + } + + public Map getAttributes() { + return map; + } + + public Trace getTrace() { + return new MockTrace(); + } +} + diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/MockTrace.java b/simple/simple-http/src/test/java/org/simpleframework/http/MockTrace.java new file mode 100644 index 0000000..2f22a0a --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/MockTrace.java @@ -0,0 +1,8 @@ +package org.simpleframework.http; + +import org.simpleframework.transport.trace.Trace; + +public class MockTrace implements Trace{ + public void trace(Object event) {} + public void trace(Object event, Object value) {} +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/RenegotiationExample.java b/simple/simple-http/src/test/java/org/simpleframework/http/RenegotiationExample.java new file mode 100644 index 0000000..0ee0e7d --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/RenegotiationExample.java @@ -0,0 +1,351 @@ +package org.simpleframework.http; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.channels.SelectableChannel; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import javax.net.SocketFactory; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509TrustManager; + +import org.simpleframework.http.core.Client.AnonymousTrustManager; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.TransportSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.Socket; +import org.simpleframework.transport.Transport; +import org.simpleframework.transport.TransportCursor; +import org.simpleframework.transport.TransportWriter; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; +import org.simpleframework.transport.trace.TraceAnalyzer; +import org.simpleframework.transport.trace.Trace; + +public class RenegotiationExample { + + public static void main(String[] list) throws Exception { + Connection serverCon = createServer(false, 443); + /*SSLSocket socketCon = createClient(); + OutputStream out = socketCon.getOutputStream(); + + for(int i = 0; i < 1000; i++) { + out.write("TEST".getBytes()); + out.flush(); + Thread.sleep(5000); + }*/ + Thread.sleep(1000000); + serverCon.close(); + } + + public static Connection createServer(boolean certificateRequired, int listenPort) throws Exception { + System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true"); + System.setProperty("sun.security.ssl.allowLegacyHelloMessages", "true"); + File file = new File("C:\\work\\development\\async_http\\yieldbroker-proxy-trading\\etc\\uat.yieldbroker.com.pfx"); + KeyStoreReader reader = new KeyStoreReader(KeyStoreType.PKCS12, file, "p", "p"); + SecureSocketContext context = new SecureSocketContext(reader, SecureProtocol.TLS); + SSLContext sslContext = context.getContext(); + TraceAnalyzer agent = new MockAgent(); + TransportProcessor processor = new MockTransportProcessor(); + TransportSocketProcessor server = new TransportSocketProcessor(processor); + ConfigurableCertificateServer certServer = new ConfigurableCertificateServer(server); + SocketConnection con = new SocketConnection(certServer, agent); + SocketAddress serverAddress = new InetSocketAddress(listenPort); + + certServer.setCertRequired(certificateRequired); + con.connect(serverAddress, sslContext); + + return con; + } + + + public static SSLSocket createClient() throws Exception { + System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true"); + System.setProperty("sun.security.ssl.allowLegacyHelloMessages", "true"); + File file = new File("C:\\work\\development\\async_http\\yieldbroker-proxy-benchmark\\etc\\niall.pfx"); + KeyStoreReader reader = new KeyStoreReader(KeyStoreType.PKCS12, file, "1234", "1234"); + SecureSocketContext context = new SecureSocketContext(reader, SecureProtocol.TLS); + SocketFactory factory = context.getSocketFactory(); + SSLSocket socket = (SSLSocket)factory.createSocket("localhost", 9333); + socket.setEnabledProtocols(new String[] {"SSLv3", "TLSv1"}); + + return socket; + } + + public static class ConfigurableCertificateServer implements SocketProcessor { + + private SocketProcessor server; + private boolean certRequired; + + public ConfigurableCertificateServer(SocketProcessor server) { + this.server = server; + } + + public void setCertRequired(boolean certRequired) { + this.certRequired = certRequired; + } + + public void process(Socket socket) throws IOException { + if(certRequired) { + socket.getEngine().setNeedClientAuth(true); + } + server.process(socket); + } + + public void stop() throws IOException { + System.err.println("stop"); + } + } + + public static class TransportPoller extends Thread { + + private final ByteCursor cursor; + private final Transport transport; + + + public TransportPoller(Transport transport) { + this.cursor = new TransportCursor(transport); + this.transport = transport; + } + + public void run() { + try { + System.err.println("Poller started"); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] chunk = new byte[1024]; + int count = 0; + + while(cursor.isOpen()) { + while(cursor.isReady()) { + count = cursor.read(chunk); + if(count != 0) { + out.write(chunk, 0, count); + } + } + String message = out.toString(); + out.reset(); + if(message != null && !message.isEmpty()) { + SSLEngine engine = transport.getEngine(); + String certificateInfo = null; + + if(engine != null) { + try { + Certificate[] list = engine.getSession().getPeerCertificates(); + StringBuilder builder = new StringBuilder(); + for(Certificate cert : list) { + X509Certificate x509 = (X509Certificate)cert; + builder.append(x509); + } + certificateInfo = builder.toString(); + } catch(Exception e) { + + // Here we would have to ask the transport to renegotiate.....!!! + transport.getEngine().setWantClientAuth(true); + transport.getEngine().beginHandshake(); + transport.getEngine().setWantClientAuth(true); + for(int i = 0; i < 100; i++) { + Runnable task = transport.getEngine().getDelegatedTask(); + if(task != null){ + task.run(); + } + } + certificateInfo = e.getMessage(); + } + } + TransportWriter sender = new TransportWriter(transport); + sender.write( + ("HTTP/1.1 200 OK\r\n" + + "Connection: keep-alive\r\n"+ + "Content-Length: 5\r\n"+ + "Content-Type: text/plain\r\n"+ + "\r\n"+ + "hello").getBytes()); + + + sender.flush(); + + + + + System.err.println("["+message+"]: " + certificateInfo); + } + Thread.sleep(5000); + } + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + + + public static class MockTransportProcessor implements TransportProcessor { + + public void process(Transport transport) throws IOException { + System.err.println("New transport"); + TransportPoller poller = new TransportPoller(transport); + poller.start(); + } + + public void stop() throws IOException { + System.err.println("Transport stopped"); + } + } + + private static class MockAgent implements TraceAnalyzer { + + public Trace attach(SelectableChannel channel) { + return new Trace() { + public void trace(Object event) { + trace(event, ""); + } + public void trace(Object event, Object value) { + if(value != null && !String.valueOf(value).isEmpty()) { + System.err.printf("%s: %s%n", event, value); + } else { + System.err.println(event); + } + } + }; + } + + public void stop() { + System.err.println("Stop agent"); + } + + } + + public enum KeyStoreType { + JKS("JKS", "SunX509"), + PKCS12("PKCS12", "SunX509"); + + private final String algorithm; + private final String type; + + private KeyStoreType(String type, String algorithm) { + this.algorithm = algorithm; + this.type = type; + } + + public String getType() { + return type; + } + + public KeyStore getKeyStore() throws KeyStoreException { + return KeyStore.getInstance(type); + } + + public KeyManagerFactory getKeyManagerFactory() throws NoSuchAlgorithmException { + return KeyManagerFactory.getInstance(algorithm); + } + } + + private static class KeyStoreManager { + + private final KeyStoreType keyStoreType; + + public KeyStoreManager(KeyStoreType keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public KeyManager[] getKeyManagers(InputStream keyStoreSource, String keyStorePassword, String keyManagerPassword) throws Exception { + KeyStore keyStore = keyStoreType.getKeyStore(); + KeyManagerFactory keyManagerFactory = keyStoreType.getKeyManagerFactory(); + + keyStore.load(keyStoreSource, keyManagerPassword.toCharArray()); + keyManagerFactory.init(keyStore, keyManagerPassword.toCharArray()); + + return keyManagerFactory.getKeyManagers(); + } + + } + + private static class KeyStoreReader { + + private final KeyStoreManager keyStoreManager; + private final String keyManagerPassword; + private final String keyStorePassword; + private final File keyStore; + + public KeyStoreReader(KeyStoreType keyStoreType, File keyStore, String keyStorePassword, String keyManagerPassword) { + this.keyStoreManager = new KeyStoreManager(keyStoreType); + this.keyManagerPassword = keyManagerPassword; + this.keyStorePassword = keyStorePassword; + this.keyStore = keyStore; + } + + public KeyManager[] getKeyManagers() throws Exception { + InputStream storeSource = new FileInputStream(keyStore); + + try { + return keyStoreManager.getKeyManagers(storeSource, keyStorePassword, keyManagerPassword); + } finally { + storeSource.close(); + } + } + } + + public enum SecureProtocol { + DEFAULT("Default"), + SSL("SSL"), + TLS("TLS"); + + private final String protocol; + + private SecureProtocol(String protocol) { + this.protocol = protocol; + } + + public SSLContext getContext() throws NoSuchAlgorithmException { + return SSLContext.getInstance(protocol); + } + } + + private static class SecureSocketContext { + + private final X509TrustManager trustManager; + private final X509TrustManager[] trustManagers; + private final KeyStoreReader keyStoreReader; + private final SecureProtocol secureProtocol; + + public SecureSocketContext(KeyStoreReader keyStoreReader, SecureProtocol secureProtocol) { + this.trustManager = new AnonymousTrustManager(); + this.trustManagers = new X509TrustManager[]{trustManager}; + this.keyStoreReader = keyStoreReader; + this.secureProtocol = secureProtocol; + } + + public SSLContext getContext() throws Exception { + KeyManager[] keyManagers = keyStoreReader.getKeyManagers(); + SSLContext secureContext = secureProtocol.getContext(); + + secureContext.init(keyManagers, trustManagers, null); + + return secureContext; + } + + public SocketFactory getSocketFactory() throws Exception { + KeyManager[] keyManagers = keyStoreReader.getKeyManagers(); + SSLContext secureContext = secureProtocol.getContext(); + + secureContext.init(keyManagers, trustManagers, null); + + return secureContext.getSocketFactory(); + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/StatusTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/StatusTest.java new file mode 100644 index 0000000..6bb8ef5 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/StatusTest.java @@ -0,0 +1,20 @@ +package org.simpleframework.http; + +import junit.framework.TestCase; + +public class StatusTest extends TestCase { + + private static final int ITERATIONS = 100000; + + public void testStatus() { + testStatus(200, "OK"); + testStatus(404, "Not Found"); + } + + public void testStatus(int code, String expect) { + for(int i = 0; i < ITERATIONS; i++) { + assertEquals(expect, Status.getDescription(code)); + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/StreamTransport.java b/simple/simple-http/src/test/java/org/simpleframework/http/StreamTransport.java new file mode 100644 index 0000000..9ca5fe7 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/StreamTransport.java @@ -0,0 +1,67 @@ +package org.simpleframework.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import java.util.Map; + +import javax.net.ssl.SSLEngine; + +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Transport; +import org.simpleframework.transport.trace.Trace; + +public class StreamTransport implements Transport { + + private final WritableByteChannel write; + private final ReadableByteChannel read; + private final OutputStream out; + + public StreamTransport(InputStream in, OutputStream out) { + this.write = Channels.newChannel(out); + this.read = Channels.newChannel(in); + this.out = out; + } + + public void close() throws IOException { + write.close(); + read.close(); + } + + public void flush() throws IOException { + out.flush(); + } + + public int read(ByteBuffer buffer) throws IOException { + return read.read(buffer); + } + + public void write(ByteBuffer buffer) throws IOException { + write.write(buffer); + } + + public Map getAttributes() { + return null; + } + + public SocketChannel getChannel() { + return null; + } + + public SSLEngine getEngine() { + return null; + } + + public Certificate getCertificate() { + return null; + } + + public Trace getTrace() { + return new MockTrace(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/AccumulatorTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/AccumulatorTest.java new file mode 100644 index 0000000..70db4a3 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/AccumulatorTest.java @@ -0,0 +1,99 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import junit.framework.TestCase; + +public class AccumulatorTest extends TestCase { + + public void testAccumulator() throws IOException { + MockChannel channel = new MockChannel(null); + MockObserver monitor = new MockObserver(); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + Conversation support = new Conversation(request, response); + ResponseBuffer buffer = new ResponseBuffer(monitor, response, support, channel); + + byte[] content = { 'T', 'E', 'S', 'T' }; + + // Start a HTTP/1.1 conversation + request.setMajor(1); + request.setMinor(1); + + // Write to a zero capacity buffer + buffer.expand(0); + buffer.write(content, 0, content.length); + + assertEquals(response.getValue("Connection"), "keep-alive"); + assertEquals(response.getValue("Transfer-Encoding"), "chunked"); + assertEquals(response.getValue("Content-Length"), null); + assertEquals(response.getContentLength(), -1); + assertTrue(response.isCommitted()); + + channel = new MockChannel(null); + monitor = new MockObserver(); + request = new MockRequest(); + response = new MockResponse(); + support = new Conversation(request, response); + buffer = new ResponseBuffer(monitor, response, support, channel); + + // Start a HTTP/1.0 conversation + request.setMajor(1); + request.setMinor(0); + + // Write to a zero capacity buffer + buffer.expand(0); + buffer.write(content, 0, content.length); + + assertEquals(response.getValue("Connection"), "close"); + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getValue("Content-Length"), null); + assertEquals(response.getContentLength(), -1); + assertTrue(response.isCommitted()); + + channel = new MockChannel(null); + monitor = new MockObserver(); + request = new MockRequest(); + response = new MockResponse(); + support = new Conversation(request, response); + buffer = new ResponseBuffer(monitor, response, support, channel); + + // Start a HTTP/1.1 conversation + request.setMajor(1); + request.setMinor(1); + + // Write to a large capacity buffer + buffer.expand(1024); + buffer.write(content, 0, content.length); + + assertEquals(response.getValue("Connection"), null); + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getValue("Content-Length"), null); + assertEquals(response.getContentLength(), -1); + assertFalse(response.isCommitted()); + assertFalse(monitor.isReady()); + assertFalse(monitor.isClose()); + assertFalse(monitor.isError()); + + // Flush the buffer + buffer.close(); + + assertEquals(response.getValue("Connection"), "keep-alive"); + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getValue("Content-Length"), "4"); + assertEquals(response.getContentLength(), 4); + assertTrue(response.isCommitted()); + assertTrue(monitor.isReady()); + assertFalse(monitor.isClose()); + assertFalse(monitor.isError()); + + boolean catchOverflow = false; + + try { + buffer.write(content, 0, content.length); + } catch(Exception e) { + catchOverflow = true; + } + assertTrue(catchOverflow); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/ChunkedProducerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/ChunkedProducerTest.java new file mode 100644 index 0000000..9f7a6bb --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/ChunkedProducerTest.java @@ -0,0 +1,43 @@ +package org.simpleframework.http.core; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.message.ChunkedConsumer; +import org.simpleframework.transport.ByteCursor; + +public class ChunkedProducerTest extends TestCase { + + public void testChunk() throws Exception { + testChunk(1024, 1); + testChunk(1024, 2); + testChunk(512, 20); + testChunk(64, 64); + } + + public void testChunk(int chunkSize, int count) throws Exception { + MockSender sender = new MockSender((chunkSize * count) + 1024); + MockObserver monitor = new MockObserver(); + ChunkedConsumer validator = new ChunkedConsumer(new ArrayAllocator()); + ChunkedEncoder producer = new ChunkedEncoder(monitor, sender); + byte[] chunk = new byte[chunkSize]; + + for(int i = 0; i < chunk.length; i++) { + chunk[i] = (byte)String.valueOf(i).charAt(0); + } + for(int i = 0; i < count; i++) { + producer.encode(chunk, 0, chunkSize); + } + producer.close(); + + System.err.println(sender.getBuffer().encode("UTF-8")); + + ByteCursor cursor = sender.getCursor(); + + while(!validator.isFinished()) { + validator.consume(cursor); + } + assertEquals(cursor.ready(), -1); + assertTrue(monitor.isReady()); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/Chunker.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/Chunker.java new file mode 100644 index 0000000..8f5ef13 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/Chunker.java @@ -0,0 +1,52 @@ + +package org.simpleframework.http.core; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class Chunker extends FilterOutputStream { + + + private byte[] size = {'0', '0', '0', '0', '0', + '0', '0', '0', 13, 10}; + + + private byte[] index = {'0', '1', '2', '3', '4', '5','6', '7', + '8', '9', 'a', 'b', 'c', 'd','e', 'f'}; + + + private byte[] zero = {'0', 13, 10, 13, 10}; + + + public Chunker(OutputStream out){ + super(out); + } + + public void write(int octet) throws IOException { + byte[] swap = new byte[1]; + swap[0] = (byte)octet; + write(swap); + } + + + public void write(byte[] buf, int off, int len) throws IOException { + int pos = 7; + + if(len > 0) { + for(int num = len; num > 0; num >>>= 4){ + size[pos--] = index[num & 0xf]; + } + String text = String.format("%s; %s\r\n", Integer.toHexString(len), len); + + out.write(text.getBytes("ISO-8859-1")); + out.write(buf, off, len); + out.write(size, 8, 2); + } + } + + public void close() throws IOException { + out.write(zero); + out.close(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/Client.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/Client.java new file mode 100644 index 0000000..13d2f30 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/Client.java @@ -0,0 +1,264 @@ +package org.simpleframework.http.core; + +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.Socket; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.SocketFactory; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; + + +public class Client { + + + private static final byte[] CERTIFICATE = { + (byte)254,(byte)237,(byte)254,(byte)237,(byte)0, (byte)0, (byte)0, (byte)2, (byte)0, (byte)0, + (byte)0, (byte)1, (byte)0, (byte)0, (byte)0, (byte)1, (byte)0, (byte)3, (byte)107,(byte)101, + (byte)121,(byte)0, (byte)0, (byte)1, (byte)26, (byte)105,(byte)38, (byte)187,(byte)170,(byte)0, + (byte)0, (byte)2, (byte)187,(byte)48, (byte)130,(byte)2, (byte)183,(byte)48, (byte)14, (byte)6, + (byte)10, (byte)43, (byte)6, (byte)1, (byte)4, (byte)1, (byte)42, (byte)2, (byte)17, (byte)1, + (byte)1, (byte)5, (byte)0, (byte)4, (byte)130,(byte)2, (byte)163,(byte)138,(byte)122,(byte)194, + (byte)218,(byte)31, (byte)101,(byte)210,(byte)131,(byte)160,(byte)37, (byte)111,(byte)187,(byte)43, + (byte)192,(byte)67, (byte)244,(byte)136,(byte)120,(byte)166,(byte)171,(byte)204,(byte)87, (byte)156, + (byte)50, (byte)58, (byte)153,(byte)37, (byte)180,(byte)248,(byte)60, (byte)73, (byte)16, (byte)110, + (byte)176,(byte)84, (byte)239,(byte)247,(byte)113,(byte)133,(byte)193,(byte)239,(byte)94, (byte)107, + (byte)126,(byte)141,(byte)199,(byte)243,(byte)243,(byte)25, (byte)179,(byte)181,(byte)201,(byte)100, + (byte)194,(byte)146,(byte)114,(byte)162,(byte)124,(byte)96, (byte)198,(byte)248,(byte)232,(byte)162, + (byte)143,(byte)170,(byte)120,(byte)106,(byte)171,(byte)128,(byte)32, (byte)18, (byte)134,(byte)69, + (byte)2, (byte)230,(byte)204,(byte)18, (byte)191,(byte)212,(byte)236,(byte)130,(byte)76, (byte)24, + (byte)24, (byte)131,(byte)210,(byte)150,(byte)209,(byte)205,(byte)174,(byte)25, (byte)175,(byte)45, + (byte)39, (byte)223,(byte)17, (byte)57, (byte)129,(byte)6, (byte)195,(byte)116,(byte)197,(byte)143, + (byte)14, (byte)160,(byte)120,(byte)249,(byte)220,(byte)48, (byte)71, (byte)109,(byte)122,(byte)64, + (byte)195,(byte)139,(byte)61, (byte)206,(byte)83, (byte)159,(byte)78, (byte)137,(byte)160,(byte)88, + (byte)252,(byte)120,(byte)217,(byte)251,(byte)254,(byte)151,(byte)94, (byte)242,(byte)170,(byte)0, + (byte)247,(byte)170,(byte)53, (byte)197,(byte)34, (byte)253,(byte)96, (byte)47, (byte)248,(byte)194, + (byte)230,(byte)62, (byte)121,(byte)117,(byte)163,(byte)35, (byte)80, (byte)15, (byte)113,(byte)203, + (byte)71, (byte)202,(byte)36, (byte)187,(byte)163,(byte)78, (byte)228,(byte)31, (byte)3, (byte)53, + (byte)214,(byte)149,(byte)170,(byte)214,(byte)161,(byte)180,(byte)53, (byte)207,(byte)158,(byte)150, + (byte)161,(byte)37, (byte)59, (byte)150,(byte)107,(byte)161,(byte)9, (byte)195,(byte)79, (byte)254, + (byte)62, (byte)231,(byte)13, (byte)195,(byte)173,(byte)139,(byte)15, (byte)153,(byte)62, (byte)20, + (byte)204,(byte)111,(byte)64, (byte)89, (byte)180,(byte)201,(byte)58, (byte)64, (byte)15, (byte)195, + (byte)18, (byte)29, (byte)29, (byte)44, (byte)5, (byte)101,(byte)132,(byte)113,(byte)204,(byte)251, + (byte)225,(byte)3, (byte)82, (byte)52, (byte)62, (byte)86, (byte)142,(byte)43, (byte)240,(byte)201, + (byte)26, (byte)226,(byte)143,(byte)162,(byte)9, (byte)97, (byte)96, (byte)185,(byte)59, (byte)85, + (byte)54, (byte)115,(byte)135,(byte)199,(byte)26, (byte)58, (byte)185,(byte)100,(byte)118,(byte)48, + (byte)119,(byte)110,(byte)203,(byte)115,(byte)74, (byte)152,(byte)144,(byte)137,(byte)13, (byte)18, + (byte)192,(byte)82, (byte)101,(byte)163,(byte)8, (byte)128,(byte)57, (byte)68, (byte)183,(byte)225, + (byte)79, (byte)6, (byte)143,(byte)94, (byte)203,(byte)203,(byte)121,(byte)52, (byte)128,(byte)94, + (byte)184,(byte)223,(byte)107,(byte)217,(byte)68, (byte)118,(byte)145,(byte)164,(byte)13, (byte)220, + (byte)135,(byte)11, (byte)74, (byte)193,(byte)48, (byte)7, (byte)95, (byte)190,(byte)17, (byte)0, + (byte)69, (byte)109,(byte)6, (byte)64, (byte)86, (byte)80, (byte)93, (byte)82, (byte)20, (byte)106, + (byte)191,(byte)201,(byte)13, (byte)91, (byte)132,(byte)102,(byte)47, (byte)188,(byte)123,(byte)79, + (byte)209,(byte)43, (byte)180,(byte)152,(byte)128,(byte)20, (byte)182,(byte)148,(byte)19, (byte)24, + (byte)230,(byte)249,(byte)42, (byte)51, (byte)197,(byte)176,(byte)113,(byte)44, (byte)100,(byte)95, + (byte)59, (byte)91, (byte)78, (byte)226,(byte)184,(byte)224,(byte)72, (byte)233,(byte)133,(byte)154, + (byte)42, (byte)221,(byte)32, (byte)165,(byte)41, (byte)156,(byte)165,(byte)247,(byte)86, (byte)115, + (byte)183,(byte)22, (byte)89, (byte)17, (byte)165,(byte)215,(byte)148,(byte)32, (byte)199,(byte)64, + (byte)139,(byte)171,(byte)236,(byte)43, (byte)5, (byte)36, (byte)35, (byte)223,(byte)35, (byte)247, + (byte)255,(byte)112,(byte)27, (byte)215,(byte)57, (byte)251,(byte)236,(byte)128,(byte)168,(byte)219, + (byte)146,(byte)235,(byte)241,(byte)68, (byte)213,(byte)127,(byte)63, (byte)231,(byte)236,(byte)176, + (byte)166,(byte)121,(byte)203,(byte)114,(byte)33, (byte)19, (byte)200,(byte)167,(byte)155,(byte)27, + (byte)38, (byte)109,(byte)133,(byte)1, (byte)184,(byte)173,(byte)253,(byte)198,(byte)122,(byte)98, + (byte)196,(byte)43, (byte)145,(byte)86, (byte)182,(byte)208,(byte)78, (byte)246,(byte)234,(byte)249, + (byte)229,(byte)202,(byte)75, (byte)66, (byte)108,(byte)134,(byte)81, (byte)134,(byte)90, (byte)251, + (byte)137,(byte)155,(byte)209,(byte)11, (byte)249,(byte)87, (byte)164,(byte)98, (byte)242,(byte)51, + (byte)184,(byte)162,(byte)35, (byte)20, (byte)248,(byte)14, (byte)224,(byte)76, (byte)31, (byte)132, + (byte)125,(byte)44, (byte)83, (byte)15, (byte)221,(byte)43, (byte)62, (byte)187,(byte)211,(byte)176, + (byte)41, (byte)70, (byte)187,(byte)3, (byte)48, (byte)150,(byte)206,(byte)54, (byte)38, (byte)33, + (byte)94, (byte)133,(byte)145,(byte)148,(byte)58, (byte)219,(byte)252,(byte)124,(byte)251,(byte)46, + (byte)72, (byte)35, (byte)244,(byte)33, (byte)97, (byte)50, (byte)21, (byte)207,(byte)163,(byte)3, + (byte)226,(byte)225,(byte)252,(byte)149,(byte)214,(byte)200,(byte)132,(byte)65, (byte)224,(byte)121, + (byte)205,(byte)241,(byte)107,(byte)155,(byte)252,(byte)158,(byte)64, (byte)40, (byte)252,(byte)143, + (byte)76, (byte)71, (byte)227,(byte)13, (byte)176,(byte)50, (byte)250,(byte)115,(byte)198,(byte)64, + (byte)174,(byte)146,(byte)108,(byte)106,(byte)66, (byte)98, (byte)78, (byte)196,(byte)126,(byte)118, + (byte)51, (byte)65, (byte)251,(byte)8, (byte)28, (byte)75, (byte)123,(byte)92, (byte)5, (byte)125, + (byte)16, (byte)127,(byte)250,(byte)65, (byte)178,(byte)54, (byte)169,(byte)109,(byte)94, (byte)171, + (byte)97, (byte)154,(byte)232,(byte)24, (byte)196,(byte)91, (byte)103,(byte)90, (byte)217,(byte)75, + (byte)126,(byte)76, (byte)129,(byte)240,(byte)67, (byte)131,(byte)147,(byte)178,(byte)29, (byte)234, + (byte)150,(byte)91, (byte)78, (byte)165,(byte)76, (byte)200,(byte)99, (byte)175,(byte)240,(byte)3, + (byte)76, (byte)151,(byte)111,(byte)167,(byte)220,(byte)162,(byte)7, (byte)249,(byte)12, (byte)201, + (byte)171,(byte)58, (byte)170,(byte)26, (byte)149,(byte)224,(byte)135,(byte)201,(byte)186,(byte)201, + (byte)253,(byte)153,(byte)248,(byte)148,(byte)171,(byte)197,(byte)70, (byte)179,(byte)127,(byte)210, + (byte)30, (byte)172,(byte)207,(byte)179,(byte)140,(byte)240,(byte)244,(byte)2, (byte)24, (byte)156, + (byte)116,(byte)6, (byte)237,(byte)42, (byte)221,(byte)201,(byte)244,(byte)207,(byte)123,(byte)19, + (byte)189,(byte)58, (byte)189,(byte)107,(byte)223,(byte)44, (byte)230,(byte)114,(byte)115,(byte)194, + (byte)189,(byte)163,(byte)189,(byte)224,(byte)161,(byte)221,(byte)40, (byte)29, (byte)73, (byte)244, + (byte)231,(byte)213,(byte)139,(byte)178,(byte)248,(byte)84, (byte)137,(byte)65, (byte)124,(byte)98, + (byte)248,(byte)62, (byte)229,(byte)86, (byte)128,(byte)57, (byte)106,(byte)38, (byte)193,(byte)185, + (byte)10, (byte)162,(byte)0, (byte)0, (byte)0, (byte)1, (byte)0, (byte)5, (byte)88, (byte)46, + (byte)53, (byte)48, (byte)57, (byte)0, (byte)0, (byte)2, (byte)72, (byte)48, (byte)130,(byte)2, + (byte)68, (byte)48, (byte)130,(byte)1, (byte)173,(byte)2, (byte)4, (byte)72, (byte)76, (byte)18, + (byte)25, (byte)48, (byte)13, (byte)6, (byte)9, (byte)42, (byte)134,(byte)72, (byte)134,(byte)247, + (byte)13, (byte)1, (byte)1, (byte)4, (byte)5, (byte)0, (byte)48, (byte)105,(byte)49, (byte)16, + (byte)48, (byte)14, (byte)6, (byte)3, (byte)85, (byte)4, (byte)6, (byte)19, (byte)7, (byte)67, + (byte)111,(byte)117,(byte)110,(byte)116,(byte)114,(byte)121,(byte)49, (byte)17, (byte)48, (byte)15, + (byte)6, (byte)3, (byte)85, (byte)4, (byte)7, (byte)19, (byte)8, (byte)76, (byte)111,(byte)99, + (byte)97, (byte)116,(byte)105,(byte)111,(byte)110,(byte)49, (byte)28, (byte)48, (byte)26, (byte)6, + (byte)3, (byte)85, (byte)4, (byte)11, (byte)19, (byte)19, (byte)79, (byte)114,(byte)103,(byte)97, + (byte)110,(byte)105,(byte)122,(byte)97, (byte)116,(byte)105,(byte)111,(byte)110,(byte)97, (byte)108, + (byte)32, (byte)85, (byte)110,(byte)105,(byte)116,(byte)49, (byte)21, (byte)48, (byte)19, (byte)6, + (byte)3, (byte)85, (byte)4, (byte)10, (byte)19, (byte)12, (byte)79, (byte)114,(byte)103,(byte)97, + (byte)110,(byte)105,(byte)122,(byte)97, (byte)116,(byte)105,(byte)111,(byte)110,(byte)49, (byte)13, + (byte)48, (byte)11, (byte)6, (byte)3, (byte)85, (byte)4, (byte)3, (byte)19, (byte)4, (byte)78, + (byte)97, (byte)109,(byte)101,(byte)48, (byte)30, (byte)23, (byte)13, (byte)48, (byte)56, (byte)48, + (byte)54, (byte)48, (byte)56, (byte)49, (byte)55, (byte)48, (byte)56, (byte)52, (byte)49, (byte)90, + (byte)23, (byte)13, (byte)48, (byte)57, (byte)48, (byte)54, (byte)48, (byte)56, (byte)49, (byte)55, + (byte)48, (byte)56, (byte)52, (byte)49, (byte)90, (byte)48, (byte)105,(byte)49, (byte)16, (byte)48, + (byte)14, (byte)6, (byte)3, (byte)85, (byte)4, (byte)6, (byte)19, (byte)7, (byte)67, (byte)111, + (byte)117,(byte)110,(byte)116,(byte)114,(byte)121,(byte)49, (byte)17, (byte)48, (byte)15, (byte)6, + (byte)3, (byte)85, (byte)4, (byte)7, (byte)19, (byte)8, (byte)76, (byte)111,(byte)99, (byte)97, + (byte)116,(byte)105,(byte)111,(byte)110,(byte)49, (byte)28, (byte)48, (byte)26, (byte)6, (byte)3, + (byte)85, (byte)4, (byte)11, (byte)19, (byte)19, (byte)79, (byte)114,(byte)103,(byte)97, (byte)110, + (byte)105,(byte)122,(byte)97, (byte)116,(byte)105,(byte)111,(byte)110,(byte)97, (byte)108,(byte)32, + (byte)85, (byte)110,(byte)105,(byte)116,(byte)49, (byte)21, (byte)48, (byte)19, (byte)6, (byte)3, + (byte)85, (byte)4, (byte)10, (byte)19, (byte)12, (byte)79, (byte)114,(byte)103,(byte)97, (byte)110, + (byte)105,(byte)122,(byte)97, (byte)116,(byte)105,(byte)111,(byte)110,(byte)49, (byte)13, (byte)48, + (byte)11, (byte)6, (byte)3, (byte)85, (byte)4, (byte)3, (byte)19, (byte)4, (byte)78, (byte)97, + (byte)109,(byte)101,(byte)48, (byte)129,(byte)159,(byte)48, (byte)13, (byte)6, (byte)9, (byte)42, + (byte)134,(byte)72, (byte)134,(byte)247,(byte)13, (byte)1, (byte)1, (byte)1, (byte)5, (byte)0, + (byte)3, (byte)129,(byte)141,(byte)0, (byte)48, (byte)129,(byte)137,(byte)2, (byte)129,(byte)129, + (byte)0, (byte)137,(byte)239,(byte)22, (byte)193,(byte)171,(byte)79, (byte)177,(byte)85, (byte)159, + (byte)210,(byte)81, (byte)174,(byte)63, (byte)210,(byte)57, (byte)43, (byte)172,(byte)130,(byte)205, + (byte)144,(byte)207,(byte)100,(byte)16, (byte)69, (byte)78, (byte)72, (byte)22, (byte)155,(byte)44, + (byte)146,(byte)252,(byte)202,(byte)119,(byte)199,(byte)69, (byte)38, (byte)48, (byte)38, (byte)39, + (byte)46, (byte)119,(byte)219,(byte)200,(byte)105,(byte)216,(byte)188,(byte)162,(byte)175,(byte)74, + (byte)43, (byte)175,(byte)6, (byte)148,(byte)131,(byte)125,(byte)226,(byte)198,(byte)239,(byte)115, + (byte)204,(byte)196,(byte)28, (byte)189,(byte)108,(byte)236,(byte)29, (byte)132,(byte)72, (byte)207, + (byte)238,(byte)3, (byte)97, (byte)223,(byte)227,(byte)82, (byte)115,(byte)202,(byte)134,(byte)43, + (byte)242,(byte)83, (byte)70, (byte)226,(byte)172,(byte)162,(byte)177,(byte)183,(byte)128,(byte)126, + (byte)164,(byte)233,(byte)250,(byte)230,(byte)18, (byte)177,(byte)126,(byte)40, (byte)36, (byte)30, + (byte)169,(byte)124,(byte)126,(byte)203,(byte)23, (byte)252,(byte)38, (byte)55, (byte)250,(byte)181, + (byte)232,(byte)168,(byte)84, (byte)232,(byte)140,(byte)85, (byte)119,(byte)163,(byte)255,(byte)117, + (byte)133,(byte)174,(byte)51, (byte)195,(byte)8, (byte)174,(byte)200,(byte)142,(byte)43, (byte)2, + (byte)3, (byte)1, (byte)0, (byte)1, (byte)48, (byte)13, (byte)6, (byte)9, (byte)42, (byte)134, + (byte)72, (byte)134,(byte)247,(byte)13, (byte)1, (byte)1, (byte)4, (byte)5, (byte)0, (byte)3, + (byte)129,(byte)129,(byte)0, (byte)9, (byte)240,(byte)8, (byte)65, (byte)178,(byte)238,(byte)119, + (byte)127,(byte)249,(byte)164,(byte)9, (byte)159,(byte)110,(byte)132,(byte)177,(byte)76, (byte)239, + (byte)164,(byte)27, (byte)130,(byte)174,(byte)97, (byte)100,(byte)2, (byte)154,(byte)231,(byte)44, + (byte)217,(byte)30, (byte)210,(byte)42, (byte)221,(byte)225,(byte)114,(byte)205,(byte)165,(byte)152, + (byte)188,(byte)232,(byte)1, (byte)128,(byte)143,(byte)116,(byte)113,(byte)128,(byte)50, (byte)199, + (byte)80, (byte)16, (byte)172,(byte)112,(byte)129,(byte)236,(byte)34, (byte)189,(byte)106,(byte)79, + (byte)152,(byte)67, (byte)233,(byte)61, (byte)114,(byte)137,(byte)40, (byte)157,(byte)233,(byte)83, + (byte)123,(byte)28, (byte)138,(byte)168,(byte)46, (byte)151,(byte)36, (byte)177,(byte)7, (byte)22, + (byte)148,(byte)253,(byte)80, (byte)144,(byte)122,(byte)52, (byte)104,(byte)196,(byte)15, (byte)225, + (byte)148,(byte)136,(byte)193,(byte)68, (byte)133,(byte)113,(byte)48, (byte)244,(byte)8, (byte)64, + (byte)117,(byte)110,(byte)115,(byte)80, (byte)110,(byte)105,(byte)56, (byte)20, (byte)170,(byte)125, + (byte)182,(byte)159,(byte)190,(byte)4, (byte)173,(byte)193,(byte)200,(byte)153,(byte)246,(byte)155, + (byte)249,(byte)33, (byte)180,(byte)233,(byte)48, (byte)109,(byte)55, (byte)208,(byte)209,(byte)196, + (byte)16, (byte)23, (byte)172,(byte)125,(byte)207,(byte)94, (byte)238,(byte)23, (byte)38, (byte)60, + (byte)58, (byte)92, (byte)244,(byte)100,(byte)145,(byte)44, (byte)204,(byte)92, (byte)21, (byte)136, + (byte)39, }; + + + public static class AnonymousTrustManager implements X509TrustManager { + + public boolean isClientTrusted(X509Certificate[] cert) { + return true; + } + + public boolean isServerTrusted(X509Certificate[] cert) { + return true; + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] arg0, String arg1) + throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] arg0, String arg1) + throws CertificateException { + } + } + + private static SSLContext sslContext; + private static SocketFactory factory; + + static { + try { + KeyStore store = KeyStore.getInstance("JKS"); + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance("SunX509"); + sslContext = SSLContext.getInstance("TLS");//SSLv3"); + InputStream stream = new ByteArrayInputStream(CERTIFICATE); + X509TrustManager trustManager = new AnonymousTrustManager(); + X509TrustManager[] trustManagers = new X509TrustManager[]{trustManager}; + + store.load(stream, "password".toCharArray()); + keyFactory.init(store, "password".toCharArray()); + sslContext.init(keyFactory.getKeyManagers(), trustManagers, null); + + factory = sslContext.getSocketFactory(); + }catch(Exception e) { + e.printStackTrace(); + } + + } + + + public SSLContext getServerSSLContext() { + return sslContext; + } + + public SocketFactory getClientSocketFactory() { + return factory; + } + + public static void main(String[] list) throws Exception { + FileOutputStream out = new FileOutputStream("c:\\client"); + final PrintStream console = System.out; + OutputStream dup = new FilterOutputStream(out) { + public void write(int off) throws IOException { + console.write(off); + out.write(off); + } + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + console.write(b, off, len); + } + public void flush() throws IOException { + out.flush(); + console.flush(); + } + public void close() throws IOException { + out.close(); + } + }; + PrintStream p = new PrintStream(dup, true); + + System.setOut(p); + System.setErr(p); + Socket socket = factory.createSocket("localhost", 9091); + OutputStream sockOut = socket.getOutputStream(); + sockOut.write("GET /tmp/amazon.htm HTTP/1.1\r\nConnection: keep-alive\r\n\r\n".getBytes("ISO-8859-1")); + sockOut.flush(); + InputStream in = socket.getInputStream(); + byte[] buf = new byte[1024]; + int all = 0; + int count = 0; + while((count = in.read(buf)) != -1) { + all += count; + if(all >= 564325) { + break; + } + System.out.write(buf, 0, count); + System.out.flush(); + } + console.println(">>>>>>>>>>>>>> ALL=["+all+"]"); + System.err.println("FINISHED READING"); + Thread.sleep(10000); + + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/Connector.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/Connector.java new file mode 100644 index 0000000..d002c86 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/Connector.java @@ -0,0 +1,9 @@ +package org.simpleframework.http.core; + +import java.net.Socket; + +public interface Connector { + + public Socket getSocket() throws Exception; + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/ConversationTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/ConversationTest.java new file mode 100644 index 0000000..00855c2 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/ConversationTest.java @@ -0,0 +1,126 @@ +package org.simpleframework.http.core; + +import org.simpleframework.http.core.Conversation; + +import junit.framework.TestCase; + +public class ConversationTest extends TestCase { + + private MockRequest request; + private MockResponse response; + private Conversation support; + + public void setUp() { + request = new MockRequest(); + response = new MockResponse(); + support = new Conversation(request, response); + } + + public void testWebSocket() { + request.setMajor(1); + request.setMinor(1); + response.setValue("Connection", "upgrade"); + + assertFalse(support.isWebSocket()); + assertFalse(support.isTunnel()); + assertTrue(support.isKeepAlive()); + + request.setValue("Upgrade", "WebSocket"); + + assertFalse(support.isWebSocket()); + assertFalse(support.isTunnel()); + assertTrue(support.isKeepAlive()); + + response.setCode(101); + response.setValue("Upgrade", "websocket"); + + assertTrue(support.isWebSocket()); + assertTrue(support.isTunnel()); + assertTrue(support.isKeepAlive()); + } + + public void testConnectTunnel() { + request.setMajor(1); + request.setMinor(1); + response.setCode(404); + request.setMethod("CONNECT"); + + assertFalse(support.isWebSocket()); + assertFalse(support.isTunnel()); + assertTrue(support.isKeepAlive()); + + response.setCode(200); + + assertFalse(support.isWebSocket()); + assertTrue(support.isTunnel()); + assertTrue(support.isKeepAlive()); + } + + public void testResponse() { + request.setMajor(1); + request.setMinor(1); + response.setValue("Content-Length", "10"); + response.setValue("Connection", "close"); + + assertFalse(support.isKeepAlive()); + assertTrue(support.isPersistent()); + assertEquals(support.getContentLength(), 10); + assertEquals(support.isChunkedEncoded(), false); + + request.setMinor(0); + + assertFalse(support.isKeepAlive()); + assertFalse(support.isPersistent()); + + response.setValue("Connection", "keep-alive"); + + assertTrue(support.isKeepAlive()); + assertFalse(support.isPersistent()); + + response.setValue("Transfer-Encoding", "chunked"); + + assertTrue(support.isChunkedEncoded()); + assertTrue(support.isKeepAlive()); + } + + public void testConversation() { + request.setMajor(1); + request.setMinor(1); + support.setChunkedEncoded(); + + assertEquals(response.getValue("Transfer-Encoding"), "chunked"); + assertEquals(response.getValue("Connection"), "keep-alive"); + assertTrue(support.isKeepAlive()); + assertTrue(support.isPersistent()); + + request.setMinor(0); + support.setChunkedEncoded(); + + assertEquals(response.getValue("Connection"), "close"); + assertFalse(support.isKeepAlive()); + + request.setMajor(1); + request.setMinor(1); + response.setValue("Content-Length", "10"); + response.setValue("Connection", "close"); + + assertFalse(support.isKeepAlive()); + assertTrue(support.isPersistent()); + assertEquals(support.getContentLength(), 10); + + request.setMinor(0); + + assertFalse(support.isKeepAlive()); + assertFalse(support.isPersistent()); + + response.setValue("Connection", "keep-alive"); + + assertTrue(support.isKeepAlive()); + assertFalse(support.isPersistent()); + + response.setValue("Transfer-Encoding", "chunked"); + + assertTrue(support.isChunkedEncoded()); + assertTrue(support.isKeepAlive()); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/DribbleCursor.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/DribbleCursor.java new file mode 100644 index 0000000..14f2768 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/DribbleCursor.java @@ -0,0 +1,62 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.transport.ByteCursor; + +public class DribbleCursor implements ByteCursor { + + private ByteCursor cursor; + private byte[] swap; + private int dribble; + + public DribbleCursor(ByteCursor cursor, int dribble) { + this.cursor = cursor; + this.dribble = dribble; + this.swap = new byte[1]; + } + + public boolean isOpen() throws IOException { + return true; + } + + public boolean isReady() throws IOException { + return cursor.isReady(); + } + + public int ready() throws IOException { + int ready = cursor.ready(); + + return Math.min(ready, dribble); + } + + public int read() throws IOException { + if(read(swap) > 0) { + return swap[0] & 0xff; + } + return 0; + } + + + public int read(byte[] data) throws IOException { + return read(data, 0, data.length); + } + + public int read(byte[] data, int off, int len) throws IOException { + int size = Math.min(len, dribble); + + return cursor.read(data, off, size); + } + + public int reset(int len) throws IOException { + return cursor.reset(len); + } + + public void push(byte[] data) throws IOException { + cursor.push(data); + } + + public void push(byte[] data, int off, int len) throws IOException { + cursor.push(data, off, len); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/FixedConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/FixedConsumerTest.java new file mode 100644 index 0000000..f0011ce --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/FixedConsumerTest.java @@ -0,0 +1,80 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.http.message.FixedLengthConsumer; +import org.simpleframework.transport.ByteCursor; + +public class FixedConsumerTest extends TestCase implements Allocator { + + private Buffer buffer; + + public Buffer allocate() { + return buffer; + } + + public Buffer allocate(long size) { + return buffer; + } + + public void testConsumer() throws Exception { + testConsumer(10, 10, 10); + testConsumer(1024, 10, 1024); + testConsumer(1024, 1024, 1024); + testConsumer(1024, 1024, 1023); + testConsumer(1024, 1, 1024); + testConsumer(1, 1, 1); + testConsumer(2, 2, 2); + testConsumer(3, 1, 2); + } + + public void testConsumer(int entitySize, int dribble, int limitSize) throws Exception { + StringBuffer buf = new StringBuffer(); + + // Ensure that we dont try read forever + limitSize = Math.min(entitySize, limitSize); + + for(int i = 0, line = 0; i < entitySize; i++) { + String text = "["+String.valueOf(i)+"]"; + + line += text.length(); + buf.append(text); + + if(line >= 48) { + buf.append("\n"); + line = 0; + } + + } + buffer = new ArrayAllocator().allocate(); + + String requestBody = buf.toString(); + FixedLengthConsumer consumer = new FixedLengthConsumer(this, limitSize); + ByteCursor cursor = new DribbleCursor(new StreamCursor(requestBody), dribble); + byte[] requestBytes = requestBody.getBytes("UTF-8"); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + byte[] consumedBytes = buffer.encode("UTF-8").getBytes("UTF-8"); + + assertEquals(buffer.encode("UTF-8").length(), limitSize); + + for(int i = 0; i < limitSize; i++) { + if(consumedBytes[i] != requestBytes[i]) { + throw new IOException("Fixed consumer modified the request!"); + } + } + } + + public void close() throws IOException { + // TODO Auto-generated method stub + + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/FixedProducerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/FixedProducerTest.java new file mode 100644 index 0000000..f7b8f33 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/FixedProducerTest.java @@ -0,0 +1,50 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.http.core.FixedLengthEncoder; + +import junit.framework.TestCase; + +public class FixedProducerTest extends TestCase { + + public void testContent() throws IOException { + testContent(1024, 1); + testContent(1024, 2); + testContent(512, 20); + testContent(64, 64); + } + + public void testContent(int chunkSize, int count) throws IOException { + MockSender sender = new MockSender((chunkSize * count) + chunkSize); + MockObserver monitor = new MockObserver(); + FixedLengthEncoder producer = new FixedLengthEncoder(monitor, sender, chunkSize * count); + byte[] chunk = new byte[chunkSize]; + + for(int i = 0; i < chunk.length; i++) { + chunk[i] = (byte)String.valueOf(i).charAt(0); + } + for(int i = 0; i < count; i++) { + producer.encode(chunk, 0, chunkSize); + } + producer.close(); + + System.err.println(sender.getBuffer().encode()); + + assertTrue(monitor.isReady()); + assertFalse(monitor.isError()); + assertFalse(monitor.isClose()); + + sender = new MockSender((chunkSize * count) + chunkSize); + monitor = new MockObserver(); + producer = new FixedLengthEncoder(monitor, sender, chunkSize * count); + + for(int i = 0; i < count; i++) { + producer.encode(chunk, 0, chunkSize); + } + producer.close(); + + assertFalse(monitor.isError()); + assertTrue(monitor.isReady()); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MessageTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MessageTest.java new file mode 100644 index 0000000..b972f03 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MessageTest.java @@ -0,0 +1,72 @@ +package org.simpleframework.http.core; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import org.simpleframework.http.message.MessageHeader; + +import junit.framework.TestCase; + +public class MessageTest extends TestCase { + + public void testMessage() { + MessageHeader message = new MessageHeader(); + + message.addValue("Content-Length", "10"); + message.addValue("Connection", "keep-alive"); + message.addValue("Accept", "image/gif, image/jpeg, */*"); + message.addValue("Set-Cookie", "a=b"); + message.addValue("Set-Cookie", "b=c"); + + assertEquals(message.getValue("CONTENT-LENGTH"), "10"); + assertEquals(message.getValue("Content-Length"), "10"); + assertEquals(message.getValue("CONTENT-length"), "10"); + assertEquals(message.getValue("connection"), "keep-alive"); + assertEquals(message.getValue("CONNECTION"), "keep-alive"); + + assertTrue(message.getValues("CONNECTION") != null); + assertEquals(message.getValues("connection").size(), 1); + + assertTrue(message.getValues("set-cookie") != null); + assertEquals(message.getValues("set-cookie").size(), 2); + assertTrue(message.getValues("SET-COOKIE").contains("a=b")); + assertTrue(message.getValues("SET-COOKIE").contains("b=c")); + + assertTrue(message.getNames().contains("Content-Length")); + assertFalse(message.getNames().contains("CONTENT-LENGTH")); + assertTrue(message.getNames().contains("Connection")); + assertFalse(message.getNames().contains("CONNECTION")); + assertTrue(message.getNames().contains("Set-Cookie")); + assertFalse(message.getNames().contains("SET-COOKIE")); + + message.setValue("Set-Cookie", "d=e"); + + assertTrue(message.getValues("set-cookie") != null); + assertEquals(message.getValues("set-cookie").size(), 1); + assertFalse(message.getValues("SET-COOKIE").contains("a=b")); + assertFalse(message.getValues("SET-COOKIE").contains("b=c")); + assertTrue(message.getValues("SET-COOKIE").contains("d=e")); + } + + public void testDates() { + MessageHeader message = new MessageHeader(); + DateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); + TimeZone zone = TimeZone.getTimeZone("GMT"); + long time = System.currentTimeMillis(); + Date date = new Date(time); + + format.setTimeZone(zone); + message.setValue("Date", format.format(date)); + + assertEquals(format.format(date), message.getValue("date")); + assertEquals(new Date(message.getDate("DATE")).toString(), date.toString()); + + message.setDate("Date", time); + + assertEquals(format.format(date), message.getValue("date")); + assertEquals(new Date(message.getDate("DATE")).toString(), date.toString()); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockChannel.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockChannel.java new file mode 100644 index 0000000..92a4f5d --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockChannel.java @@ -0,0 +1,57 @@ +package org.simpleframework.http.core; + +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.Map; + +import org.simpleframework.common.lease.Lease; +import org.simpleframework.http.MockTrace; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + + +public class MockChannel implements Channel { + + private ByteCursor cursor; + + public MockChannel(ByteCursor cursor) { + this.cursor = cursor; + } + + public boolean isSecure() { + return false; + } + + public Trace getTrace(){ + return new MockTrace(); + } + + public Lease getLease() { + return null; + } + + public Certificate getCertificate() { + return null; + } + + public ByteCursor getCursor() { + return cursor; + } + + public ByteWriter getWriter() { + return new MockSender(); + } + + public Map getAttributes() { + return new HashMap(); + } + + public void close() {} + + public SocketChannel getSocket() { + return null; + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockController.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockController.java new file mode 100644 index 0000000..b631803 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockController.java @@ -0,0 +1,55 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.transport.Channel; + +public class MockController implements Controller { + + private boolean ready; + private boolean sleep; + private boolean start; + private boolean initiated; + private boolean stop; + + public void start(Channel channel) throws IOException { + initiated = true; + } + + public void ready(Collector collector) throws IOException { + ready = true; + } + + public void select(Collector collector) throws IOException { + sleep = true; + } + + public void start(Collector collector) throws IOException { + start = true; + } + + public void stop() throws IOException { + stop = true; + } + + public boolean isStopped() { + return stop; + } + + public boolean isInitiated() { + return initiated; + } + + public boolean isReady() { + return ready; + } + + public boolean isSleep() { + return sleep; + } + + public boolean isStart() { + return start; + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockEntity.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockEntity.java new file mode 100644 index 0000000..e0ec896 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockEntity.java @@ -0,0 +1,49 @@ + +package org.simpleframework.http.core; + +import org.simpleframework.http.message.Body; +import org.simpleframework.http.message.Entity; +import org.simpleframework.http.message.Header; +import org.simpleframework.transport.Channel; + + +public class MockEntity implements Entity { + + private Body body; + private Header header; + + public MockEntity() { + super(); + } + + public MockEntity(Body body) { + this.body = body; + } + + public MockEntity(Body body, Header header) { + this.body = body; + this.header = header; + } + + public long getTime() { + return 0; + } + + public Body getBody() { + return body; + } + + public Header getHeader() { + return header; + } + + public Channel getChannel() { + return null; + } + + public void close() {} + + public long getStart() { + return 0; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockObserver.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockObserver.java new file mode 100644 index 0000000..cb4b41e --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockObserver.java @@ -0,0 +1,62 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.transport.ByteWriter; + + +public class MockObserver implements BodyObserver { + + private boolean close; + + private boolean error; + + private boolean ready; + + private boolean commit; + + public MockObserver() { + super(); + } + + public void close(ByteWriter sender) { + close = true; + } + + public boolean isClose() { + return close; + } + + public boolean isError() { + return error; + } + + public void ready(ByteWriter sender) { + ready = true; + } + + public boolean isReady() { + return ready; + } + + public void error(ByteWriter sender) { + error = true; + } + + public boolean isClosed() { + return close || error; + } + + public long getTime() { + return 0; + } + + public void commit(ByteWriter sender) { + this.commit = commit; + } + + public boolean isCommitted() { + return commit; + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockPart.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockPart.java new file mode 100644 index 0000000..614a7aa --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockPart.java @@ -0,0 +1,49 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.http.ContentDisposition; +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Part; +import org.simpleframework.http.message.MockBody; + +public class MockPart extends MockBody implements Part { + + private String name; + private boolean file; + + public MockPart(String name, String body, boolean file) { + super(body); + this.file = file; + this.name = name; + } + + public String getContent() throws IOException { + return body; + } + + public ContentType getContentType() { + return null; + } + + public ContentDisposition getDisposition() { + return null; + } + + public String getHeader(String name) { + return null; + } + + public String getName() { + return name; + } + + public boolean isFile() { + return file; + } + + public String getFileName() { + return null; + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockProxyRequest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockProxyRequest.java new file mode 100644 index 0000000..a7f12b6 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockProxyRequest.java @@ -0,0 +1,67 @@ +package org.simpleframework.http.core; + +import java.util.List; + +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.Path; +import org.simpleframework.http.Query; +import org.simpleframework.http.RequestHeader; + +public class MockProxyRequest extends MockRequest { + + private RequestHeader header; + + public MockProxyRequest(RequestHeader header) { + this.header = header; + } + + public long getContentLength() { + return header.getContentLength(); + } + + public ContentType getContentType() { + return header.getContentType(); + } + + public String getValue(String name) { + return header.getValue(name); + } + + public List getValues(String name) { + return header.getValues(name); + } + + public int getMajor() { + return header.getMajor(); + } + + public String getMethod() { + return header.getMethod(); + } + + public int getMinor() { + return header.getMajor(); + } + + public Path getPath() { + return header.getPath(); + } + + public Query getQuery() { + return header.getQuery(); + } + + public String getTarget() { + return header.getTarget(); + } + + + public String getParameter(String name) { + return header.getQuery().get(name); + } + + public Cookie getCookie(String name) { + return header.getCookie(name); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockRequest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockRequest.java new file mode 100644 index 0000000..f382a32 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockRequest.java @@ -0,0 +1,202 @@ +package org.simpleframework.http.core; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.channels.ReadableByteChannel; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.simpleframework.http.ContentDisposition; +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.Part; +import org.simpleframework.http.Path; +import org.simpleframework.http.Query; +import org.simpleframework.http.Request; +import org.simpleframework.http.message.MessageHeader; +import org.simpleframework.http.message.RequestConsumer; +import org.simpleframework.http.parse.AddressParser; +import org.simpleframework.http.parse.ContentDispositionParser; +import org.simpleframework.http.parse.ContentTypeParser; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; + +public class MockRequest extends RequestMessage implements Request { + + private MessageHeader message; + private Channel channel; + private String target; + private String method = "GET"; + private String content; + private String type; + private int major = 1; + private int minor = 1; + + public MockRequest() { + this.header = new RequestConsumer(); + this.message = new MessageHeader(); + this.channel = new MockChannel(null); + } + + public void setValue(String name, String value) { + message.setValue(name, value); + } + + public void add(String name, String value) { + message.addValue(name, value); + } + + public boolean isSecure(){ + return false; + } + + public String getTarget() { + return target; + } + + public void setContentType(String value) { + type = value; + } + + public void setTarget(String target) { + this.target = target; + } + + public Path getPath() { + return new AddressParser(target).getPath(); + } + + public Query getQuery() { + return new AddressParser(target).getQuery(); + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public int getMajor() { + return major; + } + + public void setMajor(int major) { + this.major = major; + } + + public int getMinor() { + return minor; + } + + public void setMinor(int minor) { + this.minor = minor; + } + + public Certificate getClientCertificate() { + return null; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public InputStream getInputStream() { + return null; + } + + public Part getPart(String name) { + return null; + } + + public List getParts() { + return Collections.emptyList(); + } + + public int size() { + return 0; + } + + public Cookie getCookie(String name) { + return null; + } + + public String getParameter(String name) { + return null; + } + + public Map getAttributes() { + return null; + } + + + public ContentType getContentType() { + return new ContentTypeParser(type); + } + + public long getContentLength() { + String value = getValue("Content-Length"); + + if(value != null) { + return new Long(value); + } + return -1; + } + + public String getTransferEncoding() { + List list = getValues("Transfer-Encoding"); + + if(list.size() > 0) { + return list.get(0); + } + return null; + } + + public ContentDisposition getDisposition() { + String value = getValue("Content-Disposition"); + + if(value == null) { + return null; + } + return new ContentDispositionParser(value); + } + + public List getValues(String name) { + return message.getValues(name); + } + + public String getValue(String name) { + return message.getValue(name); + } + + public Object getAttribute(Object key) { + return null; + } + + public boolean isKeepAlive() { + return true; + } + + public InetSocketAddress getClientAddress() { + return null; + } + + public ReadableByteChannel getByteChannel() throws IOException { + return null; + } + + public long getRequestTime() { + return 0; + } + + public Channel getChannel() { + return channel; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockResponse.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockResponse.java new file mode 100644 index 0000000..43c0b86 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockResponse.java @@ -0,0 +1,95 @@ +package org.simpleframework.http.core; + +import static org.simpleframework.http.Protocol.CLOSE; +import static org.simpleframework.http.Protocol.CONNECTION; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.channels.WritableByteChannel; +import java.util.Map; + +import org.simpleframework.http.Response; +import org.simpleframework.http.core.ResponseMessage; + +public class MockResponse extends ResponseMessage implements Response { + + private boolean committed; + + public MockResponse() { + super(); + } + + public OutputStream getOutputStream() { + return System.out; + } + + public boolean isKeepAlive() { + String value = getValue(CONNECTION); + + if(value != null) { + return value.equalsIgnoreCase(CLOSE); + } + return true; + } + + public boolean isCommitted() { + return committed; + } + + public void commit() { + committed = true; + } + + public void reset() { + return; + } + + public void close() { + return; + } + + public Object getAttribute(String name) { + return null; + } + + public Map getAttributes() { + return null; + } + + public OutputStream getOutputStream(int size) throws IOException { + return null; + } + + public PrintStream getPrintStream() throws IOException { + return null; + } + + public PrintStream getPrintStream(int size) throws IOException { + return null; + } + + public void setContentLength(long length) { + setValue("Content-Length", String.valueOf(length)); + } + + public WritableByteChannel getByteChannel() throws IOException { + return null; + } + + public WritableByteChannel getByteChannel(int size) throws IOException { + return null; + } + + public boolean isEmpty() { + return false; + } + + public long getResponseTime() { + return 0; + } + + public void setContentType(String type) { + setValue("Content-Type", type); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockSender.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockSender.java new file mode 100644 index 0000000..eb20930 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockSender.java @@ -0,0 +1,75 @@ +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.simpleframework.common.buffer.ArrayBuffer; +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.ByteWriter; + +public class MockSender implements ByteWriter { + + private Buffer buffer; + + public MockSender() { + this(1024); + } + + public MockSender(int size) { + this.buffer = new ArrayBuffer(size); + } + + public Buffer getBuffer() { + return buffer; + } + + public ByteCursor getCursor() throws IOException { + return new StreamCursor(buffer.encode("UTF-8")); + } + + public void write(byte[] array) throws IOException { + buffer.append(array); + } + + public void write(byte[] array, int off, int len) throws IOException { + buffer.append(array, off, len); + } + + public void flush() throws IOException { + return; + } + + public void close() throws IOException { + return; + } + + public String toString() { + return buffer.toString(); + } + + public boolean isOpen() throws Exception { + return true; + } + + public void write(ByteBuffer source) throws IOException { + int mark = source.position(); + int limit = source.limit(); + + byte[] array = new byte[limit - mark]; + source.get(array, 0, array.length); + buffer.append(array); + } + + public void write(ByteBuffer source, int off, int len) throws IOException { + int mark = source.position(); + int limit = source.limit(); + + if(limit - mark < len) { + len = limit - mark; + } + byte[] array = new byte[len]; + source.get(array, 0, len); + buffer.append(array); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/MockSocket.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockSocket.java new file mode 100644 index 0000000..5ac7a14 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/MockSocket.java @@ -0,0 +1,42 @@ +package org.simpleframework.http.core; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; + +public class MockSocket extends Socket { + + private Socket socket; + + private OutputStream out; + + public MockSocket(Socket socket) { + this(socket, System.err); + } + + public MockSocket(Socket socket, OutputStream out){ + this.socket = socket; + this.out = out; + } + + @Override + public void setSoTimeout(int delay) throws SocketException { + socket.setSoTimeout(delay); + } + + @Override + public int getSoTimeout() throws SocketException { + return socket.getSoTimeout(); + } + + + public InputStream getInputStream() throws IOException { + return socket.getInputStream(); + } + + public OutputStream getOutputStream() { + return out; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/PayloadTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/PayloadTest.java new file mode 100644 index 0000000..6b23fab --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/PayloadTest.java @@ -0,0 +1,97 @@ +package org.simpleframework.http.core; + +import java.util.List; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.Part; +import org.simpleframework.http.message.Header; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; + +public class PayloadTest extends TestCase { + + private static final String PAYLOAD = + "POST /index.html HTTP/1.0\r\n"+ + "Content-Type: multipart/form-data; boundary=AaB03x\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n" + + "--AaB03x\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file1.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file1.txt\r\n"+ + "--AaB03x\r\n"+ + "Content-Type: multipart/mixed; boundary=BbC04y\r\n\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file2.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file3.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file3.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file4.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y--\r\n"+ + "--AaB03x--\r\n"; + + + public void testPayload() throws Exception { + for(int i = 1; i < 4096; i++) { + testPayload(i); + } + } + + public void testPayload(int dribble) throws Exception { + ByteCursor cursor = new DribbleCursor(new StreamCursor(PAYLOAD), 10); + Channel channel = new MockChannel(cursor); + MockController selector = new MockController(); + Collector body = new RequestCollector(new ArrayAllocator(), channel); + long time = System.currentTimeMillis(); + + while(!selector.isReady()) { + body.collect(selector); + } + System.err.println("Time taken to parse payload "+(System.currentTimeMillis() - time)+" ms"); + + Header header = body.getHeader(); + List list = body.getBody().getParts(); + + assertEquals(header.getTarget(), "/index.html"); + assertEquals(header.getMethod(), "POST"); + assertEquals(header.getMajor(), 1); + assertEquals(header.getMinor(), 0); + assertEquals(header.getContentType().getPrimary(), "multipart"); + assertEquals(header.getContentType().getSecondary(), "form-data"); + assertEquals(header.getValue("Host"), "some.host.com"); + assertEquals(header.getValues("Accept").size(), 4); + assertEquals(header.getValues("Accept").get(0), "image/gif"); + assertEquals(header.getValues("Accept").get(1), "image/png"); + assertEquals(header.getValues("Accept").get(2), "image/jpeg"); + assertEquals(header.getValues("Accept").get(3), "*"); + assertEquals(list.size(), 4); + assertEquals(list.get(0).getContentType().getPrimary(), "text"); + assertEquals(list.get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.get(0).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file1.txt'"); + assertEquals(list.get(1).getContentType().getPrimary(), "text"); + assertEquals(list.get(1).getContentType().getSecondary(), "plain"); + assertEquals(list.get(1).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file2.txt'"); + assertEquals(list.get(2).getContentType().getPrimary(), "text"); + assertEquals(list.get(2).getContentType().getSecondary(), "plain"); + assertEquals(list.get(2).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file3.txt'"); + assertEquals(list.get(3).getContentType().getPrimary(), "text"); + assertEquals(list.get(3).getContentType().getSecondary(), "plain"); + assertEquals(list.get(3).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file4.txt'"); + assertEquals(cursor.ready(), -1); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/ProducerExceptionTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/ProducerExceptionTest.java new file mode 100644 index 0000000..d81370e --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/ProducerExceptionTest.java @@ -0,0 +1,23 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import junit.framework.TestCase; + +public class ProducerExceptionTest extends TestCase { + + public void testException() { + try { + throw new IOException("Error"); + }catch(Exception main) { + try { + throw new BodyEncoderException("Wrapper", main); + }catch(Exception cause) { + cause.printStackTrace(); + + assertEquals(cause.getCause(), main); + } + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/QueryBuilderTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/QueryBuilderTest.java new file mode 100644 index 0000000..92c9d64 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/QueryBuilderTest.java @@ -0,0 +1,35 @@ +package org.simpleframework.http.core; + +import junit.framework.TestCase; + +import org.simpleframework.http.Query; +import org.simpleframework.http.message.MockBody; +import org.simpleframework.http.message.MockHeader; + +public class QueryBuilderTest extends TestCase{ + + public void testBuilder() throws Exception { + MockRequest request = new MockRequest(); + + request.setContentType("application/x-www-form-urlencoded"); + request.setContent("a=post_A&c=post_C&e=post_E"); + + MockBody body = new MockBody(); + MockHeader header = new MockHeader("/path?a=query_A&b=query_B&c=query_C&d=query_D"); + MockEntity entity = new MockEntity(body, header); + QueryBuilder builder = new QueryBuilder(request, entity); + + Query form = builder.build(); + + assertEquals(form.getAll("a").size(), 2); + assertEquals(form.getAll("b").size(), 1); + assertEquals(form.getAll("c").size(), 2); + assertEquals(form.getAll("e").size(), 1); + + assertEquals(form.get("a"), "query_A"); + assertEquals(form.get("b"), "query_B"); + assertEquals(form.get("c"), "query_C"); + assertEquals(form.get("e"), "post_E"); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorProcessorTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorProcessorTest.java new file mode 100644 index 0000000..9b0bdcd --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorProcessorTest.java @@ -0,0 +1,247 @@ +package org.simpleframework.http.core; + +import java.nio.channels.SocketChannel; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.MockTrace; +import org.simpleframework.http.Part; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.ReactorTest.TestChannel; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +public class ReactorProcessorTest extends TestCase implements Container { + + private static final int ITERATIONS = 20000; + + private static final String MINIMAL = + "HEAD /MINIMAL/%s HTTP/1.0\r\n" + + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "\r\n"; + + private static final String SIMPLE = + "GET /SIMPLE/%s HTTP/1.0\r\n" + + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n"; + + private static final String UPLOAD = + "POST /UPLOAD/%s HTTP/1.0\r\n" + + "Content-Type: multipart/form-data; boundary=AaB03x\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n" + + "--AaB03x\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file1.txt\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file1.txt\r\n"+ + "--AaB03x\r\n"+ + "Content-Type: multipart/mixed; boundary=BbC04y\r\n\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file2.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file3.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file3.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file4.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y--\r\n"+ + "--AaB03x--\r\n"; + + private static class StopWatch { + + private long duration; + + private long start; + + public StopWatch() { + this.start = System.currentTimeMillis(); + } + + public long time() { + return duration; + } + + public void stop() { + duration = System.currentTimeMillis() - start; + } + } + + public static class MockChannel implements Channel { + + private ByteCursor cursor; + + public MockChannel(StreamCursor cursor, int dribble) { + this.cursor = new DribbleCursor(cursor, dribble); + } + public boolean isSecure() { + return false; + } + + public Trace getTrace() { + return new MockTrace(); + } + + public Certificate getCertificate() { + return null; + } + + public ByteCursor getCursor() { + return cursor; + } + + public ByteWriter getWriter() { + return null; + } + + public Map getAttributes() { + return null; + } + + public void close() {} + + public SocketChannel getSocket() { + return null; + } + } + + private ConcurrentHashMap timers = new ConcurrentHashMap(); + + private LinkedBlockingQueue finished = new LinkedBlockingQueue(); + + public void testMinimal() throws Exception { + Controller handler = new ContainerController(this, new ArrayAllocator(), 10, 2); + + testRequest(handler, "/MINIMAL/%s", MINIMAL, "MINIMAL"); + testRequest(handler, "/SIMPLE/%s", SIMPLE, "SIMPLE"); + testRequest(handler, "/UPLOAD/%s", UPLOAD, "UPLOAD"); + } + + public void testRequest(Controller handler, String target, String payload, String name) throws Exception { + long start = System.currentTimeMillis(); + + for(int i = 0; i < ITERATIONS; i++) { + String request = String.format(payload, i); + StopWatch stopWatch = new StopWatch(); + + timers.put(String.format(target, i), stopWatch); + testHandler(handler, request, 2048); + } + double sum = 0; + + for(int i = 0; i < ITERATIONS; i++) { + StopWatch stopWatch = finished.take(); + sum += stopWatch.time(); + } + double total = (System.currentTimeMillis() - start); + double count = ITERATIONS; + + System.err.println(String.format("%s total=[%s] for=[%s] average=[%s] time-per-request=[%s] request-per-millisecond=[%s] request-per-second=[%s]", + name, total, count, sum / count, total / count, count / total + 1, count / (total / 1000))); + } + + public void testHandler(Controller handler, String payload, int dribble) throws Exception { + StreamCursor cursor = new StreamCursor(payload); + Channel channel = new TestChannel(cursor, dribble); + + handler.start(channel); + } + + + public void handle(Request request, Response response) { + try { + process(request, response); + }catch(Exception e) { + e.printStackTrace(); + assertTrue(false); + } + } + + public void process(Request request, Response response) throws Exception { + List list = request.getParts(); + String method = request.getMethod(); + + if(method.equals("HEAD")) { + assertEquals(request.getMajor(), 1); + assertEquals(request.getMinor(), 0); + assertEquals(request.getValue("Host"), "some.host.com"); + } else if(method.equals("GET")) { + assertEquals(request.getMajor(), 1); + assertEquals(request.getMinor(), 0); + assertEquals(request.getValue("Host"), "some.host.com"); + assertEquals(request.getValues("Accept").size(), 4); + assertEquals(request.getValues("Accept").get(0), "image/gif"); + assertEquals(request.getValues("Accept").get(1), "image/png"); + assertEquals(request.getValues("Accept").get(2), "image/jpeg"); + assertEquals(request.getValues("Accept").get(3), "*"); + } else { + assertEquals(request.getMajor(), 1); + assertEquals(request.getMinor(), 0); + assertEquals(request.getContentType().getPrimary(), "multipart"); + assertEquals(request.getContentType().getSecondary(), "form-data"); + assertEquals(request.getValue("Host"), "some.host.com"); + assertEquals(request.getValues("Accept").size(), 4); + assertEquals(request.getValues("Accept").get(0), "image/gif"); + assertEquals(request.getValues("Accept").get(1), "image/png"); + assertEquals(request.getValues("Accept").get(2), "image/jpeg"); + assertEquals(request.getValues("Accept").get(3), "*"); + assertEquals(list.size(), 4); + assertEquals(list.get(0).getContentType().getPrimary(), "text"); + assertEquals(list.get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.get(0).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file1.txt\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\""); + assertEquals(list.get(0).getName(), "pics"); + assertEquals(list.get(0).getFileName(), "file1.txt"); + assertEquals(list.get(0).isFile(), true); + assertEquals(list.get(1).getContentType().getPrimary(), "text"); + assertEquals(list.get(1).getContentType().getSecondary(), "plain"); + assertEquals(list.get(1).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file2.txt\""); + assertEquals(list.get(1).getContentType().getPrimary(), "text"); + assertEquals(list.get(1).getName(), "pics"); + assertEquals(list.get(1).getFileName(), "file2.txt"); + assertEquals(list.get(1).isFile(), true); + assertEquals(list.get(2).getContentType().getSecondary(), "plain"); + assertEquals(list.get(2).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file3.txt\""); + assertEquals(list.get(2).getName(), "pics"); + assertEquals(list.get(2).getFileName(), "file3.txt"); + assertEquals(list.get(2).isFile(), true); + assertEquals(list.get(3).getContentType().getPrimary(), "text"); + assertEquals(list.get(3).getContentType().getSecondary(), "plain"); + assertEquals(list.get(3).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file4.txt\""); + assertEquals(list.get(3).getName(), "pics"); + assertEquals(list.get(3).getFileName(), "file4.txt"); + assertEquals(list.get(3).isFile(), true); + } + StopWatch stopWatch = timers.get(request.getTarget()); + stopWatch.stop(); + finished.offer(stopWatch); + } + + public static void main(String[] list) throws Exception { + new ReactorProcessorTest().testMinimal(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorTest.java new file mode 100644 index 0000000..b0aae80 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorTest.java @@ -0,0 +1,178 @@ +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.common.lease.Lease; +import org.simpleframework.http.MockTrace; +import org.simpleframework.http.Part; +import org.simpleframework.http.message.Body; +import org.simpleframework.http.message.Entity; +import org.simpleframework.http.message.Header; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +public class ReactorTest extends TestCase implements Controller { + + private static final String SOURCE = + "POST /index.html HTTP/1.0\r\n"+ + "Content-Type: multipart/form-data; boundary=AaB03x\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n" + + "--AaB03x\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file1.txt\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file1.txt\r\n"+ + "--AaB03x\r\n"+ + "Content-Type: multipart/mixed; boundary=BbC04y\r\n\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file2.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file3.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file3.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"pics\"; filename=\"file4.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y--\r\n"+ + "--AaB03x--\r\n"; + + public static class TestChannel implements Channel { + + private ByteCursor cursor; + + public TestChannel(StreamCursor cursor, int dribble) { + this.cursor = new DribbleCursor(cursor, dribble); + } + + public boolean isSecure() { + return false; + } + + public Trace getTrace() { + return new MockTrace(); + } + + public Certificate getCertificate() { + return null; + } + + public Lease getLease() { + return null; + } + + public ByteCursor getCursor() { + return cursor; + } + + public ByteWriter getWriter() { + return null; + } + + public Map getAttributes() { + return null; + } + + public void close() {} + + public SocketChannel getSocket() { + return null; + } + } + + public void testHandler() throws Exception { + testHandler(1024); + + for(int i = 10; i < 2048; i++) { + testHandler(i); + } + } + + public void testHandler(int dribble) throws Exception { + StreamCursor cursor = new StreamCursor(SOURCE); + Channel channel = new TestChannel(cursor, dribble); + + start(channel); + + assertEquals(cursor.ready(), -1); + } + + public void start(Channel channel) throws IOException { + start(new RequestCollector(new ArrayAllocator(), channel)); + } + + public void start(Collector collector) throws IOException { + collector.collect(this); + } + + public void select(Collector collector) throws IOException { + collector.collect(this); + } + + public void ready(Collector collector) throws IOException { + Entity entity = collector; + Channel channel = entity.getChannel(); + ByteCursor cursor = channel.getCursor(); + Header header = entity.getHeader(); + Body body = entity.getBody(); + List list = body.getParts(); + + assertEquals(header.getTarget(), "/index.html"); + assertEquals(header.getMethod(), "POST"); + assertEquals(header.getMajor(), 1); + assertEquals(header.getMinor(), 0); + assertEquals(header.getContentType().getPrimary(), "multipart"); + assertEquals(header.getContentType().getSecondary(), "form-data"); + assertEquals(header.getValue("Host"), "some.host.com"); + assertEquals(header.getValues("Accept").size(), 4); + assertEquals(header.getValues("Accept").get(0), "image/gif"); + assertEquals(header.getValues("Accept").get(1), "image/png"); + assertEquals(header.getValues("Accept").get(2), "image/jpeg"); + assertEquals(header.getValues("Accept").get(3), "*"); + assertEquals(list.size(), 4); + assertEquals(list.get(0).getContentType().getPrimary(), "text"); + assertEquals(list.get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.get(0).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file1.txt\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\""); + assertEquals(list.get(0).getName(), "pics"); + assertEquals(list.get(0).getFileName(), "file1.txt"); + assertEquals(list.get(0).isFile(), true); + assertEquals(list.get(1).getContentType().getPrimary(), "text"); + assertEquals(list.get(1).getContentType().getSecondary(), "plain"); + assertEquals(list.get(1).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file2.txt\""); + assertEquals(list.get(1).getContentType().getPrimary(), "text"); + assertEquals(list.get(1).getName(), "pics"); + assertEquals(list.get(1).getFileName(), "file2.txt"); + assertEquals(list.get(1).isFile(), true); + assertEquals(list.get(2).getContentType().getSecondary(), "plain"); + assertEquals(list.get(2).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file3.txt\""); + assertEquals(list.get(2).getName(), "pics"); + assertEquals(list.get(2).getFileName(), "file3.txt"); + assertEquals(list.get(2).isFile(), true); + assertEquals(list.get(3).getContentType().getPrimary(), "text"); + assertEquals(list.get(3).getContentType().getSecondary(), "plain"); + assertEquals(list.get(3).getHeader("Content-Disposition"), "file; name=\"pics\"; filename=\"file4.txt\""); + assertEquals(list.get(3).getName(), "pics"); + assertEquals(list.get(3).getFileName(), "file4.txt"); + assertEquals(list.get(3).isFile(), true); + assertEquals(cursor.ready(), -1); + } + + public void stop() throws IOException {} +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java new file mode 100644 index 0000000..ae9672f --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java @@ -0,0 +1,138 @@ +package org.simpleframework.http.core; + +import org.simpleframework.http.message.RequestConsumer; +import org.simpleframework.transport.ByteCursor; + +import junit.framework.TestCase; + +public class RequestConsumerTest extends TestCase { + + private static final byte[] SOURCE_1 = + ("POST /index.html HTTP/1.0\r\n"+ + "Content-Type: application/x-www-form-urlencoded\r\n"+ + "Content-Length: 42\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_2 = + ("GET /tmp/amazon_files/21lP7I1XB5L.jpg HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n" + + "\r\n").getBytes(); + + private static final byte[] SOURCE_3 = + ("GET /tmp/amazon_files/in-your-city-blue-large._V256095983_.gif HTTP/1.1Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_4 = + ("GET /tmp/amazon_files/narrowtimer_transparent._V47062518_.gif HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + public void testPerformance() throws Exception { + testPerformance(SOURCE_1, "/index.html"); + testPerformance(SOURCE_2, "/tmp/amazon_files/21lP7I1XB5L.jpg"); + testPerformance(SOURCE_3, "/tmp/amazon_files/in-your-city-blue-large._V256095983_.gif"); + testPerformance(SOURCE_4, "/tmp/amazon_files/narrowtimer_transparent._V47062518_.gif"); + } + + public void testPerformance(byte[] request, String path) throws Exception { + long start = System.currentTimeMillis(); + + for(int i = 0; i < 10000; i++) { + RequestConsumer header = new RequestConsumer(); + ByteCursor cursor = new StreamCursor(request); + + while(!header.isFinished()) { + header.consume(cursor); + } + + assertEquals(cursor.ready(), -1); + assertEquals(header.getPath().getPath(), path); + } + System.err.printf("%s time=%s%n", path, (System.currentTimeMillis() - start)); + } + + public void testHeader() throws Exception { + long start = System.currentTimeMillis(); + + for(int i = 0; i < 10000; i++) { + RequestConsumer header = new RequestConsumer(); + ByteCursor cursor = new StreamCursor(SOURCE_1); + + while(!header.isFinished()) { + header.consume(cursor); + } + + assertEquals(cursor.ready(), -1); + assertEquals(header.getTarget(), "/index.html"); + assertEquals(header.getMethod(), "POST"); + assertEquals(header.getMajor(), 1); + assertEquals(header.getMinor(), 0); + assertEquals(header.getValue("Content-Length"), "42"); + assertEquals(header.getValue("Content-Type"), "application/x-www-form-urlencoded"); + assertEquals(header.getValue("Host"), "some.host.com"); + assertEquals(header.getValues("Accept").size(), 4); + assertEquals(header.getValues("Accept").get(0), "image/gif"); + assertEquals(header.getValues("Accept").get(1), "image/png"); + assertEquals(header.getValues("Accept").get(2), "image/jpeg"); + assertEquals(header.getValues("Accept").get(3), "*"); + assertEquals(header.getContentType().getPrimary(), "application"); + assertEquals(header.getContentType().getSecondary(), "x-www-form-urlencoded"); + assertEquals(header.getTransferEncoding(), "chunked"); + } + System.err.printf("time=%s%n", (System.currentTimeMillis() - start)); + } + + public void testDribble() throws Exception { + RequestConsumer header = new RequestConsumer(); + ByteCursor cursor = new DribbleCursor(new StreamCursor(SOURCE_1), 1); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + assertEquals(header.getTarget(), "/index.html"); + assertEquals(header.getMethod(), "POST"); + assertEquals(header.getMajor(), 1); + assertEquals(header.getMinor(), 0); + assertEquals(header.getValue("Content-Length"), "42"); + assertEquals(header.getValue("Content-Type"), "application/x-www-form-urlencoded"); + assertEquals(header.getValue("Host"), "some.host.com"); + assertEquals(header.getValues("Accept").size(), 4); + assertEquals(header.getValues("Accept").get(0), "image/gif"); + assertEquals(header.getValues("Accept").get(1), "image/png"); + assertEquals(header.getValues("Accept").get(2), "image/jpeg"); + assertEquals(header.getValues("Accept").get(3), "*"); + assertEquals(header.getContentType().getPrimary(), "application"); + assertEquals(header.getContentType().getSecondary(), "x-www-form-urlencoded"); + assertEquals(header.getTransferEncoding(), "chunked"); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestTest.java new file mode 100644 index 0000000..47cbf35 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestTest.java @@ -0,0 +1,144 @@ +package org.simpleframework.http.core; + +import java.util.List; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.Part; +import org.simpleframework.http.Request; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; + +public class RequestTest extends TestCase { + + private static final String HEADER = + "POST /index.html?a=b&c=d&e=f&g=h&a=1 HTTP/1.0\r\n"+ + "Content-Type: multipart/form-data; boundary=AaB03x\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n"; + + private static final String BODY = + "--AaB03x\r\n"+ + "Content-Disposition: file; name=\"file1\"; filename=\"file1.txt\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file1.txt\r\n"+ + "--AaB03x\r\n"+ + "Content-Type: multipart/mixed; boundary=BbC04y\r\n\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"file2\"; filename=\"file2.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file2.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"file3\"; filename=\"file3.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file3.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: file; name=\"file4\"; filename=\"file4.txt\"\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y--\r\n"+ + "--AaB03x--\r\n"; + + private static final byte[] PAYLOAD = (HEADER + BODY).getBytes(); + + public void testPayload() throws Exception { + long start = System.currentTimeMillis(); + + for(int i = 1; i < 8192; i++) { + testPayload(i); + } + System.err.printf("time=%s%n",(System.currentTimeMillis() - start)); + } + + public void testPerformance() throws Exception { + long start = System.currentTimeMillis(); + + for(int i = 1; i < 10000; i++) { + testPayload(8192); + } + System.err.printf("time=%s%n",(System.currentTimeMillis() - start)); + } + + public void testPayload(int dribble) throws Exception { + System.out.println("Testing dribbling cursor of "+dribble+" ..."); + ByteCursor cursor = new StreamCursor(PAYLOAD); + + if(dribble < PAYLOAD.length) { + cursor = new DribbleCursor(cursor, dribble); + } + Channel channel = new MockChannel(cursor); + MockController selector = new MockController(); + Collector body = new RequestCollector(new ArrayAllocator(), channel); + + while(!selector.isReady()) { + body.collect(selector); + } + Request request = new RequestEntity(null, body); + List list = request.getParts(); + + assertEquals(request.getParameter("a"), "b"); + assertEquals(request.getParameter("c"), "d"); + assertEquals(request.getParameter("e"), "f"); + assertEquals(request.getParameter("g"), "h"); + assertEquals(request.getTarget(), "/index.html?a=b&c=d&e=f&g=h&a=1"); + assertEquals(request.getMethod(), "POST"); + assertEquals(request.getMajor(), 1); + assertEquals(request.getMinor(), 0); + assertEquals(request.getContentType().getPrimary(), "multipart"); + assertEquals(request.getContentType().getSecondary(), "form-data"); + assertEquals(request.getValue("Host"), "some.host.com"); + assertEquals(request.getValues("Accept").size(), 4); + assertEquals(request.getValues("Accept").get(0), "image/gif"); + assertEquals(request.getValues("Accept").get(1), "image/png"); + assertEquals(request.getValues("Accept").get(2), "image/jpeg"); + assertEquals(request.getValues("Accept").get(3), "*"); + assertEquals(request.getCookie("UID").getValue(), "1234-5678"); + assertEquals(request.getCookie("UID").getPath(), "/"); + assertEquals(request.getCookie("UID").getDomain(), ".host.com"); + assertEquals(request.getCookie("NAME").getValue(), "Niall Gallagher"); + assertEquals(request.getCookie("NAME").getPath(), "/"); + assertEquals(request.getCookie("NAME").getDomain(), null); + assertEquals(list.size(), 4); + assertEquals(list.get(0).getContentType().getPrimary(), "text"); + assertEquals(list.get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.get(0).getHeader("Content-Disposition"), "file; name=\"file1\"; filename=\"file1.txt\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\""); + assertEquals(list.get(0).getName(), "file1"); + assertEquals(list.get(0).getFileName(), "file1.txt"); + assertEquals(list.get(0).isFile(), true); + assertEquals(list.get(0).getContent(), "example contents of file1.txt"); + assertEquals(request.getPart("file1").getContent(), "example contents of file1.txt"); + assertEquals(list.get(1).getContentType().getPrimary(), "text"); + assertEquals(list.get(1).getContentType().getSecondary(), "plain"); + assertEquals(list.get(1).getHeader("Content-Disposition"), "file; name=\"file2\"; filename=\"file2.txt\""); + assertEquals(list.get(1).getContentType().getPrimary(), "text"); + assertEquals(list.get(1).getName(), "file2"); + assertEquals(list.get(1).getFileName(), "file2.txt"); + assertEquals(list.get(1).isFile(), true); + assertEquals(list.get(1).getContent(), "example contents of file2.txt ..."); + assertEquals(request.getPart("file2").getContent(), "example contents of file2.txt ..."); + assertEquals(list.get(2).getContentType().getSecondary(), "plain"); + assertEquals(list.get(2).getHeader("Content-Disposition"), "file; name=\"file3\"; filename=\"file3.txt\""); + assertEquals(list.get(2).getName(), "file3"); + assertEquals(list.get(2).getFileName(), "file3.txt"); + assertEquals(list.get(2).isFile(), true); + assertEquals(list.get(2).getContent(), "example contents of file3.txt ..."); + assertEquals(request.getPart("file3").getContent(), "example contents of file3.txt ..."); + assertEquals(list.get(3).getContentType().getPrimary(), "text"); + assertEquals(list.get(3).getContentType().getSecondary(), "plain"); + assertEquals(list.get(3).getHeader("Content-Disposition"), "file; name=\"file4\"; filename=\"file4.txt\""); + assertEquals(list.get(3).getName(), "file4"); + assertEquals(list.get(3).getFileName(), "file4.txt"); + assertEquals(list.get(3).isFile(), true); + assertEquals(list.get(3).getContent(), "example contents of file4.txt ..."); + assertEquals(request.getPart("file4").getContent(), "example contents of file4.txt ..."); + assertEquals(cursor.ready(), -1); + assertEquals(request.getContent(), BODY); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/Result.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/Result.java new file mode 100644 index 0000000..c48b248 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/Result.java @@ -0,0 +1,37 @@ +package org.simpleframework.http.core; + +import java.util.List; +import java.util.Map; + +import org.simpleframework.http.Cookie; + +class Result { + + private List cookies; + private String response; + private byte[] body; + private Map map; + + public Result(String response, byte[] body, Map map, List cookies) { + this.cookies = cookies; + this.response = response; + this.body = body; + this.map = map; + } + + public List getCookies() { + return cookies; + } + + public byte[] getBody() { + return body; + } + + public String getResponse() throws Exception { + return response; + } + + public Map getMap() { + return map; + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/StopTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/StopTest.java new file mode 100644 index 0000000..67751b8 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/StopTest.java @@ -0,0 +1,176 @@ +package org.simpleframework.http.core; + +import java.io.Closeable; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.net.InetSocketAddress; +import java.net.URL; +import java.net.URLConnection; +import java.util.Date; + +import junit.framework.TestCase; + +import org.simpleframework.common.thread.ConcurrentExecutor; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; + +public class StopTest extends TestCase { + + private static final int ITERATIONS = 20; + + public void testStop() throws Exception { + ThreadDumper dumper = new ThreadDumper(); + + dumper.start(); + dumper.waitUntilStarted(); + + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + int initialThreads = threadBean.getThreadCount(); + + for(int i = 0; i < ITERATIONS; i++) { + try { + ServerCriteria criteria = createServer(); + InetSocketAddress address = criteria.getAddress(); + Connection connection = criteria.getConnection(); + Client client = createClient(address, String.format("[%s of %s]", i, ITERATIONS)); + + Thread.sleep(2000); // allow some requests to execute + connection.close(); + Thread.sleep(100); // ensure client keeps executing + client.close(); + Thread.sleep(1000); // wait for threads to terminate + }catch(Exception e) { + e.printStackTrace(); + } + //assertEquals(initialThreads, threadBean.getThreadCount()); + } + dumper.kill(); + } + + public static Client createClient(InetSocketAddress address, String tag) throws Exception { + ConcurrentExecutor executor = new ConcurrentExecutor(Runnable.class, 20); + int port = address.getPort(); + Client client = new Client(executor, port, tag); + + client.start(); + return client; + } + + public static ServerCriteria createServer() throws Exception { + Container container = new Container() { + public void handle(Request request, Response response) { + try { + PrintStream out = response.getPrintStream(); + response.setValue("Content-Type", "text/plain"); + response.setValue("Connection", "close"); + + out.print("TEST " + new Date()); + response.close(); + }catch(Exception e) { + e.printStackTrace(); + try { + response.close(); + }catch(Exception ex) { + ex.printStackTrace(); + } + } + } + }; + ContainerSocketProcessor server = new ContainerSocketProcessor(container); + Connection connection = new SocketConnection(server); + InetSocketAddress address = (InetSocketAddress)connection.connect(null); // ephemeral port + + return new ServerCriteria(connection, address); + } + + private static class Client extends Thread implements Closeable { + + private ConcurrentExecutor executor; + private RequestTask task; + private volatile boolean dead; + + public Client(ConcurrentExecutor executor, int port, String tag) { + this.task = new RequestTask(this, port, tag); + this.executor = executor; + } + + public boolean isDead() { + return dead; + } + + public void run() { + try { + while(!dead) { + executor.execute(task); + Thread.sleep(100); + } + }catch(Exception e) { + e.printStackTrace(); + } + } + + public void close() { + dead = true; + executor.stop(); + } + } + + private static class RequestTask implements Runnable { + + private Client client; + private String tag; + private int port; + + public RequestTask(Client client, int port, String tag) { + this.client = client; + this.port = port; + this.tag = tag; + } + + public void run() { + try { + if(!client.isDead()) { + URL target = new URL("http://localhost:"+port+"/"); + URLConnection connection = target.openConnection(); + + // set a timeout + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); + + InputStream stream = connection.getInputStream(); + StringBuilder builder = new StringBuilder(); + int octet = 0; + + while((octet = stream.read()) != -1) { + builder.append((char)octet); + } + stream.close(); + System.out.println(tag + " " + Thread.currentThread() + ": " + builder); + } + }catch(Exception e) { + e.printStackTrace(); + } + } + } + + private static class ServerCriteria { + + private Connection connection; + private InetSocketAddress address; + + public ServerCriteria(Connection connection, InetSocketAddress address){ + this.connection = connection; + this.address = address; + } + public Connection getConnection() { + return connection; + } + public InetSocketAddress getAddress() { + return address; + } + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/StreamCursor.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/StreamCursor.java new file mode 100644 index 0000000..d6f6a09 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/StreamCursor.java @@ -0,0 +1,74 @@ +package org.simpleframework.http.core; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.simpleframework.http.StreamTransport; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.Transport; +import org.simpleframework.transport.TransportCursor; + +public class StreamCursor implements ByteCursor { + + private TransportCursor cursor; + private Transport transport; + private byte[] swap; + + public StreamCursor(String source) throws IOException { + this(source.getBytes("UTF-8")); + } + + public StreamCursor(byte[] data) throws IOException { + this(new ByteArrayInputStream(data)); + } + + public StreamCursor(InputStream source) throws IOException { + this.transport = new StreamTransport(source, new OutputStream() { + public void write(int octet){} + }); + this.cursor = new TransportCursor(transport); + this.swap = new byte[1]; + } + + // TODO investigate this + public boolean isOpen() throws IOException { + return true; + } + + public boolean isReady() throws IOException { + return cursor.isReady(); + } + + public int ready() throws IOException { + return cursor.ready(); + } + + public int read() throws IOException { + if(read(swap) > 0) { + return swap[0] & 0xff; + } + return 0; + } + + public int read(byte[] data) throws IOException { + return read(data, 0, data.length); + } + + public int read(byte[] data, int off, int len) throws IOException { + return cursor.read(data, off, len); + } + + public int reset(int len) throws IOException { + return cursor.reset(len); + } + + public void push(byte[] data) throws IOException { + push(data, 0, data.length); + } + + public void push(byte[] data, int off, int len) throws IOException { + cursor.push(data, off, len); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/ThreadDumper.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/ThreadDumper.java new file mode 100644 index 0000000..85960ed --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/ThreadDumper.java @@ -0,0 +1,183 @@ +package org.simpleframework.http.core; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + + +public class ThreadDumper extends Thread { + + private static String INDENT = " "; + private CountDownLatch latch; + private volatile boolean dead; + private int wait; + + public ThreadDumper() { + this(10000); + } + + public ThreadDumper(int wait) { + this.latch = new CountDownLatch(1); + this.wait = wait; + } + + public void waitUntilStarted() throws InterruptedException{ + latch.await(); + } + + public void kill(){ + try { + Thread.sleep(1000); + dead = true; + dumpThreadInfo(); + }catch(Exception e){ + e.printStackTrace(); + } + } + public void run() { + while(!dead) { + try{ + latch.countDown(); + dumpThreadInfo(); + findDeadlock(); + Thread.sleep(wait); + }catch(Exception e){ + e.printStackTrace(); + } + } + } + public String dumpThreads() { + Map stackTraces = Thread.getAllStackTraces(); + return generateDump(stackTraces); + } + + public static String dumpCurrentThread() { + Thread currentThread = Thread.currentThread(); + StackTraceElement[] stackTrace = currentThread.getStackTrace(); + Map stackTraces = Collections.singletonMap(currentThread, stackTrace); + return generateDump(stackTraces); + + } + + private static String generateDump(Map stackTraces) { + StringBuilder builder = new StringBuilder(); + + builder.append("

");
+      builder.append("Full Java thread dump");
+      builder.append("\n");
+
+      Set threads = stackTraces.keySet();
+
+      for (Thread thread : threads) {
+         StackTraceElement[] stackElements = stackTraces.get(thread);
+
+         generateDescription(thread, builder);
+         generateStackFrames(stackElements, builder);
+      }
+      builder.append("
"); + return builder.toString(); + } + + private static void generateStackFrames(StackTraceElement[] stackElements, StringBuilder builder) { + for (StackTraceElement stackTraceElement : stackElements) { + builder.append(" at "); + builder.append(stackTraceElement); + builder.append("\n"); + } + } + + private static void generateDescription(Thread thread, StringBuilder builder) { + Thread.State threadState = thread.getState(); + String threadName = thread.getName(); + long threadId = thread.getId(); + + builder.append("\n"); + builder.append(""); + builder.append(threadName); + builder.append(" Id="); + builder.append(threadId); + builder.append(" in "); + builder.append(threadState); + builder.append("\n"); + } + + /** + * Prints the thread dump information to System.out. + */ + public static void dumpThreadInfo(){ + System.out.println(getThreadInfo()); + } + + public static String getThreadInfo() { + ThreadMXBean tmbean = ManagementFactory.getThreadMXBean(); + long[] tids = tmbean.getAllThreadIds(); + ThreadInfo[] tinfos = tmbean.getThreadInfo(tids, Integer.MAX_VALUE); + StringWriter str = new StringWriter(); + PrintWriter log = new PrintWriter(str); + log.println("Full Java thread dump"); + + for (ThreadInfo ti : tinfos) { + printThreadInfo(ti, log); + } + log.flush(); + return str.toString(); + } + + private static void printThreadInfo(ThreadInfo ti, PrintWriter log) { + if(ti != null) { + StringBuilder sb = new StringBuilder("\"" + ti.getThreadName() + "\"" + + " Id=" + ti.getThreadId() + + " in " + ti.getThreadState()); + if (ti.getLockName() != null) { + sb.append(" on lock=" + ti.getLockName()); + } + if (ti.isSuspended()) { + sb.append(" (suspended)"); + } + if (ti.isInNative()) { + sb.append(" (running in native)"); + } + log.println(sb.toString()); + if (ti.getLockOwnerName() != null) { + log.println(INDENT + " owned by " + ti.getLockOwnerName() + + " Id=" + ti.getLockOwnerId()); + } + for (StackTraceElement ste : ti.getStackTrace()) { + log.println(INDENT + "at " + ste.toString()); + } + log.println(); + } + } + + /** + * Checks if any threads are deadlocked. If any, print + * the thread dump information. + */ + public static boolean findDeadlock() { + ThreadMXBean tmbean = ManagementFactory.getThreadMXBean(); + long[] tids = tmbean.findMonitorDeadlockedThreads(); + if (tids == null) { + return false; + } else { + StringWriter str = new StringWriter(); + PrintWriter log = new PrintWriter(str); + + tids = tmbean.getAllThreadIds(); + System.out.println("Deadlock found :-"); + ThreadInfo[] tinfos = tmbean.getThreadInfo(tids, Integer.MAX_VALUE); + for (ThreadInfo ti : tinfos) { + printThreadInfo(ti, log); + } + log.flush(); + System.out.println(str.toString()); + return true; + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/Ticket.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/Ticket.java new file mode 100644 index 0000000..60fcb5c --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/Ticket.java @@ -0,0 +1,22 @@ +package org.simpleframework.http.core; + +public class Ticket { + + public static final Class KEY = Ticket.class; + + private final String ticket; + private final int port; + public Ticket(int port) { + this.ticket = String.valueOf(port); + this.port = port; + } + + public int getPort() { + return port; + } + + public String getTicket() { + return ticket; + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/TicketProcessor.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/TicketProcessor.java new file mode 100644 index 0000000..4636cc7 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/TicketProcessor.java @@ -0,0 +1,28 @@ +package org.simpleframework.http.core; + +import java.io.IOException; +import java.nio.channels.SocketChannel; + +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.Socket; + +class TicketProcessor implements SocketProcessor { + + private SocketProcessor delegate; + + public TicketProcessor(SocketProcessor delegate) { + this.delegate = delegate; + } + + public void process(Socket pipe) throws IOException { + SocketChannel channel = pipe.getChannel(); + int port = channel.socket().getPort(); + + pipe.getAttributes().put(Ticket.KEY,new Ticket(port)); + delegate.process(pipe); + } + + public void stop() throws IOException { + delegate.stop(); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/TransferTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/TransferTest.java new file mode 100644 index 0000000..0d0d73d --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/TransferTest.java @@ -0,0 +1,195 @@ +package org.simpleframework.http.core; + +import java.io.IOException; + +import org.simpleframework.http.core.Conversation; +import org.simpleframework.http.core.ResponseEncoder; + +import junit.framework.TestCase; + +public class TransferTest extends TestCase { + + public void testTransferEncoding() throws IOException { + MockChannel channel = new MockChannel(null); + MockObserver monitor = new MockObserver(); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + Conversation support = new Conversation(request, response); + ResponseEncoder transfer = new ResponseEncoder(monitor, response, support, channel); + + // Start a HTTP/1.1 conversation + request.setMajor(1); + request.setMinor(1); + transfer.start(); + + assertEquals(response.getValue("Connection"), "keep-alive"); + assertEquals(response.getValue("Transfer-Encoding"), "chunked"); + assertEquals(response.getValue("Content-Length"), null); + assertEquals(response.getContentLength(), -1); + assertTrue(response.isCommitted()); + + channel = new MockChannel(null); + monitor = new MockObserver(); + request = new MockRequest(); + response = new MockResponse(); + support = new Conversation(request, response); + transfer = new ResponseEncoder(monitor, response, support, channel); + + // Start a HTTP/1.0 conversation + request.setMajor(1); + request.setMinor(0); + transfer.start(); + + assertEquals(response.getValue("Connection"), "close"); + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getValue("Content-Length"), null); + assertEquals(response.getContentLength(), -1); + assertTrue(response.isCommitted()); + } + + public void testContentLength() throws IOException { + MockChannel channel = new MockChannel(null); + MockObserver monitor = new MockObserver(); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + Conversation support = new Conversation(request, response); + ResponseEncoder transfer = new ResponseEncoder(monitor, response, support, channel); + + // Start a HTTP/1.1 conversation + request.setMajor(1); + request.setMinor(1); + transfer.start(1024); + + assertEquals(response.getValue("Connection"), "keep-alive"); + assertEquals(response.getValue("Content-Length"), "1024"); + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getContentLength(), 1024); + assertTrue(response.isCommitted()); + + channel = new MockChannel(null); + monitor = new MockObserver(); + request = new MockRequest(); + response = new MockResponse(); + support = new Conversation(request, response); + transfer = new ResponseEncoder(monitor, response, support, channel); + + // Start a HTTP/1.0 conversation + request.setMajor(1); + request.setMinor(0); + transfer.start(1024); + + assertEquals(response.getValue("Connection"), "close"); + assertEquals(response.getValue("Content-Length"), "1024"); + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getContentLength(), 1024); + assertTrue(response.isCommitted()); + + channel = new MockChannel(null); + monitor = new MockObserver(); + request = new MockRequest(); + response = new MockResponse(); + support = new Conversation(request, response); + transfer = new ResponseEncoder(monitor, response, support, channel); + + // Start a HTTP/1.0 conversation + request.setMajor(1); + request.setMinor(1); + response.setValue("Content-Length", "2048"); + response.setValue("Connection", "close"); + response.setValue("Transfer-Encoding", "chunked"); + transfer.start(1024); + + assertEquals(response.getValue("Connection"), "close"); + assertEquals(response.getValue("Content-Length"), "1024"); // should be 1024 + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getContentLength(), 1024); + assertTrue(response.isCommitted()); + } + + public void testHeadMethodWithConnectionClose() throws IOException { + MockChannel channel = new MockChannel(null); + MockObserver monitor = new MockObserver(); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + Conversation support = new Conversation(request, response); + ResponseEncoder transfer = new ResponseEncoder(monitor, response, support, channel); + + request.setMajor(1); + request.setMinor(0); + request.setMethod("HEAD"); + request.setValue("Connection", "keep-alive"); + response.setContentLength(1024); + response.setValue("Connection", "close"); + + transfer.start(); + + assertEquals(response.getValue("Connection"), "close"); + assertEquals(response.getValue("Content-Length"), "1024"); // should be 1024 + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getContentLength(), 1024); + } + + public void testHeadMethodWithSomethingWritten() throws IOException { + MockChannel channel = new MockChannel(null); + MockObserver monitor = new MockObserver(); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + Conversation support = new Conversation(request, response); + ResponseEncoder transfer = new ResponseEncoder(monitor, response, support, channel); + + request.setMajor(1); + request.setMinor(1); + request.setMethod("HEAD"); + request.setValue("Connection", "keep-alive"); + response.setContentLength(1024); + + transfer.start(512); + + assertEquals(response.getValue("Connection"), "keep-alive"); + assertEquals(response.getValue("Content-Length"), "512"); // should be 512 + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getContentLength(), 512); + } + + public void testHeadMethodWithNoContentLength() throws IOException { + MockChannel channel = new MockChannel(null); + MockObserver monitor = new MockObserver(); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + Conversation support = new Conversation(request, response); + ResponseEncoder transfer = new ResponseEncoder(monitor, response, support, channel); + + request.setMajor(1); + request.setMinor(1); + request.setMethod("HEAD"); + request.setValue("Connection", "keep-alive"); + + transfer.start(); + + assertEquals(response.getValue("Connection"), "keep-alive"); + assertEquals(response.getValue("Content-Length"), null); + assertEquals(response.getValue("Transfer-Encoding"), "chunked"); + assertEquals(response.getContentLength(), -1); + } + + public void testHeadMethodWithNoContentLengthAndSomethingWritten() throws IOException { + MockChannel channel = new MockChannel(null); + MockObserver monitor = new MockObserver(); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + Conversation support = new Conversation(request, response); + ResponseEncoder transfer = new ResponseEncoder(monitor, response, support, channel); + + request.setMajor(1); + request.setMinor(1); + request.setMethod("HEAD"); + request.setValue("Connection", "keep-alive"); + + transfer.start(32); + + assertEquals(response.getValue("Connection"), "keep-alive"); + assertEquals(response.getValue("Content-Length"), "32"); + assertEquals(response.getValue("Transfer-Encoding"), null); + assertEquals(response.getContentLength(), 32); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/WebSocketUpgradeTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/core/WebSocketUpgradeTest.java new file mode 100644 index 0000000..ea6e313 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/core/WebSocketUpgradeTest.java @@ -0,0 +1,126 @@ +package org.simpleframework.http.core; + +import java.io.OutputStream; +import java.nio.channels.SocketChannel; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.MockTrace; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.transport.Certificate; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +public class WebSocketUpgradeTest extends TestCase implements Container { + + private static final String OPEN_HANDSHAKE = + "GET /chat HTTP/1.1\r\n"+ + "Host: server.example.com\r\n"+ + "Upgrade: websocket\r\n"+ + "Connection: Upgrade\r\n"+ + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"+ + "Origin: http://example.com\r\n"+ + "Sec-WebSocket-Protocol: chat, superchat\r\n"+ + "Sec-WebSocket-Version: 14\r\n" + + "\r\n"; + + public static class MockChannel implements Channel { + + private ByteCursor cursor; + + public MockChannel(StreamCursor cursor, int dribble) { + this.cursor = new DribbleCursor(cursor, dribble); + } + public boolean isSecure() { + return false; + } + + public Trace getTrace() { + return new MockTrace(); + } + + public Certificate getCertificate() { + return null; + } + + public ByteCursor getCursor() { + return cursor; + } + + public ByteWriter getWriter() { + return new MockSender(); + } + + public Map getAttributes() { + return null; + } + + public void close() {} + + public SocketChannel getSocket() { + return null; + } + } + + private final BlockingQueue responses = new LinkedBlockingQueue(); + + public void testWebSocketUpgrade() throws Exception { + Allocator allocator = new ArrayAllocator(); + Controller handler = new ContainerController(this, allocator, 10, 2); + StreamCursor cursor = new StreamCursor(OPEN_HANDSHAKE); + Channel channel = new MockChannel(cursor, 10); + + handler.start(channel); + + Response response = responses.poll(5000, TimeUnit.MILLISECONDS); + + assertEquals(response.getValue("Connection"), "Upgrade"); + assertEquals(response.getValue("Upgrade"), "websocket"); + assertTrue(response.isCommitted()); + assertTrue(response.isKeepAlive()); + } + + public void handle(Request request, Response response) { + try { + process(request, response); + responses.offer(response); + }catch(Exception e) { + e.printStackTrace(); + assertTrue(false); + } + } + + public void process(Request request, Response response) throws Exception { + String method = request.getMethod(); + + assertEquals(method, "GET"); + assertEquals(request.getValue("Upgrade"), "websocket"); + assertEquals(request.getValue("Connection"), "Upgrade"); + assertEquals(request.getValue("Sec-WebSocket-Key"), "dGhlIHNhbXBsZSBub25jZQ=="); + + response.setCode(101); + response.setValue("Connection", "close"); + response.setValue("Upgrade", "websocket"); + + OutputStream out = response.getOutputStream(); + + out.write(10); // force commit + + assertTrue(response.isCommitted()); + assertTrue(response.isKeepAlive()); + } + + public static void main(String[] list) throws Exception { + new ReactorProcessorTest().testMinimal(); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/BoundaryConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/BoundaryConsumerTest.java new file mode 100644 index 0000000..8f52100 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/BoundaryConsumerTest.java @@ -0,0 +1,77 @@ +package org.simpleframework.http.message; + +import java.io.ByteArrayInputStream; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.message.BoundaryConsumer; + +public class BoundaryConsumerTest extends TestCase { + + private static final byte[] TERMINAL = { '-', '-', 'A', 'a', 'B', '0', '3', 'x', '-', '-', '\r', '\n', 'X', 'Y' }; + + private static final byte[] NORMAL = { '-', '-', 'A', 'a', 'B', '0', '3', 'x', '\r', '\n', 'X', 'Y' }; + + private static final byte[] BOUNDARY = { 'A', 'a', 'B', '0', '3', 'x' }; + + private BoundaryConsumer boundary; + + public void setUp() { + boundary = new BoundaryConsumer(new ArrayAllocator(), BOUNDARY); + } + + public void testBoundary() throws Exception { + StreamCursor cursor = new StreamCursor(new ByteArrayInputStream(NORMAL)); + + while(!boundary.isFinished()) { + boundary.consume(cursor); + } + assertEquals(cursor.read(), 'X'); + assertEquals(cursor.read(), 'Y'); + assertTrue(boundary.isFinished()); + assertFalse(boundary.isEnd()); + assertFalse(cursor.isReady()); + } + + public void testTerminal() throws Exception { + StreamCursor cursor = new StreamCursor(new ByteArrayInputStream(TERMINAL)); + + while(!boundary.isFinished()) { + boundary.consume(cursor); + } + assertEquals(cursor.read(), 'X'); + assertEquals(cursor.read(), 'Y'); + assertTrue(boundary.isFinished()); + assertTrue(boundary.isEnd()); + assertFalse(cursor.isReady()); + } + + public void testDribble() throws Exception { + DribbleCursor cursor = new DribbleCursor(new StreamCursor(new ByteArrayInputStream(TERMINAL)), 3); + + while(!boundary.isFinished()) { + boundary.consume(cursor); + } + assertEquals(cursor.read(), 'X'); + assertEquals(cursor.read(), 'Y'); + assertTrue(boundary.isFinished()); + assertTrue(boundary.isEnd()); + assertFalse(cursor.isReady()); + + boundary.clear(); + + cursor = new DribbleCursor(new StreamCursor(new ByteArrayInputStream(TERMINAL)), 1); + + while(!boundary.isFinished()) { + boundary.consume(cursor); + } + assertEquals(cursor.read(), 'X'); + assertEquals(cursor.read(), 'Y'); + assertTrue(boundary.isFinished()); + assertTrue(boundary.isEnd()); + assertFalse(cursor.isReady()); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/ChunkedConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/ChunkedConsumerTest.java new file mode 100644 index 0000000..3b86020 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/ChunkedConsumerTest.java @@ -0,0 +1,118 @@ +package org.simpleframework.http.message; + + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.common.buffer.ArrayBuffer; +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.http.core.Chunker; +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.message.ChunkedConsumer; + +public class ChunkedConsumerTest extends TestCase implements Allocator { + + public Buffer buffer; + + public void setUp() { + buffer = new ArrayBuffer(); + } + + public Buffer allocate() { + return buffer; + } + + public Buffer allocate(long size) { + return buffer; + } + + public void testChunks() throws Exception { + testChunks(64, 1024, 64); + testChunks(64, 11, 64); + testChunks(1024, 1024, 100000); + testChunks(1024, 10, 100000); + testChunks(1024, 11, 100000); + testChunks(1024, 113, 100000); + testChunks(1024, 1, 100000); + testChunks(1024, 2, 50000); + testChunks(1024, 3, 50000); + testChunks(10, 1024, 50000); + testChunks(1, 10, 71234); + testChunks(2, 11, 123456); + testChunks(15, 113, 25271); + testChunks(16, 1, 43265); + testChunks(64, 2, 63266); + testChunks(32, 3, 9203); + } + + public void testChunks(int chunkSize, int dribble, int entitySize) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream plain = new ByteArrayOutputStream(); + Chunker encode = new Chunker(out); + StringBuffer buf = new StringBuffer(); + int fill = 0; + + for(int i = 0, line = 0; i < entitySize; i++) { + String text = "["+String.valueOf(i)+"]"; + + if(fill >= chunkSize) { + encode.write(buf.toString().getBytes("UTF-8")); + plain.write(buf.toString().getBytes("UTF-8")); + buf.setLength(0); + fill = 0; + line = 0; + } + line += text.length(); + fill += text.length(); + buf.append(text); + + if(line >= 48) { + buf.append("\n"); + fill++; + line = 0; + } + + } + if(buf.length() > 0) { + encode.write(buf.toString().getBytes("UTF-8")); + plain.write(buf.toString().getBytes("UTF-8")); + } + buffer = new ArrayAllocator().allocate(); // N.B clear previous buffer + encode.close(); + byte[] data = out.toByteArray(); + byte[] plainText = plain.toByteArray(); + //System.out.println(">>"+new String(data, 0, data.length, "UTF-8")+"<<"); + //System.out.println("}}"+new String(plainText, 0, plainText.length,"UTF-8")+"{{"); + DribbleCursor cursor = new DribbleCursor(new StreamCursor(new ByteArrayInputStream(data)), dribble); + ChunkedConsumer test = new ChunkedConsumer(this); + + while(!test.isFinished()) { + test.consume(cursor); + } + byte[] result = buffer.encode("UTF-8").getBytes("UTF-8"); + //System.out.println("))"+new String(result, 0, result.length, "UTF-8")+"(("); + + if(result.length != plainText.length) { + throw new IOException(String.format("Bad encoding result=[%s] plainText=[%s]", result.length, plainText.length)); + } + for(int i = 0; i < result.length; i++) { + if(result[i] != plainText[i]) { + throw new IOException(String.format("Values do not match for %s, %s, and %s", chunkSize, dribble, entitySize)); + } + } + } + + public void close() throws IOException { + // TODO Auto-generated method stub + + } + + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/ContentConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/ContentConsumerTest.java new file mode 100644 index 0000000..a6f4f62 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/ContentConsumerTest.java @@ -0,0 +1,99 @@ +package org.simpleframework.http.message; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.common.buffer.Buffer; +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.message.ContentConsumer; +import org.simpleframework.http.message.PartData; + +public class ContentConsumerTest extends TestCase implements Allocator { + + private static final byte[] BOUNDARY = { 'A', 'a', 'B', '0', '3', 'x' }; + + private Buffer buffer; + + public Buffer allocate() { + return buffer; + } + + public Buffer allocate(long size) { + return buffer; + } + + public void testContent() throws Exception { + testContent(1, 1); + + for(int i = 1; i < 1000; i++) { + testContent(i, i); + } + for(int i = 20; i < 1000; i++) { + for(int j = 1; j < 19; j++) { + testContent(i, j); + } + } + testContent(10, 10); + testContent(100, 2); + } + + public void testContent(int entitySize, int dribble) throws Exception { + MockSegment segment = new MockSegment(); + PartData list = new PartData(); + ContentConsumer consumer = new ContentConsumer(this, segment, list, BOUNDARY); + StringBuffer buf = new StringBuffer(); + + segment.add("Content-Disposition", "form-data; name='photo'; filename='photo.jpg'"); + segment.add("Content-Type", "text/plain"); + segment.add("Content-ID", ""); + + for(int i = 0, line = 0; buf.length() < entitySize; i++) { + String text = String.valueOf(i); + + line += text.length(); + buf.append(text); + + if(line >= 48) { + buf.append("\n"); + line = 0; + } + } + // Get request body without boundary + String requestBody = buf.toString(); + + // Add the boundary to the request body + buf.append("\r\n--"); + buf.append(new String(BOUNDARY, 0, BOUNDARY.length, "UTF-8")); + buffer = new ArrayAllocator().allocate(); + + DribbleCursor cursor = new DribbleCursor(new StreamCursor(buf.toString()), dribble); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + byte[] consumedBytes = buffer.encode("UTF-8").getBytes("UTF-8"); + String consumedBody = new String(consumedBytes, 0, consumedBytes.length, "UTF-8"); + + assertEquals(String.format("Failed for entitySize=%s and dribble=%s", entitySize, dribble), consumedBody, requestBody); + assertEquals(cursor.read(), '\r'); + assertEquals(cursor.read(), '\n'); + assertEquals(cursor.read(), '-'); + assertEquals(cursor.read(), '-'); + assertEquals(cursor.read(), BOUNDARY[0]); + assertEquals(cursor.read(), BOUNDARY[1]); + assertEquals(consumer.getPart().getContentType().getPrimary(), "text"); + assertEquals(consumer.getPart().getContentType().getSecondary(), "plain"); + } + + public void close() throws IOException { + // TODO Auto-generated method stub + + } + + + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/FileUploadConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/FileUploadConsumerTest.java new file mode 100644 index 0000000..d668a4a --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/FileUploadConsumerTest.java @@ -0,0 +1,86 @@ +package org.simpleframework.http.message; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.transport.ByteCursor; + +public class FileUploadConsumerTest extends TestCase { + + private static final String SOURCE = + "--mxvercagiykxaqsdvrfabfhfpaseejrg\r\n"+ + "Content-Disposition: form-data; name=\"fn\"\r\n"+ + "\r\n"+ + "blah_niall\r\n"+ + "--mxvercagiykxaqsdvrfabfhfpaseejrg\r\n"+ + "Content-Disposition: form-data; name=\"Filename\"\r\n"+ + "\r\n"+ + "content\r\n"+ + "--mxvercagiykxaqsdvrfabfhfpaseejrg\r\n"+ + "Content-Disposition: form-data; name=\"Filedata[]\"; filename=\"content\"\r\n"+ + "Content-Type: application/octet-stream\r\n"+ + "\r\n"+ + "\r\n"+ + "--mxvercagiykxaqsdvrfabfhfpaseejrg\r\n"+ + "Content-Disposition: form-data; name=\"Filename\"\r\n"+ + "\r\n"+ + "image\r\n"+ + "--mxvercagiykxaqsdvrfabfhfpaseejrg\r\n"+ + "Content-Disposition: form-data; name=\"Filedata[]\"; filename=\"image\"\r\n"+ + "Content-Type: application/octet-stream\r\n"+ + "\r\n"+ + "PNG"+ + "\r\n"+ + "--mxvercagiykxaqsdvrfabfhfpaseejrg\r\n"+ + "Content-Disposition: form-data; name=\"Upload\"\r\n"+ + "\r\n"+ + "Submit Query\r\n"+ + "--mxvercagiykxaqsdvrfabfhfpaseejrg--"; + + public void testNoFinalCRLF() throws Exception { + byte[] data = SOURCE.getBytes("UTF-8"); + byte[] boundary = "mxvercagiykxaqsdvrfabfhfpaseejrg".getBytes("UTF-8"); + Allocator allocator = new ArrayAllocator(); + FileUploadConsumer consumer = new FileUploadConsumer(allocator, boundary, data.length); + ByteCursor cursor = new StreamCursor(data); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + assertEquals(consumer.getBody().getContent(), SOURCE); + assertEquals(consumer.getBody().getParts().size(), 6); + } + + public void testNoFinalCRLSWithDribble() throws Exception { + byte[] data = SOURCE.getBytes("UTF-8"); + byte[] boundary = "mxvercagiykxaqsdvrfabfhfpaseejrg".getBytes("UTF-8"); + Allocator allocator = new ArrayAllocator(); + FileUploadConsumer consumer = new FileUploadConsumer(allocator, boundary, data.length); + ByteCursor cursor = new StreamCursor(data); + DribbleCursor dribble = new DribbleCursor(cursor, 1); + + while(!consumer.isFinished()) { + consumer.consume(dribble); + } + assertEquals(consumer.getBody().getContent(), SOURCE); + assertEquals(consumer.getBody().getParts().size(), 6); + } + + public void testNoFinalCRLSWithDribble3() throws Exception { + byte[] data = SOURCE.getBytes("UTF-8"); + byte[] boundary = "mxvercagiykxaqsdvrfabfhfpaseejrg".getBytes("UTF-8"); + Allocator allocator = new ArrayAllocator(); + FileUploadConsumer consumer = new FileUploadConsumer(allocator, boundary, data.length); + ByteCursor cursor = new StreamCursor(data); + DribbleCursor dribble = new DribbleCursor(cursor, 3); + + while(!consumer.isFinished()) { + consumer.consume(dribble); + } + assertEquals(consumer.getBody().getContent(), SOURCE); + assertEquals(consumer.getBody().getParts().size(), 6); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/MessageHeaderTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/MessageHeaderTest.java new file mode 100644 index 0000000..36c0309 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/MessageHeaderTest.java @@ -0,0 +1,48 @@ +package org.simpleframework.http.message; + +import junit.framework.TestCase; + +public class MessageHeaderTest extends TestCase { + + public void testMessage() { + MessageHeader header = new MessageHeader(); + header.addValue("A", "a"); + header.addValue("A", "b"); + header.addValue("A", "c"); + + assertEquals(header.getValue("A"), "a"); + assertEquals(header.getValue("A", 0), "a"); + assertEquals(header.getValue("A", 1), "b"); + assertEquals(header.getValue("A", 2), "c"); + + header.setValue("A", null); + + assertEquals(header.getValue("A"), null); + assertEquals(header.getValue("A", 0), null); + assertEquals(header.getValue("A", 1), null); + assertEquals(header.getValue("A", 2), null); + assertEquals(header.getValue("A", 3), null); + assertEquals(header.getValue("A", 4), null); + assertEquals(header.getValue("A", 5), null); + + header.setValue("A", "X"); + + assertEquals(header.getValue("A"), "X"); + assertEquals(header.getValue("A", 0), "X"); + assertEquals(header.getValue("A", 1), null); + + header.addInteger("A", 1); + + assertEquals(header.getValue("A"), "X"); + assertEquals(header.getValue("A", 0), "X"); + assertEquals(header.getValue("A", 1), "1"); + assertEquals(header.getValue("A", 2), null); + + header.addValue("A", null); + + assertEquals(header.getValue("A"), "X"); + assertEquals(header.getValue("A", 0), "X"); + assertEquals(header.getValue("A", 1), "1"); + assertEquals(header.getValue("A", 2), null); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/MockBody.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/MockBody.java new file mode 100644 index 0000000..4a4ee75 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/MockBody.java @@ -0,0 +1,47 @@ +package org.simpleframework.http.message; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.simpleframework.http.Part; +import org.simpleframework.http.message.PartData; + + +public class MockBody implements Body { + + protected PartData list; + + protected String body; + + public MockBody() { + this(""); + } + + public MockBody(String body) { + this.list = new PartData(); + this.body = body; + } + + public List getParts() { + return list.getParts(); + } + + public Part getPart(String name) { + return list.getPart(name); + } + + public String getContent(String charset) { + return body; + } + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(body.getBytes("UTF-8")); + } + + public String getContent() throws IOException { + return body; + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/MockHeader.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/MockHeader.java new file mode 100644 index 0000000..67ea091 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/MockHeader.java @@ -0,0 +1,22 @@ +package org.simpleframework.http.message; + +import org.simpleframework.http.Address; +import org.simpleframework.http.parse.AddressParser; + +public class MockHeader extends RequestConsumer { + + private AddressParser parser; + private String address; + + public MockHeader(String address) { + this.address = address; + } + public Address getAddress() { + if(parser == null) { + parser = new AddressParser(address); + } + return parser; + } + + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/MockSegment.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/MockSegment.java new file mode 100644 index 0000000..7f49acb --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/MockSegment.java @@ -0,0 +1,83 @@ +package org.simpleframework.http.message; + +import java.util.List; + +import org.simpleframework.http.ContentDisposition; +import org.simpleframework.http.ContentType; +import org.simpleframework.http.message.MessageHeader; +import org.simpleframework.http.message.Segment; +import org.simpleframework.http.parse.ContentDispositionParser; +import org.simpleframework.http.parse.ContentTypeParser; + +public class MockSegment implements Segment { + + private MessageHeader header; + + public MockSegment() { + this.header = new MessageHeader(); + } + + public boolean isFile() { + return false; + } + + public ContentType getContentType() { + String value = getValue("Content-Type"); + + if(value == null) { + return null; + } + return new ContentTypeParser(value); + } + + public long getContentLength() { + String value = getValue("Content-Length"); + + if(value != null) { + return new Long(value); + } + return -1; + } + + public String getTransferEncoding() { + List list = getValues("Transfer-Encoding"); + + if(list.size() > 0) { + return list.get(0); + } + return null; + } + + public ContentDisposition getDisposition() { + String value = getValue("Content-Disposition"); + + if(value == null) { + return null; + } + return new ContentDispositionParser(value); + } + + public List getValues(String name) { + return header.getValues(name); + } + + public String getValue(String name) { + return header.getValue(name); + } + + public String getValue(String name, int index) { + return header.getValue(name, index); + } + + protected void add(String name, String value) { + header.addValue(name, value); + } + + public String getName() { + return null; + } + + public String getFileName() { + return null; + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/PartConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/PartConsumerTest.java new file mode 100644 index 0000000..4f96405 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/PartConsumerTest.java @@ -0,0 +1,33 @@ +package org.simpleframework.http.message; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.Part; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.message.PartConsumer; +import org.simpleframework.http.message.PartData; +import org.simpleframework.transport.ByteCursor; + +public class PartConsumerTest extends TestCase { + + private static final String SOURCE = + "Content-Disposition: form-data; name='pics'; filename='file1.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "... contents of file1.txt ...\r\n"+ + "--AaB03x\r\n"; + + public void testHeader() throws Exception { + PartData list = new PartData(); + PartConsumer consumer = new PartConsumer(new ArrayAllocator(), list, "AaB03x".getBytes("UTF-8"), 8192); + ByteCursor cursor = new StreamCursor(SOURCE); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + assertEquals(list.getParts().size(), 1); + assertEquals(list.getParts().get(0).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(0).getContentType().getSecondary(), "plain"); + assertEquals(((Part)list.getParts().get(0)).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file1.txt'"); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/PartSeriesConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/PartSeriesConsumerTest.java new file mode 100644 index 0000000..448dad5 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/PartSeriesConsumerTest.java @@ -0,0 +1,157 @@ +package org.simpleframework.http.message; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.message.PartData; +import org.simpleframework.http.message.PartSeriesConsumer; +import org.simpleframework.transport.ByteCursor; + +public class PartSeriesConsumerTest extends TestCase { + + private static final String SIMPLE = + "--AaB03x\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file1.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file1.txt ...\r\n"+ + "--AaB03x--\r\n"; + + private static final String NORMAL = + "--AaB03x\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file1.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file1.txt\r\n"+ + "--AaB03x\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file2.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file2.txt\r\n"+ + "--AaB03x\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file3.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file3.txt ...\r\n"+ + "--AaB03x--\r\n"; + + private static final String MIXED = + "--AaB03x\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file1.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file1.txt\r\n"+ + "--AaB03x\r\n"+ + "Content-Type: multipart/mixed; boundary=BbC04y\r\n\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file2.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file2.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file3.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file3.txt ...\r\n"+ + "--BbC04y\r\n"+ + "Content-Disposition: form-data; name='pics'; filename='file4.txt'\r\n"+ + "Content-Type: text/plain\r\n\r\n"+ + "example contents of file4.txt ...\r\n"+ + "--BbC04y--\r\n"+ + "--AaB03x--\r\n"; + + public void testSimple() throws Exception { + PartData list = new PartData(); + PartSeriesConsumer consumer = new PartSeriesConsumer(new ArrayAllocator(), list, "AaB03x".getBytes("UTF-8")); + ByteCursor cursor = new StreamCursor(SIMPLE); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + assertEquals(list.getParts().size(), 1); + assertEquals(list.getParts().get(0).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(0).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file1.txt'"); + assertEquals(list.getParts().get(0).getContent(), "example contents of file1.txt ..."); + assertEquals(cursor.ready(), -1); + assertEquals(consumer.getBody().getContent(), SIMPLE); + } + + public void testNormal() throws Exception { + PartData list = new PartData(); + PartSeriesConsumer consumer = new PartSeriesConsumer(new ArrayAllocator(), list, "AaB03x".getBytes("UTF-8")); + ByteCursor cursor = new StreamCursor(NORMAL); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + assertEquals(list.getParts().size(), 3); + assertEquals(list.getParts().get(0).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(0).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file1.txt'"); + assertEquals(list.getParts().get(0).getContent(), "example contents of file1.txt"); + assertEquals(list.getParts().get(1).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(1).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(1).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file2.txt'"); + assertEquals(list.getParts().get(1).getContent(), "example contents of file2.txt"); + assertEquals(list.getParts().get(2).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(2).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(2).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file3.txt'"); + assertEquals(list.getParts().get(2).getContent(), "example contents of file3.txt ..."); + assertEquals(cursor.ready(), -1); + assertEquals(consumer.getBody().getContent(), NORMAL); + } + + public void testMixed() throws Exception { + PartData list = new PartData(); + PartSeriesConsumer consumer = new PartSeriesConsumer(new ArrayAllocator(), list, "AaB03x".getBytes("UTF-8")); + ByteCursor cursor = new StreamCursor(MIXED); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + assertEquals(list.getParts().size(), 4); + assertEquals(list.getParts().get(0).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(0).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file1.txt'"); + assertEquals(list.getParts().get(0).getContent(), "example contents of file1.txt"); + assertEquals(list.getParts().get(1).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(1).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(1).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file2.txt'"); + assertEquals(list.getParts().get(1).getContent(), "example contents of file2.txt ..."); + assertEquals(list.getParts().get(2).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(2).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(2).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file3.txt'"); + assertEquals(list.getParts().get(2).getContent(), "example contents of file3.txt ..."); + assertEquals(list.getParts().get(3).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(3).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(3).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file4.txt'"); + assertEquals(list.getParts().get(3).getContent(), "example contents of file4.txt ..."); + assertEquals(cursor.ready(), -1); + assertEquals(consumer.getBody().getContent(), MIXED); + } + + public void testDribble() throws Exception { + PartData list = new PartData(); + PartSeriesConsumer consumer = new PartSeriesConsumer(new ArrayAllocator(), list, "AaB03x".getBytes("UTF-8")); + ByteCursor cursor = new DribbleCursor(new StreamCursor(NORMAL), 1); + + while(!consumer.isFinished()) { + consumer.consume(cursor); + } + assertEquals(list.getParts().size(), 3); + assertEquals(list.getParts().get(0).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(0).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(0).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file1.txt'"); + assertEquals(list.getParts().get(0).getContent(), "example contents of file1.txt"); + assertEquals(list.getParts().get(1).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(1).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(1).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file2.txt'"); + assertEquals(list.getParts().get(1).getContent(), "example contents of file2.txt"); + assertEquals(list.getParts().get(2).getContentType().getPrimary(), "text"); + assertEquals(list.getParts().get(2).getContentType().getSecondary(), "plain"); + assertEquals(list.getParts().get(2).getHeader("Content-Disposition"), "form-data; name='pics'; filename='file3.txt'"); + assertEquals(list.getParts().get(2).getContent(), "example contents of file3.txt ..."); + assertEquals(cursor.ready(), -1); + assertEquals(consumer.getBody().getContent(), NORMAL); + } + + public static void main(String[] list) throws Exception { + new PartSeriesConsumerTest().testMixed(); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/ReplyConsumer.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/ReplyConsumer.java new file mode 100644 index 0000000..a7fbfe5 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/ReplyConsumer.java @@ -0,0 +1,141 @@ +package org.simpleframework.http.message; + +import org.simpleframework.http.Cookie; +import org.simpleframework.http.ResponseHeader; +import org.simpleframework.http.Status; +import org.simpleframework.http.message.RequestConsumer; + +public class ReplyConsumer extends RequestConsumer implements ResponseHeader { + + private String text; + private int code; + + public ReplyConsumer() { + super(); + } + + private void status() { + while(pos < count) { + if(!digit(array[pos])) { + break; + } + code *= 10; + code += array[pos]; + code -= '0'; + pos++; + } + } + + private void text() { + StringBuilder builder = new StringBuilder(); + + while(pos < count) { + if(terminal(array[pos])) { + pos += 2; + break; + } + builder.append((char) array[pos]); + pos++; + } + text = builder.toString(); + } + + public String getDescription() { + return text; + } + + public void setDescription(String text) { + this.text = text; + } + + public int getCode() { + return code; + } + + public void setCode(int status) { + this.code = status; + } + + public Status getStatus() { + return Status.getStatus(code); + } + + public void setStatus(Status status) { + code = status.code; + text = status.description; + } + + @Override + protected void add(String name, String value) { + if(equal("Set-Cookie", name)) { // A=b; version=1; path=/; + String[] list = value.split(";"); // "A=b", "version=1", "path=/" + + if(list.length > 0) { + String[] pair = list[0].split("="); + + if(pair.length > 1) { + header.setCookie(pair[0], pair[1]); // "A", "b" + } + } + } + super.add(name, value); + } + + @Override + protected void process() { + version(); // HTTP/1.1 + adjust(); + status(); // 200 + adjust(); + text(); // OK + adjust(); + headers(); + } + + public void setMajor(int major) { + this.major = major; + + } + + public void setMinor(int minor) { + this.minor = minor; + + } + + public void addValue(String name, String value) { + header.addValue(name, value); + } + + public void addInteger(String name, int value) { + header.addInteger(name, value); + + } + + public void addDate(String name, long date) { + header.addDate(name, date); + } + + public void setValue(String name, String value) { + header.setValue(name, value); + } + + public void setInteger(String name, int value) { + header.setInteger(name, value); + } + + public void setLong(String name, long value) { + header.setLong(name, value); + } + + public void setDate(String name, long date) { + header.setDate(name, date); + } + + public Cookie setCookie(Cookie cookie) { + return header.setCookie(cookie); + } + + public Cookie setCookie(String name, String value) { + return header.setCookie(name, value); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerTest.java new file mode 100644 index 0000000..1c6916e --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerTest.java @@ -0,0 +1,103 @@ +package org.simpleframework.http.message; + +import java.io.IOException; + +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.message.SegmentConsumer; +import org.simpleframework.transport.ByteCursor; + +import junit.framework.TestCase; + +public class SegmentConsumerTest extends TestCase { + + private static final String SOURCE = + "Content-Type: application/x-www-form-urlencoded\r\n"+ + "User-Agent:\r\n" + + "Content-Length: 42\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n"; + + private static final String EMPTY = + "Accept-Language:\r\n"+ + "Content-Length:\r\n"+ + "Content-Type:\r\n"+ + "Content-Disposition:\r\n"+ + "Transfer-Encoding:\r\n"+ + "Expect:\r\n"+ + "Cookie:\r\n"+ + "\r\n"; + + protected SegmentConsumer header; + + public void setUp() throws IOException { + header = new SegmentConsumer(); + } + + public void testHeader() throws Exception { + ByteCursor cursor = new StreamCursor(SOURCE); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + assertEquals(header.getValue("Pragma"), null); + assertEquals(header.getValue("User-Agent"), ""); + assertEquals(header.getValue("Content-Length"), "42"); + assertEquals(header.getValue("Content-Type"), "application/x-www-form-urlencoded"); + assertEquals(header.getValue("Host"), "some.host.com"); + assertEquals(header.getValues("Accept").size(), 4); + assertEquals(header.getValues("Accept").get(0), "image/gif"); + assertEquals(header.getValues("Accept").get(1), "image/png"); + assertEquals(header.getValues("Accept").get(2), "image/jpeg"); + assertEquals(header.getValues("Accept").get(3), "*"); + assertEquals(header.getContentType().getPrimary(), "application"); + assertEquals(header.getContentType().getSecondary(), "x-www-form-urlencoded"); + assertEquals(header.getTransferEncoding(), "chunked"); + } + + public void testEmptyHeader() throws Exception { + ByteCursor cursor = new StreamCursor(EMPTY); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + assertEquals(header.getValue("Accept-Language"), ""); + assertEquals(header.getValue("Content-Length"), ""); + assertEquals(header.getValue("Content-Type"), ""); + assertEquals(header.getValue("Content-Disposition"), ""); + assertEquals(header.getValue("Transfer-Encoding"), ""); + assertEquals(header.getValue("Expect"), ""); + assertEquals(header.getValue("Cookie"), ""); + assertEquals(header.getContentType().getPrimary(), null); + assertEquals(header.getContentType().getSecondary(), null); + } + + public void testDribble() throws Exception { + ByteCursor cursor = new DribbleCursor(new StreamCursor(SOURCE), 1); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + assertEquals(header.getValue("Content-Length"), "42"); + assertEquals(header.getValue("Content-Type"), "application/x-www-form-urlencoded"); + assertEquals(header.getValue("Host"), "some.host.com"); + assertEquals(header.getValues("Accept").size(), 4); + assertEquals(header.getValues("Accept").get(0), "image/gif"); + assertEquals(header.getValues("Accept").get(1), "image/png"); + assertEquals(header.getValues("Accept").get(2), "image/jpeg"); + assertEquals(header.getValues("Accept").get(3), "*"); + assertEquals(header.getContentType().getPrimary(), "application"); + assertEquals(header.getContentType().getSecondary(), "x-www-form-urlencoded"); + assertEquals(header.getTransferEncoding(), "chunked"); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/TokenConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/TokenConsumerTest.java new file mode 100644 index 0000000..de7461c --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/TokenConsumerTest.java @@ -0,0 +1,55 @@ +package org.simpleframework.http.message; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.message.TokenConsumer; +import org.simpleframework.transport.ByteCursor; + +public class TokenConsumerTest extends TestCase { + + public void testTokenConsumer() throws IOException { + Allocator allocator = new ArrayAllocator(); + TokenConsumer consumer = new TokenConsumer(allocator, "\r\n".getBytes()); + ByteCursor cursor = new StreamCursor("\r\n"); + + consumer.consume(cursor); + + assertEquals(cursor.ready(), -1); + assertTrue(consumer.isFinished()); + } + + public void testTokenConsumerException() throws IOException { + Allocator allocator = new ArrayAllocator(); + TokenConsumer consumer = new TokenConsumer(allocator, "\r\n".getBytes()); + ByteCursor cursor = new StreamCursor("--\r\n"); + boolean exception = false; + + try { + consumer.consume(cursor); + } catch(Exception e) { + exception = true; + } + assertTrue("Exception not thrown for invalid token", exception); + } + + public void testTokenConsumerDribble() throws IOException { + Allocator allocator = new ArrayAllocator(); + TokenConsumer consumer = new TokenConsumer(allocator, "This is a large token to be consumed\r\n".getBytes()); + DribbleCursor cursor = new DribbleCursor(new StreamCursor("This is a large token to be consumed\r\n0123456789"), 1); + + consumer.consume(cursor); + + assertEquals(cursor.ready(), 1); + assertTrue(consumer.isFinished()); + assertEquals(cursor.read(), '0'); + assertEquals(cursor.read(), '1'); + assertEquals(cursor.read(), '2'); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/AddressParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/AddressParserTest.java new file mode 100644 index 0000000..0b93a90 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/AddressParserTest.java @@ -0,0 +1,92 @@ +package org.simpleframework.http.parse; + +import junit.framework.TestCase; + +import org.simpleframework.http.Query; + +public class AddressParserTest extends TestCase { + + private AddressParser link; + + protected void setUp() { + link = new AddressParser(); + } + + public void testEmptyPath() { + assertEquals("/", link.getPath().toString()); + } + + public void testEmptyQuery() { + Query query = link.getQuery(); + assertEquals(0, query.size()); + } + + public void testPath() { + link.parse("/this/./is//some/relative/./hidden/../URI.txt"); + assertEquals("/this/is//some/relative/URI.txt", link.getPath().toString()); + + link.parse("/this//is/a/simple/path.html?query"); + assertEquals("/this//is/a/simple/path.html", link.getPath().toString()); + } + + public void testQuery() { + link.parse("/?name=value&attribute=string"); + + Query query = link.getQuery(); + + assertEquals(2, query.size()); + assertEquals("value", query.get("name")); + assertTrue(query.containsKey("attribute")); + + query.clear(); + query.put("name", "change"); + + assertEquals("change", query.get("name")); + } + + public void testPathParameters() { + link.parse("/index.html;jsessionid=1234567890?jsessionid=query"); + assertEquals("1234567890", link.getParameters().get("jsessionid")); + + link.parse("/path/index.jsp"); + link.getParameters().put("jsessionid", "value"); + + assertEquals("/path/index.jsp;jsessionid=value", link.toString()); + + link.parse("/path"); + link.getParameters().put("a", "1"); + link.getParameters().put("b", "2"); + link.getParameters().put("c", "3"); + + link.parse(link.toString()); + + assertEquals("1", link.getParameters().get("a")); + assertEquals("2", link.getParameters().get("b")); + assertEquals("3", link.getParameters().get("c")); + + + } + + public void testAbsolute() { + link.parse("http://domain:9090/index.html?query=value"); + assertEquals("domain", link.getDomain()); + + link.setDomain("some.domain"); + assertEquals("some.domain", link.getDomain()); + assertEquals("http://some.domain:9090/index.html?query=value", link.toString()); + assertEquals(9090, link.getPort()); + + link.parse("domain.com:80/index.html?a=b&c=d"); + assertEquals("domain.com", link.getDomain()); + assertEquals(80, link.getPort()); + + link.parse("https://secure.com/index.html"); + assertEquals("https", link.getScheme()); + assertEquals("secure.com", link.getDomain()); + + link.setDomain("www.google.com:45"); + assertEquals("www.google.com", link.getDomain()); + assertEquals("https://www.google.com:45/index.html", link.toString()); + assertEquals(45, link.getPort()); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentDispositionParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentDispositionParserTest.java new file mode 100644 index 0000000..e5b0266 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentDispositionParserTest.java @@ -0,0 +1,33 @@ +package org.simpleframework.http.parse; + +import org.simpleframework.http.parse.ContentDispositionParser; + +import junit.framework.TestCase; + +public class ContentDispositionParserTest extends TestCase { + + private ContentDispositionParser parser; + + public void setUp() { + parser = new ContentDispositionParser(); + } + + public void testDisposition() { + parser.parse("form-data; name=\"input_check\""); + + assertFalse(parser.isFile()); + assertEquals(parser.getName(), "input_check"); + + parser.parse("form-data; name=\"input_password\""); + + assertFalse(parser.isFile()); + assertEquals(parser.getName(), "input_password"); + + parser.parse("form-data; name=\"FileItem\"; filename=\"C:\\Inetpub\\wwwroot\\Upload\\file1.txt\""); + + assertTrue(parser.isFile()); + assertEquals(parser.getName(), "FileItem"); + assertEquals(parser.getFileName(), "C:\\Inetpub\\wwwroot\\Upload\\file1.txt"); + + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentTypeParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentTypeParserTest.java new file mode 100644 index 0000000..863440e --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentTypeParserTest.java @@ -0,0 +1,74 @@ +package org.simpleframework.http.parse; + +import junit.framework.TestCase; + +import org.simpleframework.http.parse.ContentTypeParser; + +public class ContentTypeParserTest extends TestCase { + + private ContentTypeParser type; + + protected void setUp() { + type = new ContentTypeParser(); + } + + public void testEmpty() { + assertEquals(null, type.getPrimary()); + assertEquals(null, type.getSecondary()); + assertEquals(null, type.getCharset()); + } + + public void testPlain() { + type.parse("text/html"); + assertEquals("text", type.getPrimary()); + assertEquals("html", type.getSecondary()); + + type.setSecondary("plain"); + assertEquals("text", type.getPrimary()); + assertEquals("plain", type.getSecondary()); + } + + public void testCharset() { + type.parse("text/html; charset=UTF-8"); + assertEquals("text", type.getPrimary()); + assertEquals("UTF-8", type.getCharset()); + assertEquals("text/html", type.getType()); + + type.setCharset("ISO-8859-1"); + assertEquals("ISO-8859-1", type.getCharset()); + } + + public void testIgnore() { + type.parse("text/html; name=value; charset=UTF-8; property=value"); + assertEquals("UTF-8", type.getCharset()); + assertEquals("html", type.getSecondary()); + } + + public void testFlexibility() { + type.parse(" text/html ;charset= UTF-8 ; name = value" ); + assertEquals("text", type.getPrimary()); + assertEquals("html", type.getSecondary()); + assertEquals("text/html", type.getType()); + assertEquals("UTF-8", type.getCharset()); + } + + public void testString() { + type.parse(" image/gif; name=value"); + assertEquals("image/gif; name=value", type.toString()); + + type.parse(" text/html; charset =ISO-8859-1"); + assertEquals("text/html; charset=ISO-8859-1", type.toString()); + assertEquals("text/html", type.getType()); + + type.setSecondary("css"); + assertEquals("text", type.getPrimary()); + assertEquals("css", type.getSecondary()); + assertEquals("text/css", type.getType()); + assertEquals("text/css; charset=ISO-8859-1", type.toString()); + + type.setPrimary("image"); + assertEquals("image", type.getPrimary()); + assertEquals("css", type.getSecondary()); + assertEquals("image/css", type.getType()); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/CookieParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/CookieParserTest.java new file mode 100644 index 0000000..9c0c7b8 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/CookieParserTest.java @@ -0,0 +1,22 @@ +package org.simpleframework.http.parse; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.simpleframework.http.Cookie; + +public class CookieParserTest extends TestCase { + + public void testParse() throws Exception { + CookieParser parser = new CookieParser("blackbird={\"pos\": 1, \"size\": 0, \"load\": null}; JSESSIONID=31865d30-e252-4729-ac6f-9abdd1fb9071"); + List cookies = new ArrayList(); + + for(Cookie cookie : parser) { + System.out.println(cookie.toClientString()); + cookies.add(cookie); + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/DateParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/DateParserTest.java new file mode 100644 index 0000000..f262d10 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/DateParserTest.java @@ -0,0 +1,55 @@ +package org.simpleframework.http.parse; + +import java.util.Calendar; +import java.util.TimeZone; + +import junit.framework.TestCase; + +public class DateParserTest extends TestCase { + + /** + * Sun, 06 Nov 2009 08:49:37 GMT ; RFC 822, updated by RFC 1123 Sunday, + * 06-Nov-09 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 Sun Nov 6 08:49:37 + * 2009 ; ANSI C's asctime() format + */ + public void testDate() { + DateParser rfc822 = new DateParser("Sun, 06 Nov 2009 08:49:37 GMT"); + DateParser rfc850 = new DateParser("Sunday, 06-Nov-09 08:49:37 GMT"); + DateParser asctime = new DateParser("Sun Nov 6 08:49:37 2009"); + + assertEquals(rfc822.toLong() >> 10, rfc850.toLong() >> 10); // shift out + // seconds + assertEquals(rfc822.toLong() >> 10, asctime.toLong() >> 10); // shift out + // seconds + assertEquals(rfc822.toString(), rfc850.toString()); + assertEquals(rfc822.toString(), asctime.toString()); + assertEquals(rfc850.toString(), "Sun, 06 Nov 2009 08:49:37 GMT"); + assertEquals(rfc850.toString().length(), 29); + assertEquals(rfc822.toString(), "Sun, 06 Nov 2009 08:49:37 GMT"); + assertEquals(rfc822.toString().length(), 29); + assertEquals(asctime.toString(), "Sun, 06 Nov 2009 08:49:37 GMT"); + assertEquals(asctime.toString().length(), 29); + } + + public void testLong() throws Exception { + String date = "Thu, 20 Jan 2011 16:43:08 GMT"; + + DateParser dp1 = new DateParser(date); + System.out.println("value a: " + dp1.toLong()); + Thread.sleep(50); + + DateParser dp2 = new DateParser(date); + System.out.println("value b: " + dp2.toLong()); + Thread.sleep(50); + + DateParser dp3 = new DateParser(date); + System.out.println("value c: " + dp3.toLong()); + + assertEquals(dp1.toLong(), dp2.toLong()); + assertEquals(dp2.toLong(), dp3.toLong()); + assertEquals(dp1.toString(), dp2.toString()); + assertEquals(dp2.toString(), dp3.toString()); + + + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/LanguageParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/LanguageParserTest.java new file mode 100644 index 0000000..2d382f0 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/LanguageParserTest.java @@ -0,0 +1,26 @@ +package org.simpleframework.http.parse; + +import junit.framework.TestCase; + +public class LanguageParserTest extends TestCase { + + public void testLanguages() throws Exception { + LanguageParser parser = new LanguageParser(); + + parser.parse("en-gb,en;q=0.5"); + + assertEquals(parser.list().get(0).getLanguage(), "en"); + assertEquals(parser.list().get(0).getCountry(), "GB"); + assertEquals(parser.list().get(1).getLanguage(), "en"); + assertEquals(parser.list().get(1).getCountry(), ""); + + parser.parse("en-gb,en;q=0.5,*;q=0.9"); + + assertEquals(parser.list().get(0).getLanguage(), "en"); + assertEquals(parser.list().get(0).getCountry(), "GB"); + assertEquals(parser.list().get(1).getLanguage(), "*"); + assertEquals(parser.list().get(2).getLanguage(), "en"); + assertEquals(parser.list().get(2).getCountry(), ""); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/ListParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ListParserTest.java new file mode 100644 index 0000000..100bf27 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ListParserTest.java @@ -0,0 +1,97 @@ +package org.simpleframework.http.parse; + +import junit.framework.TestCase; + +public class ListParserTest extends TestCase { + + private ValueParser list; + + protected void setUp() { + list = new ValueParser(); + } + + public void testEmpty() { + assertEquals(0, list.list().size()); + } + + public void testQvalue() { + list.parse("ISO-8859-1,utf-8;q=0.7,*;q=0.7"); + assertEquals(list.list().get(0), "ISO-8859-1"); + assertEquals(list.list().get(1), "utf-8"); + assertEquals(list.list().get(2), "*"); + } + + public void testPlain() { + list.parse("en-gb"); + assertEquals("en-gb", list.list().get(0)); + + list.parse("en"); + assertEquals("en", list.list().get(0)); + } + + public void testList() { + list.parse("en-gb, en-us"); + assertEquals(2, list.list().size()); + assertEquals("en-gb", list.list().get(0)); + assertEquals("en-us", list.list().get(1)); + } + + public void testOrder() { + list.parse("en-gb, en-us"); + assertEquals(2, list.list().size()); + assertEquals("en-gb", list.list().get(0)); + assertEquals("en-us", list.list().get(1)); + + list.parse("da, en-gb;q=0.8, en;q=0.7"); + assertEquals("da", list.list().get(0)); + assertEquals("en-gb", list.list().get(1)); + assertEquals("en", list.list().get(2)); + + list.parse("fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7"); + assertEquals("en-gb", list.list().get(0)); + assertEquals("en", list.list().get(1)); + assertEquals("en-us", list.list().get(2)); + assertEquals("fr", list.list().get(3)); + + list.parse("en;q=0.2, en-us;q=1.0, en-gb"); + assertEquals("en-gb", list.list().get(0)); + assertEquals("en-us", list.list().get(1)); + assertEquals("en", list.list().get(2)); + } + + public void testRange() { + list.parse("image/gif, image/jpeg, text/html"); + assertEquals(3, list.list().size()); + assertEquals("image/gif", list.list().get(0)); + assertEquals("text/html", list.list().get(2)); + + list.parse("image/gif;q=1.0, image/jpeg;q=0.8, image/png; q=1.0,*;q=0.1"); + assertEquals("image/gif", list.list().get(0)); + assertEquals("image/png", list.list().get(1)); + assertEquals("image/jpeg", list.list().get(2)); + + list.parse("gzip;q=1.0, identity; q=0.5, *;q=0"); + assertEquals("gzip", list.list().get(0)); + assertEquals("identity", list.list().get(1)); + } + + public void testFlexibility() { + list.parse("last; quantity=1;q=0.001, first; text=\"a, b, c, d\";q=0.4"); + assertEquals(2, list.list().size()); + assertEquals("first; text=\"a, b, c, d\"", list.list().get(0)); + assertEquals("last; quantity=1", list.list().get(1)); + + list.parse("image/gif, , image/jpeg, image/png;q=0.8, *"); + assertEquals(4, list.list().size()); + assertEquals("image/gif", list.list().get(0)); + assertEquals("image/jpeg", list.list().get(1)); + assertEquals("*", list.list().get(2)); + assertEquals("image/png", list.list().get(3)); + + list.parse("first=\"\\\"a, b, c, d\\\", a, b, c, d\", third=\"a\";q=0.9,,second=2"); + assertEquals(3, list.list().size()); + assertEquals("first=\"\\\"a, b, c, d\\\", a, b, c, d\"", list.list().get(0)); + assertEquals("second=2", list.list().get(1)); + assertEquals("third=\"a\"", list.list().get(2)); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/ParameterTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ParameterTest.java new file mode 100644 index 0000000..5c14d00 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/ParameterTest.java @@ -0,0 +1,69 @@ +package org.simpleframework.http.parse; + +import org.simpleframework.http.parse.QueryParser; + +import junit.framework.TestCase; + +public class ParameterTest extends TestCase { + + private QueryParser data; + + protected void setUp() { + data = new QueryParser(); + } + + public void testEmptyPath() { + assertEquals(0, data.size()); + } + + public void testValue() { + data.parse("a="); + + assertEquals(1, data.size()); + assertEquals("", data.get("a")); + + data.parse("a=&b=c"); + + assertEquals(2, data.size()); + assertEquals("", data.get("a")); + assertEquals("c", data.get("b")); + + data.parse("a=b&c=d&e=f&"); + + assertEquals(3, data.size()); + assertEquals("b", data.get("a")); + assertEquals("d", data.get("c")); + assertEquals("f", data.get("e")); + + data.clear(); + data.put("a", "A"); + data.put("c", "C"); + data.put("x", "y"); + + assertEquals(3, data.size()); + assertEquals("A", data.get("a")); + assertEquals("C", data.get("c")); + assertEquals("y", data.get("x")); + } + + public void testValueList() { + data.parse("a=1&a=2&a=3"); + + assertEquals(data.size(), 1); + assertEquals(data.getAll("a").size(), 3); + assertEquals(data.getAll("a").get(0), "1"); + assertEquals(data.getAll("a").get(1), "2"); + assertEquals(data.getAll("a").get(2), "3"); + + data.parse("a=b&c=d&c=d&a=1"); + + assertEquals(data.size(), 2); + assertEquals(data.getAll("a").size(), 2); + assertEquals(data.getAll("a").get(0), "b"); + assertEquals(data.getAll("a").get(1), "1"); + assertEquals(data.getAll("c").size(), 2); + assertEquals(data.getAll("c").get(0), "d"); + assertEquals(data.getAll("c").get(1), "d"); + + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/PathParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/PathParserTest.java new file mode 100644 index 0000000..4ae4d60 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/PathParserTest.java @@ -0,0 +1,97 @@ +package org.simpleframework.http.parse; + +import junit.framework.TestCase; + +import org.simpleframework.http.parse.PathParser; + +public class PathParserTest extends TestCase { + + private PathParser path; + + protected void setUp() { + path = new PathParser(); + } + + public void testEmpty() { + assertEquals(null, path.getPath()); + assertEquals(null, path.getExtension()); + assertEquals(null, path.getName()); + } + + public void testSegments() { + path.parse("/a/b/c/d"); + + String[] list = path.getSegments(); + + assertEquals("a", list[0]); + assertEquals("b", list[1]); + assertEquals("c", list[2]); + assertEquals("d", list[3]); + } + + public void testSubPath() { + path.parse("/0/1/2/3/4/5/6/index.html"); + + testSubPath(1); + testSubPath(2); + testSubPath(3); + testSubPath(4); + testSubPath(5); + testSubPath(6); + testSubPath(7); + + testSubPath(0,4); + testSubPath(1,2); + testSubPath(2,3); + testSubPath(3,4); + testSubPath(1,3); + testSubPath(1,4); + testSubPath(1,5); + + path.parse("/a/b/c/d/e/index.html"); + + testSubPath(1,2); + testSubPath(2,3); + testSubPath(3,1); + testSubPath(1,3); + } + + private void testSubPath(int from) { + System.err.printf("[%s] %s: %s%n", path, from, path.getPath(from)); + } + + private void testSubPath(int from, int to) { + System.err.printf("[%s] %s, %s: %s%n", path, from, to, path.getPath(from, to)); + } + + public void testDirectory() { + path.parse("/some/directory/path/index.html"); + assertEquals("/some/directory/path/", path.getDirectory()); + + path.parse("/some/path/README"); + assertEquals("/some/path/", path.getDirectory()); + } + + public void testNormalization() { + path.parse("/path/./../index.html"); + assertEquals("/", path.getDirectory()); + + path.parse("/path/hidden/./index.html"); + assertEquals("/path/hidden/", path.getDirectory()); + + path.parse("/path/README"); + assertEquals("/path/", path.getDirectory()); + } + + public void testString() { + path.parse("/some/path/../path/./to//a/file.txt"); + assertEquals("/some/path/to//a/file.txt", path.toString()); + } + + public void testAIOB(){ + path.parse("/admin/ws"); + String result = path.getRelative("/admin/ws/"); + String expResult = null; + assertEquals(expResult, result); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/PriorityQueueTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/PriorityQueueTest.java new file mode 100644 index 0000000..5c03ebd --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/PriorityQueueTest.java @@ -0,0 +1,48 @@ +package org.simpleframework.http.parse; + +import java.util.PriorityQueue; + +import junit.framework.TestCase; + +public class PriorityQueueTest extends TestCase { + + + private static class Entry implements Comparable { + + private final String text; + private final int priority; + private final int start; + + public Entry(String text, int priority, int start) { + this.priority = priority; + this.start = start; + this.text = text; + } + + public int compareTo(Entry entry) { + int value = entry.priority - priority; + + if(value == 0) { + return entry.start - start; + } + return value; + } + } + public void testPriorityQueue() { + PriorityQueue queue = new PriorityQueue(); + int start = 10000; + + queue.offer(new Entry("a", 10, start--)); + queue.offer(new Entry("b", 10, start--)); + queue.offer(new Entry("c", 10, start--)); + queue.offer(new Entry("d", 10, start--)); + queue.offer(new Entry("e", 20, start--)); + queue.offer(new Entry("f", 30, start--)); + queue.offer(new Entry("g", 20, start--)); + + while(!queue.isEmpty()) { + System.err.println(queue.remove().text); + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/parse/QueryParserTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/parse/QueryParserTest.java new file mode 100644 index 0000000..aae92ff --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/parse/QueryParserTest.java @@ -0,0 +1,69 @@ +package org.simpleframework.http.parse; + +import org.simpleframework.http.parse.QueryParser; + +import junit.framework.TestCase; + +public class QueryParserTest extends TestCase { + + private QueryParser data; + + protected void setUp() { + data = new QueryParser(); + } + + public void testEmptyPath() { + assertEquals(0, data.size()); + } + + public void testValue() { + data.parse("a="); + + assertEquals(1, data.size()); + assertEquals("", data.get("a")); + + data.parse("a=&b=c"); + + assertEquals(2, data.size()); + assertEquals("", data.get("a")); + assertEquals("c", data.get("b")); + + data.parse("a=b&c=d&e=f&"); + + assertEquals(3, data.size()); + assertEquals("b", data.get("a")); + assertEquals("d", data.get("c")); + assertEquals("f", data.get("e")); + + data.clear(); + data.put("a", "A"); + data.put("c", "C"); + data.put("x", "y"); + + assertEquals(3, data.size()); + assertEquals("A", data.get("a")); + assertEquals("C", data.get("c")); + assertEquals("y", data.get("x")); + } + + public void testValueList() { + data.parse("a=1&a=2&a=3"); + + assertEquals(data.size(), 1); + assertEquals(data.getAll("a").size(), 3); + assertEquals(data.getAll("a").get(0), "1"); + assertEquals(data.getAll("a").get(1), "2"); + assertEquals(data.getAll("a").get(2), "3"); + + data.parse("a=b&c=d&c=d&a=1"); + + assertEquals(data.size(), 2); + assertEquals(data.getAll("a").size(), 2); + assertEquals(data.getAll("a").get(0), "b"); + assertEquals(data.getAll("a").get(1), "1"); + assertEquals(data.getAll("c").size(), 2); + assertEquals(data.getAll("c").get(0), "d"); + assertEquals(data.getAll("c").get(1), "d"); + + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebFrameTypeTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebFrameTypeTest.java new file mode 100644 index 0000000..037627d --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebFrameTypeTest.java @@ -0,0 +1,29 @@ +package org.simpleframework.http.socket; + +import junit.framework.TestCase; + +public class WebFrameTypeTest extends TestCase { + + public void testFrameType() throws Exception { + System.err.println(Integer.toBinaryString(129)); // TEXT FRAME + System.err.println(Integer.toBinaryString(128)); + System.err.println(Integer.toBinaryString(129 >>> 4)); + System.err.println(Integer.toBinaryString(0x01)); // TEXT + System.err.println(Integer.toBinaryString(0x02)); // BINARY + System.err.println(Integer.toBinaryString(0x03)); + System.err.println(Integer.toBinaryString(0x01 % 0x80)); // TEXT + System.err.println(Integer.toBinaryString(0x02 % 0x80)); // BINARY + System.err.println(Integer.toBinaryString(0x03 % 0x80)); + System.err.println(Integer.toBinaryString(0x80)); // FIN + + int b0 = 0; + if (true) { + b0 |= 1 << 7; + } + b0 |= 0x02 % 128; + + + System.err.println(Integer.toBinaryString(b0)); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketAnalyzer.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketAnalyzer.java new file mode 100644 index 0000000..3e89652 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketAnalyzer.java @@ -0,0 +1,66 @@ +package org.simpleframework.http.socket; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.channels.SelectableChannel; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.simpleframework.transport.trace.TraceAnalyzer; +import org.simpleframework.transport.trace.Trace; + +public class WebSocketAnalyzer implements TraceAnalyzer { + + private final Map map; + private final AtomicInteger count; + private final boolean debug; + + public WebSocketAnalyzer() { + this(true); + } + + public WebSocketAnalyzer(boolean debug) { + this.map = new ConcurrentHashMap(); + this.count = new AtomicInteger(); + this.debug = debug; + } + + public Trace attach(SelectableChannel channel) { + if(map.containsKey(channel)) { + throw new IllegalStateException("Can't attach twice"); + } + final int counter = count.getAndIncrement(); + map.put(channel, counter); + + return new Trace() { + + public void trace(Object event) { + if(debug) { + trace(event, ""); + } + } + + public void trace(Object event, Object value) { + if(debug) { + if(value instanceof Throwable) { + StringWriter writer = new StringWriter(); + PrintWriter out = new PrintWriter(writer); + ((Exception)value).printStackTrace(out); + out.flush(); + value = writer.toString(); + } + if(value != null && !String.valueOf(value).isEmpty()) { + System.err.printf("(%s) [%s] %s: %s%n", Thread.currentThread().getName(), counter, event, value); + } else { + System.err.printf("(%s) [%s] %s%n", Thread.currentThread().getName(), counter, event); + } + } + } + }; + } + + public void stop() { + System.err.println("Stop agent"); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketCertificate.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketCertificate.java new file mode 100644 index 0000000..99018e3 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketCertificate.java @@ -0,0 +1,170 @@ +package org.simpleframework.http.socket; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +public class WebSocketCertificate { + + private final X509TrustManager trustManager; + private final X509TrustManager[] trustManagers; + private final KeyStoreReader keyStoreReader; + private final SecureProtocol secureProtocol; + + public WebSocketCertificate(KeyStoreReader keyStoreReader, SecureProtocol secureProtocol) { + this.trustManager = new AnonymousTrustManager(); + this.trustManagers = new X509TrustManager[] { trustManager }; + this.keyStoreReader = keyStoreReader; + this.secureProtocol = secureProtocol; + } + + public WebSocketCertificate(KeyStoreReader keyStoreReader, SecureProtocol secureProtocol, X509TrustManager trustManager) { + this.trustManagers = new X509TrustManager[] { trustManager }; + this.keyStoreReader = keyStoreReader; + this.secureProtocol = secureProtocol; + this.trustManager = trustManager; + } + + public SSLContext getContext() throws Exception { + KeyManager[] keyManagers = keyStoreReader.getKeyManagers(); + SSLContext secureContext = secureProtocol.getContext(); + + secureContext.init(keyManagers, trustManagers, null); + + return secureContext; + } + + public SSLSocketFactory getSocketFactory() throws Exception { + KeyManager[] keyManagers = keyStoreReader.getKeyManagers(); + SSLContext secureContext = secureProtocol.getContext(); + + secureContext.init(keyManagers, trustManagers, null); + + return secureContext.getSocketFactory(); + } + + public SSLServerSocketFactory getServerSocketFactory() throws Exception { + KeyManager[] keyManagers = keyStoreReader.getKeyManagers(); + SSLContext secureContext = secureProtocol.getContext(); + + secureContext.init(keyManagers, trustManagers, null); + + return secureContext.getServerSocketFactory(); + } + + public static enum SecureProtocol { + DEFAULT("Default"), + SSL("SSL"), + TLS("TLS"); + + private final String protocol; + + private SecureProtocol(String protocol) { + this.protocol = protocol; + } + + public SSLContext getContext() throws NoSuchAlgorithmException { + return SSLContext.getInstance(protocol); + } + } + + public static enum KeyStoreType { + JKS("JKS", "SunX509"), + PKCS12("PKCS12", "SunX509"); + + private final String algorithm; + private final String type; + + private KeyStoreType(String type, String algorithm) { + this.algorithm = algorithm; + this.type = type; + } + + public String getType() { + return type; + } + + public KeyStore getKeyStore() throws KeyStoreException { + return KeyStore.getInstance(type); + } + + public KeyManagerFactory getKeyManagerFactory() throws NoSuchAlgorithmException { + return KeyManagerFactory.getInstance(algorithm); + } + } + + public static class KeyStoreReader { + + private final KeyStoreManager keyStoreManager; + private final String keyManagerPassword; + private final String keyStorePassword; + private final File keyStore; + + public KeyStoreReader(KeyStoreType keyStoreType, File keyStore, String keyStorePassword, String keyManagerPassword) { + this.keyStoreManager = new KeyStoreManager(keyStoreType); + this.keyManagerPassword = keyManagerPassword; + this.keyStorePassword = keyStorePassword; + this.keyStore = keyStore; + } + + public KeyManager[] getKeyManagers() throws Exception { + InputStream storeSource = new FileInputStream(keyStore); + + try { + return keyStoreManager.getKeyManagers(storeSource, keyStorePassword, keyManagerPassword); + } finally { + storeSource.close(); + } + } + } + + public static class KeyStoreManager { + + private final KeyStoreType keyStoreType; + + public KeyStoreManager(KeyStoreType keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public KeyManager[] getKeyManagers(InputStream keyStoreSource, String keyStorePassword, String keyManagerPassword) throws Exception { + KeyStore keyStore = keyStoreType.getKeyStore(); + KeyManagerFactory keyManagerFactory = keyStoreType.getKeyManagerFactory(); + + keyStore.load(keyStoreSource, keyManagerPassword.toCharArray()); + keyManagerFactory.init(keyStore, keyManagerPassword.toCharArray()); + + return keyManagerFactory.getKeyManagers(); + } + } + + public static class AnonymousTrustManager implements X509TrustManager { + + public boolean isClientTrusted(X509Certificate[] cert) { + return true; + } + + public boolean isServerTrusted(X509Certificate[] cert) { + return true; + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatApplication.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatApplication.java new file mode 100644 index 0000000..7d27b9f --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatApplication.java @@ -0,0 +1,170 @@ +package org.simpleframework.http.socket; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Map; + +import javax.net.ssl.SSLContext; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.Cookie; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.Status; +import org.simpleframework.http.core.Container; +import org.simpleframework.http.core.ContainerTransportProcessor; +import org.simpleframework.http.socket.service.Router; +import org.simpleframework.http.socket.service.RouterContainer; +import org.simpleframework.http.socket.service.DirectRouter; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.TransportSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.Transport; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; +import org.simpleframework.transport.trace.TraceAnalyzer; + +public class WebSocketChatApplication implements Container, TransportProcessor { + + private final WebSocketCertificate certificate; + private final Router negotiator; + private final RouterContainer container; + private final SocketAddress address; + private final Connection connection; + private final TransportProcessor processor; + private final Allocator allocator; + private final SocketProcessor server; + + public WebSocketChatApplication(WebSocketChatRoom service, WebSocketCertificate certificate, TraceAnalyzer agent, int port) throws Exception { + this.negotiator = new DirectRouter(service); + this.container = new RouterContainer(this, negotiator, 10); + this.allocator = new ArrayAllocator(); + this.processor = new ContainerTransportProcessor(container, allocator, 1); + this.server = new TransportSocketProcessor(this); + this.connection = new SocketConnection(server, agent); + this.address = new InetSocketAddress(port); + this.certificate = certificate; + } + + public void connect() throws Exception { + // if(certificate != null) { + // SSLContext context = certificate.getContext(); + // + // connection.connect(address, context); + // container.start(); + // } else { + connection.connect(address); + // } + } + + public void handle(Request req, Response resp) { + System.err.println(req); + + if(req.getTarget().equals("/")) { + long time = System.currentTimeMillis(); + + try { + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketChatApplication/1.0"); + resp.setContentType("text/html"); + String page = loadPage("WebSocketChatLogin.html"); + + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketChatApplication/1.0"); + resp.setContentType("text/html"); + + PrintStream out = resp.getPrintStream(); + out.println(page); + out.close(); + }catch(Exception e) { + e.printStackTrace(); + } + } else if(req.getTarget().equals("/login")) { + String user = req.getParameter("user"); + long time = System.currentTimeMillis(); + + try { + resp.setStatus(Status.FOUND); + resp.setValue("Location", "/chat"); + resp.setCookie("user", user); + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketChatApplication/1.0"); + resp.setContentType("text/html"); + resp.close(); + }catch(Exception e) { + e.printStackTrace(); + } + } else if(req.getTarget().equals("/chat")) { + long time = System.currentTimeMillis(); + + try { + Cookie user = req.getCookie("user"); + String name = user.getValue(); + String page = loadPage("WebSocketChatRoom.html"); + + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketChatApplication/1.0"); + resp.setContentType("text/html"); + + PrintStream out = resp.getPrintStream(); + page = page.replaceAll("%1", name); + out.println(page); + out.close(); + } catch(Exception e) { + e.printStackTrace(); + } + } else if(req.getTarget().equals("/talk")){ + long time = System.currentTimeMillis(); + try { + container.handle(req, resp); + } catch(Exception e) { + e.printStackTrace(); + } + } else { + long time = System.currentTimeMillis(); + + try { + resp.setCode(404); + resp.setDescription("Not Found"); + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketChatApplication/1.0"); + resp.setContentType("text/plain"); + + PrintStream out = resp.getPrintStream(); + + out.println("Not Found"); + out.close(); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + public String loadPage(String name) throws IOException { + InputStream loginPage = WebSocketChatApplication.class.getResourceAsStream(name); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] chunk = new byte[1024]; + int count = 0; + + while((count = loginPage.read(chunk)) != -1) { + out.write(chunk, 0, count); + } + out.close(); + return out.toString(); + } + + public void process(Transport transport) throws IOException { + Map map = transport.getAttributes(); + map.put(Transport.class, transport); + processor.process(transport); + } + + public void stop() throws IOException { + processor.stop(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatLogin.html b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatLogin.html new file mode 100644 index 0000000..4711494 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatLogin.html @@ -0,0 +1,12 @@ + + + Login Page + + +

Please Login

+
+ + +
+ + \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.html b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.html new file mode 100644 index 0000000..1443c69 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.html @@ -0,0 +1,29 @@ + + + WebSocket Echo Test + + + +

Welcome %1

Refresh browser to clear page and resubscribe

+ +
+ + \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.java new file mode 100644 index 0000000..6ca855a --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.java @@ -0,0 +1,86 @@ +package org.simpleframework.http.socket; + +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.simpleframework.http.Cookie; +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.WebSocketCertificate.KeyStoreReader; +import org.simpleframework.http.socket.service.Service; +import org.simpleframework.transport.trace.TraceAnalyzer; + +public class WebSocketChatRoom extends Thread implements Service { + + private final WebSocketChatRoomListener listener; + private final Map sockets; + private final Set users; + + public WebSocketChatRoom() { + this.listener = new WebSocketChatRoomListener(this); + this.sockets = new ConcurrentHashMap(); + this.users = new CopyOnWriteArraySet(); + } + + public void connect(Session connection) { + FrameChannel socket = connection.getChannel(); + Request req = connection.getRequest(); + Cookie user = req.getCookie("user"); + + if(user == null) { + user = new Cookie("user", "anonymous"); + } + String name = user.getValue(); + + try { + socket.register(listener); + join(name, socket); + } catch(Exception e) { + e.printStackTrace(); + } + + } + + public void join(String user, FrameChannel operation) { + sockets.put(user, operation); + users.add(user); + } + + public void leave(String user, FrameChannel operation){ + sockets.put(user, operation); + users.add(user); + } + + public void distribute(Frame frame) { + try { + for(String user : users) { + FrameChannel operation = sockets.get(user); + + try { + + operation.send(frame); + } catch(Exception e){ + sockets.remove(user); + users.remove(user); + e.printStackTrace(); + operation.close(); + } + } + } catch(Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] list) throws Exception { + TraceAnalyzer agent = new WebSocketAnalyzer(); + WebSocketChatRoom application = new WebSocketChatRoom(); + File file = new File("C:\\work\\development\\async_http\\proxy\\yieldbroker-proxy-site\\certificate\\www.yieldbroker.com.pfx"); + KeyStoreReader reader = new KeyStoreReader(WebSocketCertificate.KeyStoreType.PKCS12, file, "p", "p"); + WebSocketCertificate certificate = new WebSocketCertificate(reader, WebSocketCertificate.SecureProtocol.TLS); + WebSocketChatApplication container = new WebSocketChatApplication(application, certificate, agent, 6060); + application.start(); + container.connect(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoomListener.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoomListener.java new file mode 100644 index 0000000..e976b78 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoomListener.java @@ -0,0 +1,106 @@ +package org.simpleframework.http.socket; + +import java.security.Principal; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.security.cert.X509Certificate; + +import org.simpleframework.http.Request; +import org.simpleframework.transport.Certificate; + + +public class WebSocketChatRoomListener implements FrameListener { + + private final CertificateUserExtractor extractor; + private final WebSocketChatRoom room; + + public WebSocketChatRoomListener(WebSocketChatRoom room) { + this.extractor = new CertificateUserExtractor(".*EMAILADDRESS=(.*?),.*"); + this.room = room; + } + + public void onFrame(Session socket, Frame frame) { + FrameType type = frame.getType(); + String text = frame.getText(); + + if(type == FrameType.TEXT){ + try { + Request request = socket.getRequest(); + String user = extractor.extractUser(request); + + text = text + " (SSL=" + request.isSecure() + ", EMAILADDRESS=" + user + ")"; + } catch(Exception e) { + e.printStackTrace(); + } + Frame replay = new DataFrame(type, text); + room.distribute(replay); + } + } + + public void onError(Session socket, Exception cause) { + System.err.println("onError("); + cause.printStackTrace(); + System.err.println(")"); + } + + public void onOpen(Session socket) { + System.err.println("onOpen(" + socket +")"); + } + + public void onClose(Session session, Reason reason) { + System.err.println("onClose(" + reason +")"); + } + + public static class CertificateUserExtractor { + + private final Map cache; + private final Pattern pattern; + + public CertificateUserExtractor(String pattern) { + this.cache = new ConcurrentHashMap(); + this.pattern = Pattern.compile(pattern); + } + + public String extractUser(Request request) throws Exception { + try { + Certificate certificate = request.getClientCertificate(); + + if(certificate != null) { + X509Certificate[] certificates = certificate.getChain(); + + for(X509Certificate entry : certificates) { + String user = extractCertificateUser(entry); + + if(user != null) { + return user; + } + } + } + } catch(Exception e) { + e.printStackTrace(); + } + return null; + } + + private String extractCertificateUser(X509Certificate certificate) throws Exception { + Principal principal = certificate.getSubjectDN(); + String name = principal.getName(); + String user = cache.get(name); + + if(user == null) { + if(!cache.containsKey(name)) { + Matcher matcher = pattern.matcher(name); + + if(matcher.matches()) { + user = matcher.group(1); + } + cache.put(name, user); + } + } + return user; + } + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketKeyTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketKeyTest.java new file mode 100644 index 0000000..5e6f81e --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketKeyTest.java @@ -0,0 +1,37 @@ +package org.simpleframework.http.socket; + +import java.security.MessageDigest; + +import junit.framework.TestCase; + +import org.simpleframework.common.encode.Base64Encoder; + +public class WebSocketKeyTest extends TestCase { + + /* + From RFC 6455 + + Concretely, if as in the example above, the |Sec-WebSocket-Key| + header field had the value "dGhlIHNhbXBsZSBub25jZQ==", the server + would concatenate the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + to form the string "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA- + C5AB0DC85B11". The server would then take the SHA-1 hash of this, + giving the value 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 + 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea. This value is + then base64-encoded (see Section 4 of [RFC4648]), to give the value + "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=". This value would then be echoed in + the |Sec-WebSocket-Accept| header field. + */ + public void testKey() throws Exception { + String key = "dGhlIHNhbXBsZSBub25jZQ=="; + String result = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] data = result.getBytes("ISO-8859-1"); + digest.update(data); + byte[] digested = digest.digest(); + String value = new String(Base64Encoder.encode(digested)); + + assertEquals(value, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketTestClient.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketTestClient.java new file mode 100644 index 0000000..21c4abc --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketTestClient.java @@ -0,0 +1,25 @@ +package org.simpleframework.http.socket; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +public class WebSocketTestClient { + + public static void main(String[] list) throws Exception { + Socket socket = new Socket("localhost", 80); + OutputStream out = socket.getOutputStream(); + byte[] request = ("GET / HTTP/1.0\r\n\r\n").getBytes("ISO-8859-1"); + out.write(request); + InputStream in = socket.getInputStream(); + byte[] chunk = new byte[1024]; + int count = 0; + + while((count = in.read(chunk)) != -1) { + Thread.sleep(1000); + System.err.write(chunk, 0, count); + } + + + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/service/PathRouterTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/service/PathRouterTest.java new file mode 100644 index 0000000..95bc6ef --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/service/PathRouterTest.java @@ -0,0 +1,62 @@ +package org.simpleframework.http.socket.service; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.simpleframework.http.core.MockRequest; +import org.simpleframework.http.core.MockResponse; +import org.simpleframework.http.socket.Session; + +public class PathRouterTest extends TestCase { + + public static class A implements Service { + public void connect(Session session) {} + } + + public static class B implements Service { + public void connect(Session session) {} + } + + public static class C implements Service { + public void connect(Session session) {} + } + + public void testRouter() throws Exception{ + Map services = new HashMap(); + + services.put("/a", new A()); + services.put("/b", new B()); + + PathRouter router = new PathRouter(services, new C()); + MockRequest request = new MockRequest(); + MockResponse response = new MockResponse(); + + request.setTarget("/a"); + + Service service = router.route(request, response); + + assertNull(service); + + request.setValue("Sec-WebSocket-Version", "13"); + request.setValue("connection", "upgrade"); + request.setValue("upgrade", "WebSocket"); + + service = router.route(request, response); + + assertNotNull(service); + assertEquals(service.getClass(), A.class); + assertEquals(response.getValue("Sec-WebSocket-Version"), "13"); + + request.setTarget("/c"); + + service = router.route(request, response); + + assertNotNull(service); + assertEquals(service.getClass(), C.class); + assertEquals(response.getValue("Sec-WebSocket-Version"), "13"); + + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/service/WebSocketPerformanceTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/service/WebSocketPerformanceTest.java new file mode 100644 index 0000000..fbabeeb --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/service/WebSocketPerformanceTest.java @@ -0,0 +1,439 @@ +package org.simpleframework.http.socket.service; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.channels.SelectableChannel; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.common.thread.Daemon; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.Container; +import org.simpleframework.http.core.ContainerTransportProcessor; +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.http.core.ThreadDumper; +import org.simpleframework.http.message.ReplyConsumer; +import org.simpleframework.http.socket.DataFrame; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; +import org.simpleframework.http.socket.FrameChannel; +import org.simpleframework.http.socket.WebSocketAnalyzer; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.TransportSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; +import org.simpleframework.transport.trace.TraceAnalyzer; +import org.simpleframework.transport.trace.Trace; + +public class WebSocketPerformanceTest { + + public static class MessageCounter implements FrameListener { + + private final AtomicInteger counter; + + public MessageCounter(AtomicInteger counter) { + this.counter = counter; + } + + public void onFrame(Session socket, Frame frame) { + counter.getAndIncrement(); + } + + public void onError(Session socket, Exception cause) { + System.err.println("onError("); + cause.printStackTrace(); + System.err.println(")"); + } + + public void onOpen(Session socket) { + System.err.println("onOpen(" + socket +")"); + } + + public void onClose(Session session, Reason reason) { + System.err.println("onClose(" + reason +")"); + } + } + + public static class MessageGeneratorService extends Thread implements Service { + + private static final String MESSAGE = + "{'product': {\r\n"+ + " 'id': '1234',\r\n"+ + " 'name': 'AU3TB00001256',\r\n"+ + " 'values': {\r\n"+ + " 'best': [\r\n"+ + " {'bid': '13.344'},\r\n"+ + " {'offer': '12.1'},\r\n"+ + " {'volume': '100000'}\r\n"+ + " ]\r\n"+ + " }\r\n"+ + "}}"; + + private final Set sockets; + private final MessageCounter listener; + private final AtomicInteger counter; + private final AtomicBoolean begin; + + public MessageGeneratorService() { + this.sockets = new CopyOnWriteArraySet(); + this.counter = new AtomicInteger(); + this.listener = new MessageCounter(counter); + this.begin = new AtomicBoolean(); + } + + public void begin() { + if(begin.compareAndSet(false, true)) { + start(); + } + } + + public void connect(Session connection) { + FrameChannel socket = connection.getChannel(); + + try { + sockets.add(socket); + socket.register(listener); + } catch(Exception e) { + e.printStackTrace(); + } + } + + public void distribute(Frame frame) { + try { + for(FrameChannel socket : sockets) { + try { + socket.send(frame); + } catch(Exception e){ + e.printStackTrace(); + sockets.remove(socket); + socket.close(); + } + } + } catch(Exception e) { + e.printStackTrace(); + } + } + + public void run() { + try { + for(int i = 0; i < 10000000; i++) { + distribute(new DataFrame(FrameType.TEXT, System.currentTimeMillis() + ":" + MESSAGE)); + } + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + public static class MessageGeneratorContainer implements Container { + + private final RouterContainer container; + private final SocketAddress address; + private final Connection connection; + private final Allocator allocator; + private final TransportProcessor processor; + private final Router negotiator; + private final SocketProcessor server; + + public MessageGeneratorContainer(MessageGeneratorService service, TraceAnalyzer agent, int port) throws Exception { + this.negotiator = new DirectRouter(service); + this.container = new RouterContainer(this, negotiator, 10, 100000); + this.allocator = new ArrayAllocator(); + this.processor = new ContainerTransportProcessor(container, allocator, 10); + this.server = new TransportSocketProcessor(processor, 10, 8192*10); + this.connection = new SocketConnection(server, agent); + this.address = new InetSocketAddress(port); + } + + public void connect() throws Exception { + connection.connect(address); + } + + public void handle(Request req, Response resp) { + long time = System.currentTimeMillis(); + + System.err.println(req); + + try { + PrintStream out = resp.getPrintStream(); + + resp.setDate("Date", time); + resp.setValue("Server", "MessageGeneratorContainer/1.0"); + resp.setContentType("text/plain"); + resp.setDate("Date", time); + resp.setValue("Server", "MessageGeneratorContainer/1.0"); + resp.setContentType("text/html"); + + out.println("Your request is invalid as this is a websocket test!!"); + out.close(); + }catch(Exception e) { + e.printStackTrace(); + } + } + } + + public static class MessageGeneratorClient extends Thread { + + private final MessageGeneratorService service; + private final AtomicInteger counter; + private final AtomicLong duration; + private final int port; + private final boolean debug; + + public MessageGeneratorClient(MessageGeneratorService service, AtomicInteger counter, AtomicLong duration, int port, boolean debug) { + this.duration = duration; + this.counter = counter; + this.service = service; + this.debug = debug; + this.port = port; + } + + public void run() { + try { + Socket socket = new Socket("localhost", port); + StreamCursor cursor = new StreamCursor(socket.getInputStream()); + FrameConsumer consumer = new FrameConsumer(); + ReplyConsumer response = new ReplyConsumer(); + + byte[] request = ("GET /chat HTTP/1.1\r\n"+ + "Host: server.example.com\r\n"+ + "Upgrade: websocket\r\n"+ + "Connection: Upgrade\r\n"+ + "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"+ + "Sec-WebSocket-Protocol: chat, superchat\r\n"+ + "Sec-WebSocket-Version: 13\r\n"+ + "Origin: http://example.com\r\n" + + "\r\n").getBytes("ISO-8859-1"); + + socket.getOutputStream().write(request); + + while(cursor.isOpen()) { + response.consume(cursor); + + if(response.isFinished()) { + System.err.println(response); + break; + } + } + service.begin(); + + while(cursor.isOpen()) { + consumer.consume(cursor); + + if(consumer.isFinished()) { + Frame frame = consumer.getFrame(); + + if(frame != null) { + validate(frame); + } + consumer.clear(); + } + + if(!cursor.isReady()) { // wait for it to fill + Thread.sleep(1); + } + } + } catch(Exception e) { + e.printStackTrace(); + } + } + + public void validate(Frame frame) throws Exception { + FrameType type = frame.getType(); + + if(type == FrameType.TEXT) { + String text = frame.getText(); + int index = text.indexOf(':'); + String time = text.substring(0, index); + long sendTime = Long.parseLong(time); + long timeElapsed = System.currentTimeMillis() - sendTime; + + duration.getAndAdd(timeElapsed); + counter.getAndIncrement(); + + if(debug) { + System.err.println("count=" + counter + " text="+text + " time="+duration); + } + } + } + } + + public static class MessageTimer extends Thread { + + private final AtomicLong duration; + private final AtomicInteger counter; + + public MessageTimer(AtomicInteger counter, AtomicLong duration) { + this.duration = duration; + this.counter = counter; + } + + public void run() { + while(true) { + try { + Thread.sleep(1000); + int count = counter.getAndSet(0); + long total = duration.getAndSet(0); + long average = (total > 0 ? total : 1) / (count > 0 ? count : 1); + + System.err.println("framesPerSecond="+count+" millisPerFrame="+average); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + } + + public static class ConsoleAnalyzer extends Daemon implements TraceAnalyzer { + + private final Queue queue; + private final AtomicLong count; + private final String filter; + + public ConsoleAnalyzer() { + this(null); + } + + public ConsoleAnalyzer(String filter) { + this.queue = new ConcurrentLinkedQueue(); + this.count = new AtomicLong(); + this.filter = filter; + } + + public Trace attach(SelectableChannel channel) { + return new TraceFeeder(channel); + } + + public void run() { + try { + while(isActive()) { + Thread.sleep(1000); + + while(!queue.isEmpty()) { + TraceRecord record = queue.poll(); + + if(filter != null) { + Object event = record.event; + Class type = event.getClass(); + String name = type.getName(); + + if(name.contains(filter)) { + System.err.println(record); + } + } else { + System.err.println(record); + } + } + } + } catch(Exception e) { + e.printStackTrace(); + } + + } + + private class TraceFeeder implements Trace { + + private final SelectableChannel channel; + private final long sequence; + + public TraceFeeder(SelectableChannel channel) { + this.sequence = count.getAndIncrement(); + this.channel = channel; + } + + public void trace(Object event) { + trace(event, null); + } + + public void trace(Object event, Object value) { + TraceRecord record = new TraceRecord(channel, event, value, sequence); + + if(isActive()) { + queue.offer(record); + } + } + + } + + private class TraceRecord { + + private final SelectableChannel channel; + private final String thread; + private final Object event; + private final Object value; + private final long sequence; + + public TraceRecord(SelectableChannel channel, Object event, Object value, long sequence) { + this.thread = Thread.currentThread().getName(); + this.sequence = sequence; + this.channel = channel; + this.event = event; + this.value = value; + } + + public String toString() { + StringWriter builder = new StringWriter(); + PrintWriter writer = new PrintWriter(builder); + + writer.print(sequence); + writer.print(" ["); + writer.print(channel); + writer.print("]"); + writer.print(" "); + writer.print(thread); + writer.print(": "); + writer.print(event); + + if(value != null) { + if(value instanceof Throwable) { + ((Throwable)value).printStackTrace(writer); + } else { + writer.print(" -> "); + writer.print(value); + } + } + writer.close(); + return builder.toString(); + } + } + + } + + + public static void main(String[] list) throws Exception { + ThreadDumper dumper = new ThreadDumper(); + ConsoleAnalyzer agent = new ConsoleAnalyzer(); + AtomicLong duration = new AtomicLong(); + AtomicInteger counter = new AtomicInteger(); + MessageGeneratorService service = new MessageGeneratorService(); + MessageGeneratorContainer container = new MessageGeneratorContainer(service, agent, 7070); + MessageTimer timer = new MessageTimer(counter, duration); + + //agent.start(); + dumper.start(); + timer.start(); + container.connect(); + + for(int i = 0; i < 100; i++) { + MessageGeneratorClient client = new MessageGeneratorClient(service, counter, duration, 7070, false); + client.start(); + } + } +} + \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTable.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTable.java new file mode 100644 index 0000000..d6cd401 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTable.java @@ -0,0 +1,151 @@ +package org.simpleframework.http.socket.table; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class WebSocketTable { + + private final List rows; + private final WebSocketTableRowAnnotator annotator; + private final WebSocketTableSchema schema; + private final String key; + + public WebSocketTable(String key, WebSocketTableSchema schema, WebSocketTableRowAnnotator annotator) { + this.rows = new LinkedList(); + this.annotator = annotator; + this.schema = schema; + this.key = key; + } + + public String getKey(){ + return key; + } + + public WebSocketTableSchema getSchema(){ + return schema; + } + + public int getRows(){ + return rows.size(); + } + + public WebSocketTableRow updateRow(int index, String value) { + Map map = new HashMap(); + String[] cells = value.split(","); + for(String cell : cells){ + String[] pair = cell.split("="); + map.put(pair[0], pair[1]); + } + return updateRow(index, map); + + } + + public WebSocketTableRow updateRow(int index, Map data) { + WebSocketTableRow row = rows.get(index); + if(row != null) { + Set columns = data.keySet(); + for(String column : columns) { + if(!schema.validColumn(column)) { + throw new IllegalArgumentException("Schema does not match row " + data); + } + + } + for(String column : columns){ + String value = data.get(column); + WebSocketTableCell tableCell = row.getValue(column); + + if(tableCell == null) { + row.setValue(column, value); + } else { + if(!tableCell.getValue().equals(value)) { + row.setValue(column, value); + } + } + } + } + return row; + } + + public WebSocketTableRow addRow(String value) { + Map map = new HashMap(); + String[] cells = value.split(","); + for(String cell : cells){ + String[] pair = cell.split("="); + map.put(pair[0], pair[1]); + } + return addRow(map); + + } + + public WebSocketTableRow addRow(Map data) { + Set columns = data.keySet(); + for(String column : columns) { + if(!schema.validColumn(column)) { + throw new IllegalArgumentException("Schema does not match row " + data); + } + } + int length = rows.size(); + WebSocketTableRow row = new WebSocketTableRow(schema, length); + for(String column : columns){ + String value = data.get(column); + row.setValue(column, value); + } + rows.add(row); + return row; + } + + public WebSocketTableRow getRow(int row) { + int size = rows.size(); + + if(row < size) { + return rows.get(row); + } + return null; + } + + public String calculateHighlight(long since) { + StringBuilder builder = new StringBuilder(); + String delim = ""; + int size = rows.size(); + + for(int i = 0; i < size; i++) { + WebSocketTableRow row = rows.get(i); + long time = since; + String text = annotator.calculateHighlight(row, time); + + if(text != null && text.length() > 0) { + builder.append(delim); + builder.append(text); + delim = "|"; + } + } + return builder.toString(); + } + + public String calculateChange(long since) { + StringBuilder builder = new StringBuilder(); + String delim = ""; + int size = rows.size(); + + for(int i = 0; i < size; i++) { + WebSocketTableRow row = rows.get(i); + long time = since; + String text = row.calculateChange(time); + + if(text != null && text.length() > 0) { + builder.append(delim); + builder.append(text); + delim = "|"; + } + } + return builder.toString(); + } + + public void clearTable() { + rows.clear(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableCell.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableCell.java new file mode 100644 index 0000000..24dfbec --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableCell.java @@ -0,0 +1,26 @@ +package org.simpleframework.http.socket.table; + +public class WebSocketTableCell { + + private final long timeStamp; + private final String column; + private final String value; + + public WebSocketTableCell(String column, String value) { + this.timeStamp = System.currentTimeMillis(); + this.value = value; + this.column = column; + } + + public long getTimeStamp(){ + return timeStamp; + } + + public String getValue(){ + return value; + } + + public String getColumn(){ + return column; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableChanger.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableChanger.java new file mode 100644 index 0000000..5a77537 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableChanger.java @@ -0,0 +1,58 @@ +package org.simpleframework.http.socket.table; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class WebSocketTableChanger { + + private final Map currentRows; + private final WebSocketValueEncoder encoder; + private final WebSocketTable table; + + public WebSocketTableChanger(WebSocketTable table) { + this.currentRows = new ConcurrentHashMap(); + this.encoder = new WebSocketValueEncoder(); + this.table = table; + } + + public void onChange(Map values) { + Map row = new HashMap(); + Map header = new HashMap(); + Set columns = values.keySet(); + + for (String column : columns) { + Object value = values.get(column); + String encoded = encoder.encode(value); + + row.put(column, encoded); + header.put(column, column); + } + WebSocketTableRow headerRow = table.getRow(0); + + if (headerRow == null) { + table.addRow(header); + } else { + for (String column : columns) { + String name = header.get(column); + headerRow.setValue(column, name); + } + } + String key = table.getKey(); + Object keyAttribute = values.get(key); + + if (keyAttribute != null) { + String tableKey = String.valueOf(keyAttribute); + Integer index = currentRows.get(tableKey); + + if (index == null) { + WebSocketTableRow newRow = table.addRow(row); + index = newRow.getIndex(); + currentRows.put(tableKey, index); + } else { + table.updateRow(index, row); + } + } + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumn.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumn.java new file mode 100644 index 0000000..966d92f --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumn.java @@ -0,0 +1,22 @@ +package org.simpleframework.http.socket.table; + +public enum WebSocketTableColumn { + BID_OUTRIGHT_VOLUME("bov", "Bid Outright Volume"), + OFFER_OUTRIGHT_VOLUME("bov", "Offer Outright Volume"), + BID_OUTRIGHT("bo", "Bid Outright"), + OFFER_OUTRIGHT("bov", "Offer Outright"), + BID_EFP_VOLUME("bov", "Bid EFP Volume"), + OFFER_EFP_VOLUME("bov", "Offer EFP Volume"), + BID_EFP("bo", "Bid EFP"), + OFFER_EFP("bov", "Offer EFP"), + PRODUCT("p", "Product"); + + public final String name; + public final String title; + + private WebSocketTableColumn(String name, String title) { + this.name = name; + this.title = title; + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumnStyle.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumnStyle.java new file mode 100644 index 0000000..f514295 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumnStyle.java @@ -0,0 +1,35 @@ +package org.simpleframework.http.socket.table; + +public class WebSocketTableColumnStyle { + + private final String template; + private final String caption; + private final String name; + private final boolean sortable; + private final boolean resizable; + + public WebSocketTableColumnStyle(String name, String caption, String template, boolean resizable, boolean sortable) { + this.name = name; + this.caption = caption; + this.template = template; + this.resizable = resizable; + this.sortable = sortable; + } + + public String createStyle() { + StringBuilder builder = new StringBuilder(); + WebSocketValueEncoder encoder = new WebSocketValueEncoder(); + + builder.append(name); + builder.append(","); + builder.append(encoder.encode(caption)); + builder.append(","); + builder.append(encoder.encode(template)); + builder.append(","); + builder.append(resizable); + builder.append(","); + builder.append(sortable); + + return builder.toString(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableListener.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableListener.java new file mode 100644 index 0000000..d099f27 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableListener.java @@ -0,0 +1,69 @@ +package org.simpleframework.http.socket.table; + +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; + +public class WebSocketTableListener implements FrameListener { + + private final WebSocketTableUpdater updater; + + public WebSocketTableListener(WebSocketTableUpdater updater) { + this.updater = updater; + } + + public void onFrame(Session socket, Frame frame) { + FrameType type = frame.getType(); + + if(type != FrameType.PONG && type != FrameType.PING) { + + if(type == FrameType.TEXT) { + String text = frame.getText(); + String[] command = text.split(":"); + String operation = command[0]; + String parameters = command[1]; + String[] values = parameters.split(","); + + if(values.length > 0) { + for(String value : values) { + String[] pair = value.split("="); + + if(operation.equals("refresh")) { + updater.refresh(socket); + }else if(operation.equals("status")) { + System.err.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + value); + + if(pair[0].equals("sequence")) { + if(pair[1].indexOf("@") != -1) { + String time = pair[1].split("@")[1]; + Long sent = Long.parseLong(time); + + System.err.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> TIME RTT: " + (System.currentTimeMillis() - sent)); + } + } + } + } + } + } + System.err.println("onFrame("); + System.err.println(frame.getText()); + System.err.println(")"); + } + } + + public void onError(Session socket, Exception cause) { + System.err.println("onError("); + cause.printStackTrace(); + System.err.println(")"); + } + + public void onOpen(Session socket) { + System.err.println("onOpen(" + socket +")"); + } + + public void onClose(Session session, Reason reason) { + System.err.println("onClose(" + reason +" reason="+reason.getText()+" code="+reason.getCode()+")"); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRow.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRow.java new file mode 100644 index 0000000..bb8a6db --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRow.java @@ -0,0 +1,84 @@ +package org.simpleframework.http.socket.table; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class WebSocketTableRow { + + private final Map cells; + private final WebSocketValueEncoder encoder; + private final WebSocketTableSchema schema; + private final int index; + + public WebSocketTableRow(WebSocketTableSchema schema, int index) { + this.cells = new ConcurrentHashMap(); + this.encoder = new WebSocketValueEncoder(); + this.index = index; + this.schema = schema; + } + + public int getIndex(){ + return index; + } + + public void setValue(String column, String value){ + WebSocketTableCell cell = getValue(column); + + if(cell == null) { + WebSocketTableCell newCell = new WebSocketTableCell(column, value); + List columns = schema.columnNames(); + boolean match = false; + for(String name : columns) { + if(name.equals(column)) { + match = true; + } + } + if(!match) { + throw new IllegalStateException("Could not find " + column + " in schema"); + } + cells.put(column, newCell); + } else { + String previous = cell.getValue(); + + if(previous != null && !previous.equals(value)) { + WebSocketTableCell replaceCell = new WebSocketTableCell(column, value); + cells.put(column, replaceCell); + } + } + } + + public WebSocketTableCell getValue(String column) { + return cells.get(column); + } + + public String calculateChange(long lastUpdateDone) { + StringBuilder builder = new StringBuilder(); + builder.append(index); + builder.append(":"); + String delim = ""; + int count = 0; + List columns = schema.columnNames(); + for(int i = 0; i < columns.size(); i++){ + String column = columns.get(i); + WebSocketTableCell cell = cells.get(column); + if(cell != null) { + long cellChanged = cell.getTimeStamp(); + long difference = cellChanged - lastUpdateDone; + + if(difference > 0) { // positive means change happened later than update + builder.append(delim); + builder.append(i); + builder.append("="); + builder.append(cell.getValue()); + count++; + delim = ","; + } + } + } + if(count <= 0) { + return ""; + } + return builder.toString(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowAnnotator.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowAnnotator.java new file mode 100644 index 0000000..0ef307e --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowAnnotator.java @@ -0,0 +1,89 @@ +package org.simpleframework.http.socket.table; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class WebSocketTableRowAnnotator { + + private final WebSocketTableSchema schema; + private final WebSocketValueEncoder encoder; + + public WebSocketTableRowAnnotator(WebSocketTableSchema schema) { + this.encoder = new WebSocketValueEncoder(); + this.schema = schema; + } + + public String calculateHighlight(WebSocketTableRow row, long lastUpdateDone) { + Map pairs = new LinkedHashMap(); + Map values = new LinkedHashMap(); + + pairs.put("bidEFP", "bidEFPVolume"); + pairs.put("bidEFPVolume", "bidEFP"); + pairs.put("offerEFP", "offerEFPVolume"); + pairs.put("offerEFPVolume", "offerEFP"); + pairs.put("bidOutright", "bidOutrightVolume"); + pairs.put("bidOutrightVolume", "bidOutright"); + pairs.put("offerOutright", "offerOutrightVolume"); + pairs.put("offerOutrightVolume", "offerOutright"); + + StringBuilder builder = new StringBuilder(); + int index = row.getIndex(); + builder.append(index); + builder.append(":"); + String delim = ""; + int count = 0; + List columns = schema.columnNames(); + for(int i = 0; i < columns.size(); i++){ + String column = columns.get(i); + WebSocketTableCell cell = row.getValue(column); + + if(cell == null) { + throw new IllegalStateException("Could not find column " + column); + } + String value = cell.getValue(); + + if(cell != null) { + long cellChanged = cell.getTimeStamp(); + long difference = cellChanged - lastUpdateDone; + + if(difference > 0 || values.containsKey(column)) { // positive means change happened later than update + builder.append(delim); + builder.append(i); + builder.append("="); + String doneAlready = values.get(column); + + if(doneAlready != null) { + builder.append(doneAlready); + } else { + if(String.valueOf(value).indexOf("20") != -1 && (column.indexOf("bid") != -1 || column.indexOf("offer") != -1)) { + String style = encoder.encode("background-color: #32cd32;"); + String other = pairs.get(column); + + if(other != null) { + values.put(other, style); + } + values.put(column, style); + builder.append(style); + } else { + String style = encoder.encode(""); + String other = pairs.get(column); + + if(other != null) { + values.put(other, style); + } + values.put(column, style); + builder.append(style); + } + } + count++; + delim = ","; + } + } + } + if(count <= 0) { + return ""; + } + return builder.toString(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowChanger.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowChanger.java new file mode 100644 index 0000000..bdc6755 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowChanger.java @@ -0,0 +1,73 @@ +package org.simpleframework.http.socket.table; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class WebSocketTableRowChanger extends Thread { + + private final WebSocketTableChanger changer; + + public WebSocketTableRowChanger(WebSocketTable table) { + this.changer = new WebSocketTableChanger(table); + } + + public void run() { + Random random = new SecureRandom(); + List products = new ArrayList(); + + products.add("QTC44"); + products.add("NSW22"); + products.add("NSW23"); + products.add("NSW24"); + products.add("WAGA19"); + products.add("CGS19"); + products.add("CGS21"); + products.add("CGS22"); + products.add("CGSJun14"); + products.add("CGSOct14"); + products.add("CGSDec12"); + products.add("QTC33"); + + for(int i = 0; i < 100; i++) { + products.add("CGS" + i); + } + + while(true) { + try { + int rows = products.size(); + long randomWait = random.nextInt(50) + 40; + int randomRow = random.nextInt(rows); + int randomBid = random.nextInt(50) + 1; + int randomOffer = random.nextInt(50) + 1; + int randomVolume = (random.nextInt(5) + 1) * 10; + + if(randomRow != 0) { + String product = products.get(randomRow); + Map map = new HashMap(); + + map.put("id", randomRow); + map.put("product", product); + map.put("bidOutrightVolume", randomVolume); + map.put("bidOutright", randomBid); + map.put("offerOutright", randomOffer); + map.put("offerOutrightVolume", randomVolume); + map.put("bidEFPVolume", randomVolume); + map.put("bidEFP", randomBid); + map.put("offerEFP", randomOffer); + map.put("offerEFPVolume", randomVolume); + map.put("reference", "3ySep"); + + changer.onChange(map); + } + Thread.sleep(randomWait); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSchema.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSchema.java new file mode 100644 index 0000000..c8f4a68 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSchema.java @@ -0,0 +1,40 @@ +package org.simpleframework.http.socket.table; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class WebSocketTableSchema { + + private final Map columns; + + public WebSocketTableSchema(Map columns) { + this.columns = columns; + } + + public List columnNames(){ + return new ArrayList(columns.keySet()); + } + + public boolean validColumn(String name) { + return columns.containsKey(name); + } + + public String createStyle() { + StringBuilder builder = new StringBuilder(); + Set keys = columns.keySet(); + int count = 0; + + for(String key : keys){ + WebSocketTableColumnStyle style = columns.get(key); + String columnStyle = style.createStyle(); + + if(count++ > 0) { + builder.append("|"); + } + builder.append(columnStyle); + } + return builder.toString(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSubscription.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSubscription.java new file mode 100644 index 0000000..b8402eb --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSubscription.java @@ -0,0 +1,44 @@ +package org.simpleframework.http.socket.table; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import org.simpleframework.http.socket.FrameChannel; + +public class WebSocketTableSubscription { + + private final Set missedUpdates; + private final AtomicLong timeStamp; + private final FrameChannel socket; + private final AtomicLong send; + private final AtomicLong received; + + public WebSocketTableSubscription(FrameChannel socket) { + this.timeStamp = new AtomicLong(); + this.received = new AtomicLong(); + this.send = new AtomicLong(); + this.missedUpdates = new HashSet(); + this.socket = socket; + } + + public Set getMissedUpdates() { + return missedUpdates; + } + + public AtomicLong getSendCount() { + return send; + } + + public AtomicLong getReceiveCount() { + return received; + } + + public AtomicLong getTimeStamp() { + return timeStamp; + } + + public FrameChannel getSocket() { + return socket; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSweeper.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSweeper.java new file mode 100644 index 0000000..5f09efa --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSweeper.java @@ -0,0 +1,33 @@ +package org.simpleframework.http.socket.table; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class WebSocketTableSweeper { + + private final WebSocketTable table; + + public WebSocketTableSweeper(WebSocketTable table) { + this.table = table; + } + + public Map sweep(long time, long count) { + Map messages = new LinkedHashMap(); + + if(count <= 1) { + WebSocketTableSchema schema = table.getSchema(); + String schemaUpdate = schema.createStyle(); + messages.put(WebSocketTableUpdateType.SCHEMA, schemaUpdate); + } + String highlightUpdate = table.calculateHighlight(time); + String deltaUpdate = table.calculateChange(time);// really should only take small batches... + + highlightUpdate = count + "@" + System.currentTimeMillis() + ":" + highlightUpdate; + deltaUpdate = count + "@" + System.currentTimeMillis() + ":" + deltaUpdate; + + messages.put(WebSocketTableUpdateType.HIGHLIGHT, highlightUpdate); + messages.put(WebSocketTableUpdateType.DELTA, deltaUpdate); + + return messages; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdateType.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdateType.java new file mode 100644 index 0000000..d7780e7 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdateType.java @@ -0,0 +1,17 @@ +package org.simpleframework.http.socket.table; + +public enum WebSocketTableUpdateType { + SCHEMA('S'), + HIGHLIGHT('H'), + DELTA('D'); + + public final char code; + + private WebSocketTableUpdateType(char code) { + this.code = code; + } + + public char getCode() { + return code; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdater.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdater.java new file mode 100644 index 0000000..0c4ec4d --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdater.java @@ -0,0 +1,126 @@ +package org.simpleframework.http.socket.table; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicLong; + +import org.simpleframework.http.socket.Session; +import org.simpleframework.http.socket.FrameChannel; +import org.simpleframework.http.socket.WebSocketAnalyzer; +import org.simpleframework.http.socket.service.Service; +import org.simpleframework.transport.trace.TraceAnalyzer; + +public class WebSocketTableUpdater extends Thread implements Service { + + private final Set subscriptions; + private final WebSocketTableListener listener; + private final WebSocketTableRowChanger changer; + private final WebSocketTableSweeper sweeper; + private final WebSocketTable table; + private final AtomicLong time; + + public WebSocketTableUpdater(String key, WebSocketTableSchema schema, WebSocketTableRowAnnotator annotator) { + this.subscriptions = new CopyOnWriteArraySet(); + this.table = new WebSocketTable(key, schema, annotator); + this.sweeper = new WebSocketTableSweeper(table); + this.changer = new WebSocketTableRowChanger(table); + this.listener = new WebSocketTableListener(this); + this.time = new AtomicLong(); + } + + public void refresh(Session session) { + for(WebSocketTableSubscription subscription : subscriptions) { + FrameChannel socket = subscription.getSocket(); + FrameChannel other = session.getChannel(); + + if(socket == other) { + AtomicLong timeStamp = subscription.getTimeStamp(); + timeStamp.set(0); + } + } + } + + public void run() { + changer.start(); + + while(true) { + try { + Thread.sleep(200); + + for(WebSocketTableSubscription subscription : subscriptions) { + FrameChannel socket = subscription.getSocket(); + AtomicLong timeStamp = subscription.getTimeStamp(); + AtomicLong sendCount = subscription.getSendCount(); + long before = System.currentTimeMillis(); + long time = timeStamp.get(); + long count = sendCount.get(); + + try { + Map messages = sweeper.sweep(time - 1000, count); + Set updates = messages.keySet(); + + for(WebSocketTableUpdateType update : updates) { + String message = messages.get(update); + + if(message != null) { + socket.send(update.code + message); + } + } + } catch(Exception e) { + e.printStackTrace(); + subscriptions.remove(subscription); + socket.close(); + } finally { + sendCount.getAndIncrement(); + timeStamp.set(before); + } + } + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + public void connect(Session connection) { + FrameChannel socket = connection.getChannel(); + + try { + WebSocketTableSubscription subscription = new WebSocketTableSubscription(socket); + + socket.register(listener); + subscriptions.add(subscription); + time.set(0); + Thread.sleep(1000); // crap + time.set(0); + } catch(Exception e) { + e.printStackTrace(); + } + + } + + public static void main(String[] list) throws Exception { + TraceAnalyzer agent = new WebSocketAnalyzer(); + Map columns = new LinkedHashMap(); + + WebSocketTableSchema schema = new WebSocketTableSchema(columns); + columns.put("id", new WebSocketTableColumnStyle("id", "Id", "{id}", true, true)); + columns.put("bidOutrightVolume", new WebSocketTableColumnStyle("bidOutrightVolume", "$ B", "
{bidOutrightVolume}", true, false)); + columns.put("bidOutright", new WebSocketTableColumnStyle("bidOutright", "Bid", "
{bidOutright}", true, false)); + columns.put("offerOutright", new WebSocketTableColumnStyle("offerOutright", "Offer", "
{offerOutright}", true, false)); + columns.put("offerOutrightVolume", new WebSocketTableColumnStyle("offerOutrightVolume", "$ O", "
{offerOutrightVolume}", true, false)); + columns.put("product", new WebSocketTableColumnStyle("product", "Security", "
{product}
", true, true)); + columns.put("bidEFPVolume", new WebSocketTableColumnStyle("bidEFPVolume", "$ B", "
{bidEFPVolume}", true, false)); + columns.put("bidEFP", new WebSocketTableColumnStyle("bidEFP", "Bid", "
{bidEFP}", true, false)); + columns.put("offerEFP", new WebSocketTableColumnStyle("offerEFP", "Offer", "
{offerEFP}", true, false)); + columns.put("offerEFPVolume", new WebSocketTableColumnStyle("offerEFPVolume", "$ O", "
{offerEFPVolume}", true, false)); + columns.put("reference", new WebSocketTableColumnStyle("reference", "Ref", "{reference}", true, true)); + WebSocketTableRowAnnotator annotator = new WebSocketTableRowAnnotator(schema); + WebSocketTableUpdater application = new WebSocketTableUpdater("product", schema, annotator); + + WebSocketTableUpdaterApplication container = new WebSocketTableUpdaterApplication(application, agent, 6060); + application.start(); + container.connect(); + } +} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdaterApplication.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdaterApplication.java new file mode 100644 index 0000000..586c82f --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdaterApplication.java @@ -0,0 +1,154 @@ +package org.simpleframework.http.socket.table; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Map; + +import org.simpleframework.common.buffer.Allocator; +import org.simpleframework.common.buffer.ArrayAllocator; +import org.simpleframework.http.Path; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.Status; +import org.simpleframework.http.core.Container; +import org.simpleframework.http.core.ContainerTransportProcessor; +import org.simpleframework.http.socket.service.Router; +import org.simpleframework.http.socket.service.RouterContainer; +import org.simpleframework.http.socket.service.DirectRouter; +import org.simpleframework.transport.TransportProcessor; +import org.simpleframework.transport.TransportSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.Transport; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; +import org.simpleframework.transport.trace.TraceAnalyzer; + +public class WebSocketTableUpdaterApplication implements Container, TransportProcessor { + + private final String ROOT_PATH = "C:\\Work\\development\\github\\simpleframework\\simple\\simple-http\\src\\test\\java\\org\\simpleframework\\http\\socket\\table"; + + private final Router negotiator; + private final RouterContainer container; + private final SocketAddress address; + private final Connection connection; + private final TransportProcessor processor; + private final Allocator allocator; + private final SocketProcessor server; + + public WebSocketTableUpdaterApplication(WebSocketTableUpdater handler, TraceAnalyzer agent, int port) throws Exception { + this.negotiator = new DirectRouter(handler); + this.container = new RouterContainer(this, negotiator, 10); + this.allocator = new ArrayAllocator(); + this.processor = new ContainerTransportProcessor(container, allocator, 1); + this.server = new TransportSocketProcessor(this); + this.connection = new SocketConnection(server, agent); + this.address = new InetSocketAddress(port); + } + + public void connect() throws IOException { + connection.connect(address); + } + + public void handle(Request req, Response resp) { + Path path = req.getPath(); + String normal = path.getPath(); + + System.err.println(req); + + if(req.getTarget().equals("/login")) { + String user = req.getParameter("user"); + long time = System.currentTimeMillis(); + + try { + resp.setStatus(Status.FOUND); + resp.setValue("Location", "/table"); + resp.setCookie("user", user); + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketTableApplication/1.0"); + resp.setContentType("text/html"); + resp.close(); + }catch(Exception e) { + e.printStackTrace(); + } + } else if(req.getTarget().equals("/update")){ + long time = System.currentTimeMillis(); + try { + container.handle(req, resp); + } catch(Exception e) { + e.printStackTrace(); + } + } else { + long time = System.currentTimeMillis(); + + try { + byte[] page = loadPage(normal); + + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketTableApplication/1.0"); + + if(normal.endsWith(".html")) { + resp.setContentType("text/html"); + } else if(normal.endsWith(".css")) { + resp.setContentType("text/css"); + } else if(normal.endsWith(".js")) { + resp.setContentType("text/javascript"); + } else if(normal.endsWith(".png")) { + resp.setContentType("image/png"); + } else { + resp.setContentType("text/plain"); + } + OutputStream out = resp.getOutputStream(); + out.write(page); + out.close(); + }catch(Exception e) { + e.printStackTrace(); + + try { + resp.setCode(404); + resp.setDescription("Not Found"); + resp.setDate("Date", time); + resp.setValue("Server", "WebSocketTableApplication/1.0"); + resp.setContentType("text/plain"); + + PrintStream out = resp.getPrintStream(); + + e.printStackTrace(out); + out.close(); + } catch(Exception ex) { + ex.printStackTrace(); + } + } + + } + } + + public byte[] loadPage(String name) throws IOException { + InputStream loginPage = new FileInputStream(new File(ROOT_PATH, name)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] chunk = new byte[1024]; + int count = 0; + + while((count = loginPage.read(chunk)) != -1) { + out.write(chunk, 0, count); + } + out.close(); + return out.toByteArray(); + } + + public void process(Transport transport) throws IOException { + Map map = transport.getAttributes(); + map.put(Transport.class, transport); + processor.process(transport); + } + + public void stop() throws IOException { + processor.stop(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketValueEncoder.java b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketValueEncoder.java new file mode 100644 index 0000000..7afa93b --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketValueEncoder.java @@ -0,0 +1,53 @@ +package org.simpleframework.http.socket.table; + +public class WebSocketValueEncoder { + + public String encode(Object value) { + if(value instanceof String) { + String text = String.valueOf(value); + + if(containsAnyOf(text, "<>|:=,")) { + StringBuffer buffer = new StringBuffer("<"); + + for(int i = 0; i < text.length(); i++){ + char ch = text.charAt(i); + String hex = Integer.toHexString(ch); + + buffer.append(hex); + } + return buffer.toString(); + } + } + return ">" + value; + } + + public String decode(String text) { + String value = text.substring(1); + + if(text.startsWith("?")) { + StringBuilder buffer = new StringBuilder(); + + for(int i = 0; i < value.length() - 1; i += 2){ + String output = value.substring(i, (i + 2)); + int decimal = Integer.parseInt(output, 16); + + buffer.append((char)decimal); + } + return buffer.toString(); + } + return value; + } + + public boolean containsAnyOf(String text, String chars) { + int length = chars.length(); + + for(int i = 0; i < length; i++) { + char value = chars.charAt(i); + + if(text.indexOf(value) != -1) { + return true; + } + } + return false; + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.css b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.css new file mode 100644 index 0000000..20bf961 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.css @@ -0,0 +1,5774 @@ +/*! + * Bootstrap v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +.text-warning { + color: #c09853; +} + +.text-error { + color: #b94a48; +} + +.text-info { + color: #3a87ad; +} + +.text-success { + color: #468847; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 1; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1 { + font-size: 36px; + line-height: 40px; +} + +h2 { + font-size: 30px; + line-height: 40px; +} + +h3 { + font-size: 24px; + line-height: 40px; +} + +h4 { + font-size: 18px; + line-height: 20px; +} + +h5 { + font-size: 14px; + line-height: 20px; +} + +h6 { + font-size: 12px; + line-height: 20px; +} + +h1 small { + font-size: 24px; +} + +h2 small { + font-size: 18px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 25px; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 9px; + font-size: 14px; + line-height: 20px; + color: #555555; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; + cursor: pointer; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 18px; + padding-left: 18px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"] { + float: left; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning > label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error > label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success > label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info > label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:required:invalid, +textarea:focus:required:invalid, +select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:required:invalid:focus, +textarea:focus:required:invalid:focus, +select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + margin-bottom: 5px; + font-size: 0; + white-space: nowrap; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + font-size: 14px; + vertical-align: top; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append .add-on, +.input-append .btn { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child th:first-child, +.table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child th:last-child, +.table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child th:first-child, +.table-bordered tbody:last-child tr:last-child td:first-child, +.table-bordered tfoot:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child th:last-child, +.table-bordered tbody:last-child tr:last-child td:last-child, +.table-bordered tfoot:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-striped tbody tr:nth-child(odd) td, +.table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover td, +.table-hover tbody tr:hover th { + background-color: #f5f5f5; +} + +table [class*=span], +.row-fluid table [class*=span] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table .span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table .span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table .span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table .span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table .span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table .span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table .span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table .span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table .span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table .span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table .span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table .span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table .span13 { + float: none; + width: 1004px; + margin-left: 0; +} + +.table .span14 { + float: none; + width: 1084px; + margin-left: 0; +} + +.table .span15 { + float: none; + width: 1164px; + margin-left: 0; +} + +.table .span16 { + float: none; + width: 1244px; + margin-left: 0; +} + +.table .span17 { + float: none; + width: 1324px; + margin-left: 0; +} + +.table .span18 { + float: none; + width: 1404px; + margin-left: 0; +} + +.table .span19 { + float: none; + width: 1484px; + margin-left: 0; +} + +.table .span20 { + float: none; + width: 1564px; + margin-left: 0; +} + +.table .span21 { + float: none; + width: 1644px; + margin-left: 0; +} + +.table .span22 { + float: none; + width: 1724px; + margin-left: 0; +} + +.table .span23 { + float: none; + width: 1804px; + margin-left: 0; +} + +.table .span24 { + float: none; + width: 1884px; + margin-left: 0; +} + +.table tbody tr.success td { + background-color: #dff0d8; +} + +.table tbody tr.error td { + background-color: #f2dede; +} + +.table tbody tr.warning td { + background-color: #fcf8e3; +} + +.table tbody tr.info td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/active states of certain elements */ + +.icon-white, +.nav-tabs > .active > a > [class^="icon-"], +.nav-tabs > .active > a > [class*=" icon-"], +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu li > a:hover, +.dropdown-menu li > a:focus, +.dropdown-submenu:hover > a { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; + background-color: #0081c2; + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .disabled > a, +.dropdown-menu .disabled > a:hover { + color: #999999; +} + +.dropdown-menu .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 14px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + *line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #bbbbbb; + *border: 0; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-bottom-color: #a2a2a2; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + *background-color: #d9d9d9; + /* Buttons in IE7 don't get borders, so darken on hover */ + + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-color: #e6e6e6; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 9px 14px; + font-size: 16px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.btn-large [class^="icon-"] { + margin-top: 2px; +} + +.btn-small { + padding: 3px 9px; + font-size: 12px; + line-height: 18px; +} + +.btn-small [class^="icon-"] { + margin-top: 0; +} + +.btn-mini { + padding: 2px 6px; + font-size: 11px; + line-height: 17px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn { + border-color: #c5c5c5; + border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-image: -moz-linear-gradient(top, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-toolbar .btn + .btn, +.btn-toolbar .btn-group + .btn, +.btn-toolbar .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 11px; +} + +.btn-group > .btn-small { + font-size: 12px; +} + +.btn-group > .btn-large { + font-size: 16px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-mini .caret, +.btn-small .caret, +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.dropup .btn-large .caret { + border-top: 0; + border-bottom: 5px solid #000000; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical .btn { + display: block; + float: none; + width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + color: #c09853; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover { + color: #fff; + cursor: default; + background-color: #0093FF; + border: 1px solid #0093FF !important; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; + color: #777777; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 6px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + width: 100%; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse { + color: #999999; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #333, #222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333), to(#222)); + background-image: -webkit-linear-gradient(top, #333, #222); + background-image: -o-linear-gradient(top, #333, #222); + background-image: linear-gradient(to bottom, #333, #222); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-image: -moz-linear-gradient(top, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb .active { + color: #999999; +} + +.pagination { + height: 40px; + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 0 14px; + line-height: 38px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager a, +.pager span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next a, +.pager .next span { + float: right; +} + +.pager .previous a { + float: left; +} + +.pager .disabled a, +.pager .disabled a:hover, +.pager .disabled span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-open .modal .dropdown-menu { + z-index: 2050; +} + +.modal-open .modal .dropdown.open { + *z-index: 2050; +} + +.modal-open .modal .popover { + z-index: 2060; +} + +.modal-open .modal .tooltip { + z-index: 2080; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + width: 560px; + margin: -250px 0 0 -280px; + overflow: auto; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 50%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + margin-top: -3px; +} + +.tooltip.right { + margin-left: 3px; +} + +.tooltip.bottom { + margin-top: 3px; +} + +.tooltip.left { + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + width: 236px; + padding: 1px; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-bottom: 10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-right: 10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover-content p, +.popover-content ul, +.popover-content ol { + margin-bottom: 0; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: inline-block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow:after { + z-index: -1; + content: ""; +} + +.popover.top .arrow { + bottom: -10px; + left: 50%; + margin-left: -10px; + border-top-color: #ffffff; + border-width: 10px 10px 0; +} + +.popover.top .arrow:after { + bottom: -1px; + left: -11px; + border-top-color: rgba(0, 0, 0, 0.25); + border-width: 11px 11px 0; +} + +.popover.right .arrow { + top: 50%; + left: -10px; + margin-top: -10px; + border-right-color: #ffffff; + border-width: 10px 10px 10px 0; +} + +.popover.right .arrow:after { + bottom: -11px; + left: -1px; + border-right-color: rgba(0, 0, 0, 0.25); + border-width: 11px 11px 11px 0; +} + +.popover.bottom .arrow { + top: -10px; + left: 50%; + margin-left: -10px; + border-bottom-color: #ffffff; + border-width: 0 10px 10px; +} + +.popover.bottom .arrow:after { + top: -1px; + left: -11px; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-width: 0 11px 11px; +} + +.popover.left .arrow { + top: 50%; + right: -10px; + margin-top: -10px; + border-left-color: #ffffff; + border-width: 10px 0 10px 10px; +} + +.popover.left .arrow:after { + right: -1px; + bottom: -11px; + border-left-color: rgba(0, 0, 0, 0.25); + border-width: 11px 0 11px 11px; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.label, +.badge { + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + padding: 1px 4px 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding: 1px 9px 2px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +a.label:hover, +a.badge:hover { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel .item > img { + display: block; + line-height: 1; +} + +.carousel .active, +.carousel .next, +.carousel .prev { + display: block; +} + +.carousel .active { + left: 0; +} + +.carousel .next, +.carousel .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel .next { + left: 100%; +} + +.carousel .prev { + left: -100%; +} + +.carousel .next.left, +.carousel .prev.right { + left: 0; +} + +.carousel .active.left { + left: -100%; +} + +.carousel .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.min.js b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.min.js new file mode 100644 index 0000000..0e33fb1 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.min.js @@ -0,0 +1,6 @@ +/*! +* Bootstrap.js by @fat & @mdo +* Copyright 2012 Twitter, Inc. +* http://www.apache.org/licenses/LICENSE-2.0.txt +*/ +!function(e){e(function(){"use strict";e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()},e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e(function(){e("body").on("click.alert.data-api",t,n.prototype.close)})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")},e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e(function(){e("body").on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=n,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},to:function(t){var n=this.$element.find(".item.active"),r=n.parent().children(),i=r.index(n),s=this;if(t>r.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){s.to(t)}):i==t?this.pause().cycle():this.slide(t>i?"next":"prev",e(r[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f=e.Event("slide",{relatedTarget:i[0]});this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u]();if(i.hasClass("active"))return;if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}},e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e(function(){e("body").on("click.carousel.data-api","[data-slide]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=!i.data("modal")&&e.extend({},i.data(),n.data());i.carousel(s),t.preventDefault()})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning)return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning)return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=typeof n=="object"&&n;i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e(function(){e("body").on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})})}(window.jQuery),!function(e){"use strict";function r(){i(e(t)).removeClass("open")}function i(t){var n=t.attr("data-target"),r;return n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=e(n),r.length||(r=t.parent()),r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||(s.toggleClass("open"),n.focus()),!1},keydown:function(t){var n,r,s,o,u,a;if(!/(38|40|27)/.test(t.keyCode))return;n=e(this),t.preventDefault(),t.stopPropagation();if(n.is(".disabled, :disabled"))return;o=i(n),u=o.hasClass("open");if(!u||u&&t.keyCode==27)return n.click();r=e("[role=menu] li:not(.divider) a",o);if(!r.length)return;a=r.index(r.filter(":focus")),t.keyCode==38&&a>0&&a--,t.keyCode==40&&a').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,e.proxy(this.removeBackdrop,this)):this.removeBackdrop()):t&&t()}},e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e(function(){e("body").on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,this.options.trigger=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):this.options.trigger!="manual"&&(i=this.options.trigger=="hover"?"mouseenter":"focus",s=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this))),this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,t,this.$element.data()),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);if(!n.options.delay||!n.options.delay.show)return n.show();clearTimeout(this.timeout),n.hoverState="in",this.timeout=setTimeout(function(){n.hoverState=="in"&&n.show()},n.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var e,t,n,r,i,s,o;if(this.hasContent()&&this.enabled){e=this.tip(),this.setContent(),this.options.animation&&e.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,e[0],this.$element[0]):this.options.placement,t=/in/.test(s),e.remove().css({top:0,left:0,display:"block"}).appendTo(t?this.$element:document.body),n=this.getPosition(t),r=e[0].offsetWidth,i=e[0].offsetHeight;switch(t?s.split(" ")[1]:s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}e.css(o).addClass(s).addClass("in")}},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function r(){var t=setTimeout(function(){n.off(e.support.transition.end).remove()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.remove()})}var t=this,n=this.tip();return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?r():n.remove(),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(t){return e.extend({},t?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}},e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
',trigger:"hover",title:"",delay:0,html:!0}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content > *")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-content")||(typeof n.content=="function"?n.content.call(t[0]):n.content),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}}),e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

'})}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var t=e(this),n=t.data("target")||t.attr("href"),r=/^#\w/.test(n)&&e(n);return r&&r.length&&[[r.position().top,n]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}},e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active a").last()[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}},e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e(function(){e("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=e(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:t.top+t.height,left:t.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))},e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/delta.js b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/delta.js new file mode 100644 index 0000000..ee1178a --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/delta.js @@ -0,0 +1,344 @@ +var templates = new Array(); +var records = new Array(); +var schema = new Array(); +var connections = 0; +var attempts = 0; +var total = 1; + +function connect() { + socket = new WebSocket("ws://localhost:6060/update"); + + socket.onopen = function() { + attempts = 1; + connections++; + reportStatus(this, "success.png", "0", "0", "0", "0", "0", ""); + }; + + socket.onerror = function(message) { + reportStatus(this, "failure.png", "0", "0", "0", "0", "0", ""); + }; + + socket.onclose = function(message) { + var exponent = Math.pow(2, attempts++); + var interval = (exponent - 1) * 1000; + var reference = connect(); + + if (interval > 30 * 1000) { + interval = 30 * 1000; + } + setTimeout(reference, interval); + reportStatus(this, "pending.png", "0", "0", "0", "0", "0", ""); + }; + + socket.onmessage = function(message) { + var data = message.data.substring(1); + var table = w2ui['mainGrid']; + + if (message.data.charAt(0) == 'T') { + deltaUpdate(this, table, data, updateTable); + } else if (message.data.charAt(0) == 'H') { + deltaUpdate(this, table, data, highlightTable); + } else if (message.data.charAt(0) == 'S') { + schemaUpdate(this, table, data); + } + }; +} + +function reportStatus(socket, status, height, delta, change, duration, sequence, method) { + var image = ' 0) { + index = record[0]; + } else { + index = height + 1; + } + return index; +} + +function highlightTable(socket, table, rows) { + for ( var i = 0; i < rows.length; i++) { + var row = rows[i]; + var pair = row.split(':'); + var index = pair[0]; + + if (index > 0) { + index = findRow(table, index); + + if (pair != null && pair.length > 1) { + var cells = pair[1].split(','); + + if (cells.length > 0) { + highlightRow(table, index, cells); + } + } + } + } +} + +function updateTable(socket, table, rows) { + for ( var i = 0; i < rows.length; i++) { + var row = rows[i]; + var pair = row.split(':'); + var index = pair[0]; + + if (index > 0) { + index = findRow(table, index); + + if (pair != null && pair.length > 1) { + var cells = pair[1].split(','); + + if (cells.length > 0) { + updateRow(socket, table, index, cells); + } + } + } + } +} + +function findCell(table, row, column) { + var height = table.total; + var width = schema.length; + + if(row <= height && column <= width) { + var expression = "#mainGrid_"; + + expression += table.name; + expression += "_rec_"; + expression += row; + expression += " td[col="; + expression += column; + expression += "]"; + + return $(expression)[0]; + } + return null; +} + +function highlightRow(table, row, cells) { + var height = table.total; + + if (height <= row) { + expandHeight(table, row); + } + var record = records[row]; + + for ( var i = 0; i < cells.length; i++) { + var cell = cells[i].split('='); + var column = cell[0]; + var value = cell[1]; + var style = schema[column]; + var decoded = decodeValue(value); + + record.style[column] = decoded; + } +} + +function updateRow(socket, table, row, cells) { + var height = table.total; + + if (height <= row) { + expandHeight(table, row); + } + var record = records[row]; + var template = templates[row]; + + for ( var i = 0; i < cells.length; i++) { + var cell = cells[i].split('='); + var column = cell[0]; + var value = cell[1]; + var style = schema[column]; + var decoded = decodeValue(value); + + record[style.name] = decoded; + } + interpolateRow(record, template); + table.set(record.recid, template, false); + reconcileRow(socket, table, row); +} + +function interpolateRow(record, template) { + for ( var i = 0; i < schema.length; i++) { + var style = schema[i]; + var name = style.name; + var text = style.template; + + for( var j = 0; j < schema.length; j++) { + var index = text.indexOf('{'); + + if(index == -1) { + break; + } + var key = schema[j].name; + var token = "{" + key + "}"; + var value = record[key]; + + text = text.replace(token, value); + } + template.style[i] = record.style[i]; + template[name] = text; + } +} + +function reconcileRow(socket, table, row) { + var template = templates[row]; + var index = findRow(table, row); + var row = table.get(index); + + for( var i = 0; i < schema.length; i++) { + var style = schema[i]; + var name = style.name; + var actual = row[name]; + var expect = template[name]; + + if(actual != expect) { + requestRefresh(socket, 'reconcileFailure'); + } + } +} + +function decodeValue(value) { + var text = value.substring(1); + + if (value.charAt(0) == '<') { + var encoded = text.toString(); + var decoded = ''; + + for ( var i = 0; i < encoded.length; i += 2) { + var char = encoded.substr(i, 2); + var decimal = parseInt(char, 16); + + decoded += String.fromCharCode(decimal); + } + return decoded; + } + return text; +} + +function expandWidth(table) { + var width = table.columns.length; + var height = table.total; + + for ( var i = width; i < schema.length; i++) { + var style = schema[i]; + var column = {}; + + column['field'] = style.name; + column['caption'] = style.caption; + column['resizable'] = style.resizable; + column['sortable'] = style.sortable; + column['size'] = '50px'; + + for( var j = 0; j < height; j++) { + templates[i][name] = ''; + records[i][name] = ''; + } + table.addColumn(column); + } +} + +function expandHeight(table, row) { + var height = table.total; + + for ( var i = height; i < row; i++) { + var index = i + 1; + var record = {recid : index, id: index, style: []}; + var template = {recid : index, id: index, style: []}; + + for( var j = 0; j < schema.length; j++) { + var name = schema[j].name; + + template[name] = ''; + record[name] = ''; + } + templates[row] = template; + records[row] = record; + table.add(template); + } +} + +window.addEventListener("load", connect, false); \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/failure.png b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/failure.png new file mode 100644 index 0000000..e560b08 Binary files /dev/null and b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/failure.png differ diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/font-awesome.min.css b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/font-awesome.min.css new file mode 100644 index 0000000..417f2c9 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/font-awesome.min.css @@ -0,0 +1,33 @@ +/*! + * Font Awesome 3.0.2 + * the iconic font designed for use with Twitter Bootstrap + * ------------------------------------------------------- + * The full suite of pictographic icons, examples, and documentation + * can be found at: http://fortawesome.github.com/Font-Awesome/ + * + * License + * ------------------------------------------------------- + * - The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL + * - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - + * http://opensource.org/licenses/mit-license.html + * - The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/ + * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: + * "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" + + * Contact + * ------------------------------------------------------- + * Email: dave@davegandy.com + * Twitter: http://twitter.com/fortaweso_me + * Work: Lead Product Designer @ http://kyruus.com + */ + +@font-face{ + font-family:'FontAwesome'; + src:url('font/fontawesome-webfont.eot?v=3.0.1'); + src:url('font/fontawesome-webfont.eot?#iefix&v=3.0.1') format('embedded-opentype'), + url('font/fontawesome-webfont.woff?v=3.0.1') format('woff'), + url('font/fontawesome-webfont.ttf?v=3.0.1') format('truetype'); + font-weight:normal; + font-style:normal } + +[class^="fa-"],[class*=" fa-"]{font-family:FontAwesome !important;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0 0;background-repeat:repeat;margin-top:0}.fa-white,.nav-pills>.active>a>[class^="fa-"],.nav-pills>.active>a>[class*=" fa-"],.nav-list>.active>a>[class^="fa-"],.nav-list>.active>a>[class*=" fa-"],.navbar-inverse .nav>.active>a>[class^="fa-"],.navbar-inverse .nav>.active>a>[class*=" fa-"],.dropdown-menu>li>a:hover>[class^="fa-"],.dropdown-menu>li>a:hover>[class*=" fa-"],.dropdown-menu>.active>a>[class^="fa-"],.dropdown-menu>.active>a>[class*=" fa-"],.dropdown-submenu:hover>a>[class^="fa-"],.dropdown-submenu:hover>a>[class*=" fa-"]{background-image:none}[class^="fa-"]:before,[class*=" fa-"]:before{text-decoration:inherit;display:inline-block;speak:none}a [class^="fa-"],a [class*=" fa-"]{display:inline-block}.fa-large:before{vertical-align:-10%;font-size:1.3333333333333333em}.btn [class^="fa-"],.nav [class^="fa-"],.btn [class*=" fa-"],.nav [class*=" fa-"]{display:inline}.btn [class^="fa-"].fa-large,.nav [class^="fa-"].fa-large,.btn [class*=" fa-"].fa-large,.nav [class*=" fa-"].fa-large{line-height:.9em}.btn [class^="fa-"].fa-spin,.nav [class^="fa-"].fa-spin,.btn [class*=" fa-"].fa-spin,.nav [class*=" fa-"].fa-spin{display:inline-block}.nav-tabs [class^="fa-"],.nav-pills [class^="fa-"],.nav-tabs [class*=" fa-"],.nav-pills [class*=" fa-"],.nav-tabs [class^="fa-"].fa-large,.nav-pills [class^="fa-"].fa-large,.nav-tabs [class*=" fa-"].fa-large,.nav-pills [class*=" fa-"].fa-large{line-height:.9em}li [class^="fa-"],.nav li [class^="fa-"],li [class*=" fa-"],.nav li [class*=" fa-"]{display:inline-block;width:1.25em;text-align:center}li [class^="fa-"].fa-large,.nav li [class^="fa-"].fa-large,li [class*=" fa-"].fa-large,.nav li [class*=" fa-"].fa-large{width:1.5625em}ul.icons{list-style-type:none;text-indent:-0.75em}ul.icons li [class^="fa-"],ul.icons li [class*=" fa-"]{width:.75em}.fa-muted{color:#eee}.fa-border{border:solid 1px #eee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fa-2x{font-size:2em}.fa-2x.fa-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.fa-3x{font-size:3em}.fa-3x.fa-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.fa-4x{font-size:4em}.fa-4x.fa-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.pull-right{float:right}.pull-left{float:left}[class^="fa-"].pull-left,[class*=" fa-"].pull-left{margin-right:.3em}[class^="fa-"].pull-right,[class*=" fa-"].pull-right{margin-left:.3em}.btn [class^="fa-"].pull-left.fa-2x,.btn [class*=" fa-"].pull-left.fa-2x,.btn [class^="fa-"].pull-right.fa-2x,.btn [class*=" fa-"].pull-right.fa-2x{margin-top:.18em}.btn [class^="fa-"].fa-spin.fa-large,.btn [class*=" fa-"].fa-spin.fa-large{line-height:.8em}.btn.btn-small [class^="fa-"].pull-left.fa-2x,.btn.btn-small [class*=" fa-"].pull-left.fa-2x,.btn.btn-small [class^="fa-"].pull-right.fa-2x,.btn.btn-small [class*=" fa-"].pull-right.fa-2x{margin-top:.25em}.btn.btn-large [class^="fa-"],.btn.btn-large [class*=" fa-"]{margin-top:0}.btn.btn-large [class^="fa-"].pull-left.fa-2x,.btn.btn-large [class*=" fa-"].pull-left.fa-2x,.btn.btn-large [class^="fa-"].pull-right.fa-2x,.btn.btn-large [class*=" fa-"].pull-right.fa-2x{margin-top:.05em}.btn.btn-large [class^="fa-"].pull-left.fa-2x,.btn.btn-large [class*=" fa-"].pull-left.fa-2x{margin-right:.2em}.btn.btn-large [class^="fa-"].pull-right.fa-2x,.btn.btn-large [class*=" fa-"].pull-right.fa-2x{margin-left:.2em}.fa-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}@-moz-document url-prefix(){.fa-spin{height:.9em}.btn .fa-spin{height:auto}.fa-spin.fa-large{height:1.25em}.btn .fa-spin.fa-large{height:.75em}}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-empty:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-ok:before{content:"\f00c"}.fa-remove:before{content:"\f00d"}.fa-zoom-in:before{content:"\f00e"}.fa-zoom-out:before{content:"\f010"}.fa-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-cog:before{content:"\f013"}.fa-trash:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file:before{content:"\f016"}.fa-time:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download-alt:before{content:"\f019"}.fa-download:before{content:"\f01a"}.fa-upload:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle:before{content:"\f01d"}.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-indent-left:before{content:"\f03b"}.fa-indent-right:before{content:"\f03c"}.fa-facetime-video:before{content:"\f03d"}.fa-picture:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before{content:"\f044"}.fa-share:before{content:"\f045"}.fa-check:before{content:"\f046"}.fa-move:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-sign:before{content:"\f055"}.fa-minus-sign:before{content:"\f056"}.fa-remove-sign:before{content:"\f057"}.fa-ok-sign:before{content:"\f058"}.fa-question-sign:before{content:"\f059"}.fa-info-sign:before{content:"\f05a"}.fa-screenshot:before{content:"\f05b"}.fa-remove-circle:before{content:"\f05c"}.fa-ok-circle:before{content:"\f05d"}.fa-ban-circle:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-share-alt:before{content:"\f064"}.fa-resize-full:before{content:"\f065"}.fa-resize-small:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-sign:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye-open:before{content:"\f06e"}.fa-eye-close:before{content:"\f070"}.fa-warning-sign:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder-close:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-resize-vertical:before{content:"\f07d"}.fa-resize-horizontal:before{content:"\f07e"}.fa-bar-chart:before{content:"\f080"}.fa-twitter-sign:before{content:"\f081"}.fa-facebook-sign:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-up:before{content:"\f087"}.fa-thumbs-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-empty:before{content:"\f08a"}.fa-signout:before{content:"\f08b"}.fa-linkedin-sign:before{content:"\f08c"}.fa-pushpin:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-signin:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-sign:before{content:"\f092"}.fa-upload-alt:before{content:"\f093"}.fa-lemon:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-check-empty:before{content:"\f096"}.fa-bookmark-empty:before{content:"\f097"}.fa-phone-sign:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0a2"}.fa-certificate:before{content:"\f0a3"}.fa-hand-right:before{content:"\f0a4"}.fa-hand-left:before{content:"\f0a5"}.fa-hand-up:before{content:"\f0a6"}.fa-hand-down:before{content:"\f0a7"}.fa-circle-arrow-left:before{content:"\f0a8"}.fa-circle-arrow-right:before{content:"\f0a9"}.fa-circle-arrow-up:before{content:"\f0aa"}.fa-circle-arrow-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-fullscreen:before{content:"\f0b2"}.fa-group:before{content:"\f0c0"}.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-beaker:before{content:"\f0c3"}.fa-cut:before{content:"\f0c4"}.fa-copy:before{content:"\f0c5"}.fa-paper-clip:before{content:"\f0c6"}.fa-save:before{content:"\f0c7"}.fa-sign-blank:before{content:"\f0c8"}.fa-reorder:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-sign:before{content:"\f0d3"}.fa-google-plus-sign:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-sort:before{content:"\f0dc"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-up:before{content:"\f0de"}.fa-envelope-alt:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-undo:before{content:"\f0e2"}.fa-legal:before{content:"\f0e3"}.fa-dashboard:before{content:"\f0e4"}.fa-comment-alt:before{content:"\f0e5"}.fa-comments-alt:before{content:"\f0e6"}.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before{content:"\f0ea"}.fa-lightbulb:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-alt:before{content:"\f0f3"}.fa-coffee:before{content:"\f0f4"}.fa-food:before{content:"\f0f5"}.fa-file-alt:before{content:"\f0f6"}.fa-building:before{content:"\f0f7"}.fa-hospital:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-sign:before{content:"\f0fd"}.fa-plus-sign-alt:before{content:"\f0fe"}.fa-double-angle-left:before{content:"\f100"}.fa-double-angle-right:before{content:"\f101"}.fa-double-angle-up:before{content:"\f102"}.fa-double-angle-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before{content:"\f10b"}.fa-circle-blank:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-close-alt:before{content:"\f114"}.fa-folder-open-alt:before{content:"\f115"} \ No newline at end of file diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/grid.html b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/grid.html new file mode 100644 index 0000000..69b546a --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/grid.html @@ -0,0 +1,103 @@ + + + + Bond Semi Central + + + + + + + + + + +
    + + + + diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/index.html b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/index.html new file mode 100644 index 0000000..5377060 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/index.html @@ -0,0 +1,40 @@ + + + + Bond Semi Central + + + + + + + + +
    +
    + + + diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/jquery-2.1.1.min.js b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/jquery-2.1.1.min.js new file mode 100644 index 0000000..9ed2acc --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/jquery-2.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
    ",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) +},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("