summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormikaelpeltier <mikaelpeltier@google.com>2015-06-24 14:31:11 +0200
committerMikael Peltier <mikaelpeltier@google.com>2015-06-24 14:59:36 +0000
commit04563874ddaac702d6c715eaa89c29b253f4c54e (patch)
treec305fa98670c3e80be494cc054a8e31b51bfe7f2
parentf1828481ebcfee3bddc323fca178a4502a60ceef (diff)
downloadtoolchain_jack-04563874ddaac702d6c715eaa89c29b253f4c54e.tar.gz
toolchain_jack-04563874ddaac702d6c715eaa89c29b253f4c54e.tar.bz2
toolchain_jack-04563874ddaac702d6c715eaa89c29b253f4c54e.zip
Add simpleframework source files
Change-Id: I18d01df16de2868ca5458f79a88e6070b75db2c3 (cherry picked from commit 3e9f84cf7b22f6970eb8041ca38d12d75c6bb270)
-rw-r--r--simple/pom.xml15
-rw-r--r--simple/simple-common/pom.xml132
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/KeyMap.java93
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/Allocator.java55
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayAllocator.java111
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/ArrayBuffer.java397
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/Buffer.java129
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferAllocator.java229
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/BufferException.java43
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileAllocator.java137
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileBuffer.java622
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/FileWatcher.java179
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/buffer/FilterAllocator.java123
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64Encoder.java166
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64InputStream.java123
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/encode/Base64OutputStream.java138
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/Cleaner.java44
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/Contract.java77
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractController.java84
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractLease.java119
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractMaintainer.java115
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/ContractQueue.java44
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/Expiration.java163
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/Lease.java85
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseCleaner.java155
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseException.java52
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseManager.java93
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseMap.java83
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/lease/LeaseProvider.java60
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/parse/MapParser.java251
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/parse/ParseBuffer.java247
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/parse/Parser.java197
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentExecutor.java109
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/ConcurrentScheduler.java122
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/Daemon.java164
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/DaemonFactory.java147
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/ExecutorQueue.java128
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/Scheduler.java57
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/SchedulerQueue.java127
-rw-r--r--simple/simple-common/src/main/java/org/simpleframework/common/thread/SynchronousExecutor.java43
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/KeyTest.java195
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/ArrayBufferTest.java54
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/BufferAllocatorTest.java79
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileBufferTest.java45
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueue.java109
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/FileByteQueueTest.java22
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueue.java100
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ArrayByteQueueTest.java119
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueue.java67
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/BufferQueueTest.java44
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueue.java13
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/buffer/queue/ByteQueueStream.java40
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractQueueTest.java57
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/lease/ContractTest.java40
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseManagerTest.java227
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/lease/LeaseTest.java87
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/lease/TimeTestCase.java25
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/thread/SchedulerTest.java65
-rw-r--r--simple/simple-common/src/test/java/org/simpleframework/common/thread/TransientApplication.java54
-rw-r--r--simple/simple-http/pom.xml142
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Address.java157
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/ContentDisposition.java59
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/ContentType.java142
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Cookie.java527
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Method.java70
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Part.java107
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Path.java166
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Principal.java48
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Protocol.java370
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Query.java99
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Request.java210
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/RequestHeader.java201
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/RequestLine.java98
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/RequestWrapper.java520
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Response.java262
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/ResponseHeader.java304
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/ResponseWrapper.java747
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Scheme.java136
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/Status.java320
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/StatusLine.java122
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoder.java108
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderException.java58
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/BodyEncoderFactory.java118
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/BodyObserver.java121
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ChunkedEncoder.java221
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/CloseEncoder.java179
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/Collector.java50
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/Container.java62
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerController.java161
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerEvent.java93
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerSocketProcessor.java155
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ContainerTransportProcessor.java96
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/Controller.java100
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/Conversation.java358
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/EmptyEncoder.java132
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/FixedLengthEncoder.java198
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/QueryBuilder.java148
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/QueryCombiner.java148
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCertificate.java183
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/RequestCollector.java184
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/RequestDispatcher.java128
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/RequestEntity.java398
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/RequestMessage.java341
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/RequestReader.java131
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseBuffer.java303
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEncoder.java324
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseEntity.java437
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseException.java58
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseMessage.java283
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/ResponseObserver.java238
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/core/Timer.java94
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/ArrayConsumer.java184
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/Body.java95
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/BodyConsumer.java43
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/BoundaryConsumer.java206
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/BufferBody.java166
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/BufferPart.java160
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/ByteConsumer.java64
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/ChunkedConsumer.java258
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/ConsumerFactory.java201
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/ContentConsumer.java226
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/ContinueDispatcher.java88
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyConsumer.java69
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/EmptyInputStream.java44
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/Entity.java75
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/EntityConsumer.java184
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/FileUploadConsumer.java272
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/FixedLengthConsumer.java128
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/Header.java213
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/HeaderConsumer.java114
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/Message.java273
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/MessageHeader.java477
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartBodyConsumer.java129
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartConsumer.java135
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartData.java101
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryConsumer.java112
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartEntryFactory.java84
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartFactory.java78
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartHeaderConsumer.java85
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeries.java68
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/PartSeriesConsumer.java165
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/RequestConsumer.java457
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/Segment.java163
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java750
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/TokenConsumer.java113
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/message/UpdateConsumer.java143
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/AddressParser.java1347
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentDispositionParser.java296
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/ContentTypeParser.java556
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/CookieParser.java589
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/DateParser.java642
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/LanguageParser.java156
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/ListParser.java456
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/PathParser.java726
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/PrincipalParser.java362
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/QueryParser.java636
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/parse/ValueParser.java108
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java75
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java150
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java51
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java111
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java212
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java85
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java117
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java64
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java142
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java97
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java91
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java75
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java127
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java107
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java118
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java179
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java214
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java162
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java229
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java80
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java235
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java255
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java99
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java111
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java105
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java114
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java137
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java159
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java59
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java109
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java44
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java149
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java101
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java97
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java139
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java93
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java111
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java220
-rw-r--r--simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java93
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/ConnectionTest.java267
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/CookieTest.java63
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/Debug.java11
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/MockRenegotiationServer.java434
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/MockSocket.java45
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/MockTrace.java8
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/RenegotiationExample.java351
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/StatusTest.java20
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/StreamTransport.java67
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/AccumulatorTest.java99
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/ChunkedProducerTest.java43
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/Chunker.java52
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/Client.java264
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/Connector.java9
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/ConversationTest.java126
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/DribbleCursor.java62
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/FixedConsumerTest.java80
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/FixedProducerTest.java50
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MessageTest.java72
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockChannel.java57
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockController.java55
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockEntity.java49
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockObserver.java62
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockPart.java49
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockProxyRequest.java67
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockRequest.java202
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockResponse.java95
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockSender.java75
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/MockSocket.java42
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/PayloadTest.java97
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/ProducerExceptionTest.java23
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/QueryBuilderTest.java35
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorProcessorTest.java247
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/ReactorTest.java178
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java138
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/RequestTest.java144
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/Result.java37
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/StopTest.java176
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/StreamCursor.java74
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/ThreadDumper.java183
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/Ticket.java22
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/TicketProcessor.java28
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/TransferTest.java195
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/core/WebSocketUpgradeTest.java126
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/BoundaryConsumerTest.java77
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/ChunkedConsumerTest.java118
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/ContentConsumerTest.java99
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/FileUploadConsumerTest.java86
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/MessageHeaderTest.java48
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/MockBody.java47
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/MockHeader.java22
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/MockSegment.java83
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/PartConsumerTest.java33
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/PartSeriesConsumerTest.java157
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/ReplyConsumer.java141
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerTest.java103
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/message/TokenConsumerTest.java55
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/AddressParserTest.java92
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentDispositionParserTest.java33
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/ContentTypeParserTest.java74
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/CookieParserTest.java22
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/DateParserTest.java55
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/LanguageParserTest.java26
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/ListParserTest.java97
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/ParameterTest.java69
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/PathParserTest.java97
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/PriorityQueueTest.java48
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/parse/QueryParserTest.java69
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebFrameTypeTest.java29
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketAnalyzer.java66
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketCertificate.java170
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatApplication.java170
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatLogin.html12
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.html29
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.java86
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoomListener.java106
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketKeyTest.java37
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketTestClient.java25
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/service/PathRouterTest.java62
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/service/WebSocketPerformanceTest.java439
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTable.java151
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableCell.java26
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableChanger.java58
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumn.java22
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableColumnStyle.java35
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableListener.java69
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRow.java84
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowAnnotator.java89
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableRowChanger.java73
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSchema.java40
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSubscription.java44
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableSweeper.java33
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdateType.java17
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdater.java126
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketTableUpdaterApplication.java154
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/WebSocketValueEncoder.java53
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.css5774
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/bootstrap.min.js6
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/delta.js344
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/failure.pngbin0 -> 3113 bytes
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/font-awesome.min.css33
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/grid.html103
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/index.html40
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/jquery-2.1.1.min.js4
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/login.html12
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/main.html182
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/pending.pngbin0 -> 4897 bytes
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/success.pngbin0 -> 4514 bytes
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/table.html347
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.css2750
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.js13617
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.css2
-rw-r--r--simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.js11
-rw-r--r--simple/simple-transport/pom.xml137
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/ByteCursor.java131
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/ByteReader.java107
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/ByteWriter.java98
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/Certificate.java65
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/CertificateChallenge.java73
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/Channel.java128
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/FlushScheduler.java197
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/FlushSignaller.java120
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/Handshake.java652
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/Negotiation.java69
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/NegotiationState.java337
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/OperationFactory.java150
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/Phase.java165
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/PhaseType.java45
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SecureTransport.java428
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/ServerCleaner.java86
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/Socket.java89
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBuffer.java308
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferAppender.java289
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferWriter.java103
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SocketFlusher.java142
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SocketProcessor.java61
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SocketTransport.java262
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/SocketWrapper.java144
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/Transport.java91
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportChannel.java195
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportCursor.java260
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportDispatcher.java114
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportEvent.java91
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportException.java55
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportProcessor.java63
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportReader.java229
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportSocketProcessor.java163
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/TransportWriter.java150
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/Connection.java73
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionEvent.java42
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionException.java58
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAcceptor.java315
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAnalyzer.java84
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketConnection.java141
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListener.java125
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListenerManager.java127
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketTrace.java75
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Action.java71
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionDistributor.java741
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSelector.java193
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSet.java269
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/CancelAction.java114
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecuteAction.java121
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecutorReactor.java132
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Latch.java71
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Operation.java69
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/OperationDistributor.java62
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/PartitionDistributor.java136
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Reactor.java79
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ReactorEvent.java120
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/SynchronousReactor.java107
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/trace/Trace.java57
-rw-r--r--simple/simple-transport/src/main/java/org/simpleframework/transport/trace/TraceAnalyzer.java59
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/MockSocket.java45
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/ServerBuffer.java75
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/SocketBufferTest.java86
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportPipeTest.java92
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportTest.java51
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/StreamTransport.java66
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/TransportCursorTest.java83
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/TransportTest.java404
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/reactor/DistributorTest.java269
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/trace/CompareQueueTest.java174
-rw-r--r--simple/simple-transport/src/test/java/org/simpleframework/transport/trace/MockTrace.java6
380 files changed, 76677 insertions, 0 deletions
diff --git a/simple/pom.xml b/simple/pom.xml
new file mode 100644
index 00000000..d731e356
--- /dev/null
+++ b/simple/pom.xml
@@ -0,0 +1,15 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple</artifactId>
+ <packaging>pom</packaging>
+ <version>6.0.1</version>
+ <name>Simple</name>
+ <modules>
+ <module>simple-common</module>
+ <module>simple-transport</module>
+ <module>simple-http</module>
+ </modules>
+</project>
+
+
diff --git a/simple/simple-common/pom.xml b/simple/simple-common/pom.xml
new file mode 100644
index 00000000..e1a4b3ad
--- /dev/null
+++ b/simple/simple-common/pom.xml
@@ -0,0 +1,132 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.sonatype.oss</groupId>
+ <artifactId>oss-parent</artifactId>
+ <version>7</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple-common</artifactId>
+ <packaging>jar</packaging>
+ <version>6.0.1</version>
+ <name>Simple Common</name>
+ <url>http://www.simpleframework.org</url>
+ <description>Simple is a high performance asynchronous HTTP framework for Java</description>
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <scm>
+ <url>http://simpleweb.svn.sourceforge.net/viewvc/simpleweb.tags/simple-common-6.0.1</url>
+ <developerConnection>scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-common-6.0.1</developerConnection>
+ <connection>scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-common-6.0.1</connection>
+ </scm>
+ <developers>
+ <developer>
+ <id>niallg</id>
+ <name>Niall Gallagher</name>
+ <email>niallg@users.sf.net</email>
+ </developer>
+ </developers>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <file.encoding>UTF-8</file.encoding>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>3.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>cobertura-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </reporting>
+ <build>
+ <extensions>
+ <extension>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-ssh-external</artifactId>
+ <version>1.0-alpha-5</version>
+ </extension>
+ </extensions>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.5</source>
+ <target>1.5</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <configuration>
+ <encoding>UTF-8</encoding>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-javadocs</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <profiles>
+ <profile>
+ <id>release-sign-artifacts</id>
+ <activation>
+ <property>
+ <name>performRelease</name>
+ <value>true</value>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-gpg-plugin</artifactId>
+ <version>1.1</version>
+ <executions>
+ <execution>
+ <id>sign-artifacts</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>sign</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
+
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 00000000..6f450bbd
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>KeyMap</code> 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<T> extends LinkedHashMap<String, T> implements Iterable<String> {
+
+ /**
+ * Constructor for the <code>KeyMap</code> object. This creates
+ * a hash map that can expose the keys and values of the map as
+ * an independent <code>List</code> containing the values. This
+ * can also be used within a for loop for convenience.
+ */
+ public KeyMap() {
+ super();
+ }
+
+ /**
+ * This is used to produce an <code>Iterator</code> 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<String> iterator() {
+ return getKeys().iterator();
+ }
+
+ /**
+ * This is used to produce a <code>List</code> 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<String> getKeys() {
+ Set<String> keys = keySet();
+
+ if(keys == null) {
+ return new ArrayList<String>();
+ }
+ return new ArrayList<String>(keys);
+ }
+
+ /**
+ * This is used to produce a <code>List</code> 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<T> getValues() {
+ Collection<T> values = values();
+
+ if(values == null) {
+ return new ArrayList<T>();
+ }
+ return new ArrayList<T>(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 00000000..aa20b76c
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Allocator</code> 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 00000000..e2dbb3c7
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ArrayAllocator</code> 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 <code>ArrayAllocator</code> 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 <code>ArrayAllocator</code> 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 <code>ArrayAllocator</code> 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 00000000..972ad924
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ArrayBuffer</code> 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.
+ * <p>
+ * 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 <code>append</code> 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 <code>ArrayBuffer</code> 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 <code>ArrayBuffer</code> 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 <code>ArrayBuffer</code> 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 <code>Segment</code> 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 <code>Segment</code> 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 00000000..ebde2807
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Buffer</code> 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 00000000..033ba3ab
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BufferAllocator</code> 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 <code>BufferAllocator</code> 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 <code>BufferAllocator</code> 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 <code>BufferAllocator</code> 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 00000000..4ec2019e
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BufferException</code> 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 <code>BufferException</code> 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 00000000..c91b1dc5
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FileAllocator</code> 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 <code>FileAllocator</code> 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 <code>FileAllocator</code> 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 <code>FileAllocator</code> 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 <code>FileAllocator</code> 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 00000000..8fcea77e
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FileBuffer</code> 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 <code>InputStream</code> 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 <code>FileBuffer</code> 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 <code>InputStream</code> 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 <code>InputStream</code> 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 <code>Segment</code> 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 <code>Segment</code> 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 <code>InputStream</code> 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 <code>InputStream</code> 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 <code>Range</code> 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 <code>Range</code> 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 <code>Range</code> 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 00000000..6d1f3b27
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FileWatcher</code> object is used to create files that
+ * are to be used for file buffers. All files created by this are
+ * created in the <code>java.io.tmpdir</code> 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 <code>FileWatcher</code> 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 <code>FileWatcher</code> 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 <code>FileBuffer</code> 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 <code>java.io.tmpdir</code> 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 <code>FileBuffer</code> 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 <code>java.io.tmpdir</code> 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 00000000..ab974235
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FilterAllocator</code> 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 <code>Allocator</code> 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 <code>FilterAllocator</code> 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 <code>FilterAllocator</code> 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 <code>FilterAllocator</code> 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 00000000..54c820ae
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Base64Encoder</code> 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 00000000..8aa28af5
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Base64InputStream</code> 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 <code>Base64InputStream</code> 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 00000000..a8f425e3
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Base64OutputStream</code> 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 <code>Base64OutputStream</code> 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 <code>Base64OutputStream</code> 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 <code>toString</code> 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 <code>toString</code> 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 <code>toString</code> 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 00000000..08d7fb09
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Cleaner</code> represents an object that is used to
+ * clean up after the keyed resource. Typically this is used when
+ * a <code>Lease</code> 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<T> {
+
+ /**
+ * 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 00000000..ac05682e
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Contract</code> 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 <code>TimeUnit</code> for convinienct.
+ *
+ * @author Niall Gallagher
+ */
+interface Contract<T> 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 <code>TimeUnit</code>
+ * 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 <code>TimeUnit</code>
+ * 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 00000000..03536479
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContractController</code> 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<T> {
+
+ /**
+ * 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 <code>Cleaner</code> 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<T> 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<T> contract) throws LeaseException;
+
+ /**
+ * This will cancel the lease and release the resource. This
+ * has the same effect as the <code>renew</code> method with
+ * a zero length duration. Once this has been called the
+ * <code>Cleaner</code> 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<T> 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 00000000..60cd4748
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContractLease</code> 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<T> implements Lease<T> {
+
+ /**
+ * This is the controller object used to handle contracts.
+ */
+ private final ContractController<T> handler;
+
+ /**
+ * This is the contract object representing the lease.
+ */
+ private final Contract<T> contract;
+
+ /**
+ * Constructor for the <code>ContractLease</code> 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<T> handler, Contract<T> contract) {
+ this.handler = handler;
+ this.contract = contract;
+ }
+
+ /**
+ * Determines the duration remaining before the lease expires.
+ * The expiry is given as the number of <code>TimeUnit</code>
+ * 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 <code>TimeUnit</code> 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 <code>renew</code> method with
+ * a zero length duration. Once this has been called the
+ * <code>Cleaner</code> 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 <code>Object</code>.
+ *
+ * @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 00000000..3e650390
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContractMaintainer</code> 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<T> implements ContractController<T> {
+
+ /**
+ * The queue that is used to issue and revoke contracts.
+ */
+ private final LeaseCleaner<T> queue;
+
+ /**
+ * Constructor for the <code>ContractMaintainer</code> 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<T> cleaner) {
+ this.queue = new LeaseCleaner<T>(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 <code>Cleaner</code> 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<T> 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<T> 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 <code>renew</code> method with
+ * a zero length duration. Once this has been called the
+ * <code>Cleaner</code> 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<T> 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 00000000..df881dcc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContraceQueue</code> 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<T> extends DelayQueue<Contract<T>> {
+
+ /**
+ * Constructor for the <code>ContractQueue</code> 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 00000000..fca1c140
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Expiration</code> 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 <code>TimeUnit</code> for convenience.
+ *
+ * @author Niall Gallagher
+ */
+class Expiration<T> implements Contract<T> {
+
+ /**
+ * 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 <code>Expiration</code> 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 <code>TimeUnit</code>
+ * 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 <code>TimeUnit</code>
+ * 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 00000000..d2a97851
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Lease</code> 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<T> {
+
+ /**
+ * Determines the duration remaining before the lease expires.
+ * The expiry is given as the number of <code>TimeUnit</code>
+ * 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 <code>TimeUnit</code> 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 <code>renew</code> method with
+ * a zero length duration. Once this has been called the
+ * <code>Cleaner</code> 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 <code>Object</code>.
+ *
+ * @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 00000000..d1fe912d
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>LeaseCleaner</code> 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 <code>Cleaner</code> 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<T> extends Daemon {
+
+ /**
+ * This is used to queue contracts that are to be cleaned.
+ */
+ private final ContractQueue<T> queue;
+
+ /**
+ * This is the cleaner that is invoked to clean contracts.
+ */
+ private final Cleaner<T> cleaner;
+
+ /**
+ * Constructor for the <code>LeaseCleaner</code> object. This
+ * can be used to issue, update, and expire leases. When a lease
+ * expires notification is sent to the <code>Cleaner</code>
+ * 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<T> cleaner) {
+ this.queue = new ContractQueue<T>();
+ 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<T> 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 <code>Cleaner</code>
+ * implementation, to signify that the resource has expired.
+ *
+ * @param contract this is the contract that contains details
+ */
+ public boolean issue(Contract<T> 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 <code>Cleaner</code> 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<T> 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<T> 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 00000000..3a479493
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>LeaseException</code> 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 00000000..93459fdc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>LeaseManager</code> 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 <code>Lease</code> 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<T> implements LeaseProvider<T> {
+
+ /**
+ * This is the controller used to handle lease operations.
+ */
+ private ContractController<T> handler;
+
+ /**
+ * Constructor for the <code>LeaseManager</code> object. This
+ * instance is created using a specified notification handler.
+ * The specified <code>Cleaner</code> 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<T> cleaner) {
+ this.handler = new ContractMaintainer<T>(cleaner);
+ }
+
+ /**
+ * This method will issue a <code>Lease</code> 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 <code>Cleaner</code>
+ * 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<T> lease(T key, long duration, TimeUnit unit) {
+ Contract<T> contract = new Expiration<T>(key, duration, unit);
+
+ try {
+ handler.issue(contract);
+ } catch(Exception e) {
+ throw new LeaseException("Could not issue lease", e);
+ }
+ return new ContractLease<T>(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 <code>lease</code> 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 00000000..711a466b
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>LeaseMap</code> 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<T> extends ConcurrentHashMap<T, Lease<T>> {
+
+ /**
+ * Constructor for the <code>LeaseMap</code> 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 <code>LeaseMap</code> 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 <code>Lease</code> 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<T> get(Object key) {
+ return super.get(key);
+ }
+
+ /**
+ * This is used to remove the <code>Lease</code> 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<T> 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 00000000..e5e7d8c9
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>LeaseProvider</code> 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 <code>Lease</code> 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<T> {
+
+ /**
+ * This method will issue a <code>Lease</code> 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 <code>Cleaner</code>
+ * 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<T> 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 <code>lease</code> 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 00000000..d242937a
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>MapParser</code> 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 <code>java.util.Map</code> interface
+ * and as such can be used with the Java collections framework. The
+ * internal map used by this is a <code>Hashtable</code>, however
+ * subclasses are free to assign a different type to the map used.
+ *
+ * @author Niall Gallagher
+ */
+public abstract class MapParser<T> extends Parser implements Map<T, T> {
+
+ /**
+ * Represents all values inserted to the map as a list of values.
+ */
+ protected Map<T, List<T>> all;
+
+ /**
+ * Represents the last value inserted into this map instance.
+ */
+ protected Map<T, T> map;
+
+ /**
+ * Constructor for the <code>MapParser</code> object. This is
+ * used to create a new parser that makes use of a thread safe
+ * map implementation. The <code>HashMap</code> 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<T, List<T>>();
+ this.map = new HashMap<T, T>();
+ }
+
+ /**
+ * 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<Map.Entry<T, T>> entrySet() {
+ return map.entrySet();
+ }
+
+ /**
+ * The <code>get</code> 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 <code>List</code> 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<T> getAll(Object key) {
+ return all.get(key);
+ }
+
+ /**
+ * This method is used to determine whether the parser has any
+ * tokens available. If the <code>size</code> is zero then the
+ * parser is empty and this returns true. The is acts as a
+ * proxy the the <code>isEmpty</code> 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<T> keySet() {
+ return map.keySet();
+ }
+
+ /**
+ * The <code>put</code> 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<T> list = all.get(name);
+ T first = map.get(name);
+
+ if(list == null) {
+ list = new ArrayList<T>();
+ 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<? extends T, ? extends T> data) {
+ Set<? extends T> keySet = data.keySet();
+
+ for(T key : keySet) {
+ T value = data.get(key);
+
+ if(value != null) {
+ put(key, value);
+ }
+ }
+ }
+
+ /**
+ * The <code>remove</code> 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 <code>size</code>.
+ *
+ * @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<T> values() {
+ return map.values();
+ }
+
+ /**
+ * The <code>clear</code> 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 00000000..d680a08f
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>StringBuffer</code>
+ * class, as a way for the <code>Parser</code> 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 <code>char</code>'s to be taken quicker than the
+ * <code>StringBuffer</code> class.
+ *
+ * @author Niall Gallagher
+ */
+public class ParseBuffer {
+
+ /**
+ * This is used to quicken <code>toString</code>.
+ */
+ protected String cache;
+
+ /**
+ * The <code>char</code>'s this buffer accumulated.
+ */
+ protected char[] buf;
+
+ /**
+ * This is the number of <code>char</code>'s stored.
+ */
+ protected int count;
+
+ /**
+ * Constructor for <code>ParseBuffer</code>. The default
+ * <code>ParseBuffer</code> stores 16 <code>char</code>'s
+ * before a <code>resize</code> is needed to accommodate
+ * extra characters.
+ */
+ public ParseBuffer(){
+ this(16);
+ }
+
+ /**
+ * This creates a <code>ParseBuffer</code> with a specific
+ * default size. The buffer will be created the with the
+ * length specified. The <code>ParseBuffer</code> can grow
+ * to accommodate a collection of <code>char</code>'s larger
+ * the the size specified.
+ *
+ * @param size initial size of this <code>ParseBuffer</code>
+ */
+ public ParseBuffer(int size){
+ this.buf = new char[size];
+ }
+
+ /**
+ * This will add a <code>char</code> to the end of the buffer.
+ * The buffer will not overflow with repeated uses of the
+ * <code>append</code>, it uses an <code>ensureCapacity</code>
+ * method which will allow the buffer to dynamically grow in
+ * size to accommodate more <code>char</code>'s.
+ *
+ * @param c the <code>char</code> to be appended
+ */
+ public void append(char c){
+ ensureCapacity(count+ 1);
+ buf[count++] = c;
+ }
+
+ /**
+ * This will add a <code>String</code> to the end of the buffer.
+ * The buffer will not overflow with repeated uses of the
+ * <code>append</code>, it uses an <code>ensureCapacity</code>
+ * method which will allow the buffer to dynamically grow in
+ * size to accommodate large <code>String</code> objects.
+ *
+ * @param text the <code>String</code> 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 <code>append(String)</code> 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 <code>ParseBuffer</code> to the end of this.
+ * The buffer will not overflow with repeated uses of the
+ * <code>append</code>, it uses an <code>ensureCapacity</code>
+ * method which will allow the buffer to dynamically grow in
+ * size to accommodate large <code>ParseBuffer</code> objects.
+ *
+ * @param text the <code>ParseBuffer</code> 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 <code>append(ParseBuffer)</code> 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 <code>char</code> to the end of the buffer.
+ * The buffer will not overflow with repeated uses of the
+ * <code>append</code>, it uses an <code>ensureCapacity</code>
+ * method which will allow the buffer to dynamically grow in
+ * size to accommodate large <code>char</code> arrays.
+ *
+ * @param c the <code>char</code> array to be appended to this
+ * @param off the read offset for the array
+ * @param len the number of <code>char</code>'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 <code>String</code> to the end of the buffer.
+ * The buffer will not overflow with repeated uses of the
+ * <code>append</code>, it uses an <code>ensureCapacity</code>
+ * method which will allow the buffer to dynamically grow in
+ * size to accommodate large <code>String</code> objects.
+ *
+ * @param str the <code>String</code> to be appended to this
+ * @param off the read offset for the <code>String</code>
+ * @param len the number of <code>char</code>'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 <code>ParseBuffer</code> to the end of this.
+ * The buffer will not overflow with repeated uses of the
+ * <code>append</code>, it uses an <code>ensureCapacity</code>
+ * method which will allow the buffer to dynamically grow in
+ * size to accommodate large <code>ParseBuffer</code> objects.
+ *
+ * @param text the <code>ParseBuffer</code> to be appended
+ * @param off the read offset for the <code>ParseBuffer</code>
+ * @param len the number of <code>char</code>'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 <code>char</code>'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 <code>ParseBuffer</code> so that the
+ * <code>toString</code> parameter will return <code>null</code>.
+ * This is used so that the same <code>ParseBuffer</code> 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 <code>ParseBuffer</code>. This will return zero after
+ * the clear method has been invoked.
+ *
+ * @return the number of <code>char</code>'s within the buffer
+ */
+ public int length(){
+ return count;
+ }
+
+ /**
+ * This will return the characters that have been appended to the
+ * <code>ParseBuffer</code> as a <code>String</code> object.
+ * If the <code>String</code> object has been created before then
+ * a cached <code>String</code> object will be returned. This
+ * method will return <code>null</code> after clear is invoked.
+ *
+ * @return the <code>char</code>'s appended as a <code>String</code>
+ */
+ 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 00000000..5ba7b522
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Parser</code> object is to be used as a simple template
+ * for parsing uncomplicated expressions. This object is used to parse
+ * a <code>String</code>. 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 <code>Parser</code> for a given <code>String</code>.
+ *
+ * @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 <code>Parser</code>.
+ * This will be invoked by each subclass of this object. It will
+ * set the buffer to a zero length buffer so that when the
+ * <code>ensureCapacity</code> method is used the buf's
+ * length can be checked.
+ */
+ protected Parser(){
+ this.buf = new char[0];
+ }
+
+ /**
+ * This is used to parse the <code>String</code> given to it. This
+ * will ensure that the <code>char</code> buffer has enough space
+ * to contain the characters from the <code>String</code>. This
+ * will firstly ensure that the buffer is resized if nessecary. The
+ * second step in this <code>parse</code> method is to initialize
+ * the <code>Parser</code> object so that multiple parse invocations
+ * can be made. The <code>init</code> method will reset this to an
+ * prepared state. Then finally the <code>parse</code> method is
+ * called to parse the <code>char</code> buffer.
+ *
+ * @param text the <code>String</code> to be parsed with this
+ * <code>Parser</code>
+ */
+ 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 <code>char</code>'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 <code>char</code> 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 <code>String</code> within the
+ * <code>char</code> buf. It checks the length of the <code>String</code>
+ * first to ensure that it will not go out of bounds. A comparison
+ * is then made with the buffers contents and the <code>String</code>
+ * if the reigon in the buffer matched the <code>String</code> then the
+ * offset within the buffer is increased by the <code>String</code>'s
+ * length so that it has effectively skipped it.
+ *
+ * @param text this is the <code>String</code> value to be skipped
+ *
+ * @return true if the <code>String</code> 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 <code>Parser</code> when it is ready
+ * to parse a new <code>String</code>. This will reset the
+ * <code>Parser</code> to a ready state. The <code>init</code>
+ * method is invoked by the <code>Parser</code> when the
+ * <code>parse</code> 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 <code>init</code> 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 00000000..9f99025e
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ConcurrentExecutor</code> 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 <code>ConcurrentExecutor</code> object. This
+ * is used to create a pool of threads that can be used to execute
+ * arbitrary <code>Runnable</code> 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 <code>ConcurrentExecutor</code> object. This
+ * is used to create a pool of threads that can be used to execute
+ * arbitrary <code>Runnable</code> 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 <code>ConcurrentExecutor</code> object. This
+ * is used to create a pool of threads that can be used to execute
+ * arbitrary <code>Runnable</code> 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 <code>execute</code> 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 00000000..bb4a1174
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ConcurrentScheduler</code> 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 <code>ConcurrentScheduler</code> 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 <code>ConcurrentScheduler</code> 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 00000000..3b7b5bf9
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Daemon</code> object provides a named thread which will
+ * execute the <code>run</code> 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<Thread> 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 <code>Daemon</code> 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 <code>getName</code>. If
+ * the name provided is null then no name is set for the thread.
+ */
+ protected Daemon() {
+ this.reference = new AtomicReference<Thread>();
+ 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 <code>run</code> 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 <code>isActive</code> method
+ * is checked to determine whether it should continue to run.
+ */
+ public void stop() {
+ active.set(false);
+ }
+
+ /**
+ * The <code>RunnableDelegate</code> object is used to actually
+ * invoke the <code>run</code> 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 <code>RunnableDelegate</code> 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 00000000..d5da16a2
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>DaemonFactory</code> 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 <code>DaemonFactory</code> object. This
+ * will provide a thread factory that names the threads based
+ * on the type of <code>Runnable</code> the pool executes.
+ */
+ public DaemonFactory() {
+ this(null);
+ }
+
+ /**
+ * Constructor for the <code>DaemonFactory</code> object. This
+ * will provide a thread factory that names the threads based
+ * on the type of <code>Runnable</code> 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 00000000..99e8fb21
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ExecutorQueue</code> 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<Runnable> 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 <code>ExecutorQueue</code> object. This is
+ * used to create a pool of threads that can be used to execute
+ * arbitrary <code>Runnable</code> 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 <code>ExecutorQueue</code> object. This is
+ * used to create a pool of threads that can be used to execute
+ * arbitrary <code>Runnable</code> 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<Runnable>();
+ this.factory = new DaemonFactory(type);
+ this.executor = new ThreadPoolExecutor(rest, active, duration, unit, queue, factory);
+ }
+
+ /**
+ * The <code>execute</code> 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 00000000..d24fa17a
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Scheduler</code> 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 00000000..67385ed3
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>SchedulerQueue</code> 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 <code>SchedulerQueue</code> 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 <code>execute</code> 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 00000000..decd41d7
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>SynchronousExecutor</code> object is used for synchronous
+ * execution of tasks. This simple acts as an adapter for running
+ * a <code>Runnable</code> implementation and can be used wherever
+ * the executor interface is required.
+ *
+ * @author Niall Gallagher
+ */
+public class SynchronousExecutor implements Executor {
+
+ /**
+ * This will execute the provided <code>Runnable</code> 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 00000000..f9056fe5
--- /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<T> {
+
+ private final Map<Name, T> map;
+
+ public NameTable() {
+ this.map = new HashMap<Name, T>();
+ }
+
+ 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<String> map = new NameTable<String>();
+
+ 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 00000000..1f6f4943
--- /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 00000000..95840471
--- /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 00000000..54cb142b
--- /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 00000000..b4045629
--- /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<Block> blocks;
+ private BlockAllocator allocator;
+ private Block source;
+
+ public FileByteQueue(Allocator allocator) throws IOException {
+ this.blocks = new LinkedBlockingQueue<Block>();
+ 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 00000000..96994543
--- /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 00000000..893ae802
--- /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 00000000..7d361f37
--- /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 00000000..5f3e97f8
--- /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 00000000..22eaba7c
--- /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 00000000..dc567e91
--- /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 00000000..dbf73e18
--- /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 00000000..6fa55c18
--- /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<Long> queue = new ContractQueue<Long>();
+ List<String> complete = new ArrayList<String>();
+
+ for(long i = 0; i < 10000; i++) {
+ long random = (long)(Math.random() * 1000);
+ Contract<Long> contract = new Expiration(random, random, TimeUnit.NANOSECONDS);
+
+ queue.offer(contract);
+ }
+ for(int i = 0; i < 10000; i++) {
+ Contract<Long> 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<Long> queue = new ContractQueue<Long>();
+
+ for(long i = 0; i < 10000; i++) {
+ long random = (long)(Math.random() * 1000);
+ Contract<Long> contract = new Expiration(random, random, TimeUnit.NANOSECONDS);
+
+ queue.offer(contract);
+ }
+ for(int i = 0; i < 10000; i++) {
+ Contract<Long> 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 00000000..1cc40af4
--- /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 00000000..9ce2b938
--- /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<Long> timeList = new ArrayList<Long>();
+
+ 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<Integer> clean = new LinkedBlockingQueue<Integer>();
+ final ConcurrentHashMap<Integer, Long> record = new ConcurrentHashMap<Integer, Long>();
+
+ Cleaner<Integer> cleaner = new Cleaner<Integer>() {
+
+ long start = System.currentTimeMillis();
+
+ public void clean(Integer key) {
+ record.put(key, start - System.currentTimeMillis());
+ clean.offer(key);
+
+ }
+ };
+ LeaseManager<Integer> manager = new LeaseManager<Integer>(cleaner);
+ List<Lease<Integer>> list = new ArrayList<Lease<Integer>>();
+
+ 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<Integer> 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<Expectation> clean = new LinkedBlockingQueue<Expectation>();
+
+ Cleaner<Expectation> cleaner = new Cleaner<Expectation>() {
+ public void clean(Expectation key) {
+ clean.offer(key);
+ }
+ };
+ final BlockingQueue<Lease<Expectation>> renewalQueue = new LinkedBlockingQueue<Lease<Expectation>>();
+ final BlockingQueue<Lease<Expectation>> expiryQueue = new LinkedBlockingQueue<Lease<Expectation>>();
+ 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<Expectation> 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<Expectation> manager = new LeaseManager<Expectation>(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<Expectation> 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 00000000..d4b73d7e
--- /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<Integer> clean = new LinkedBlockingQueue<Integer>();
+
+ Cleaner<Integer> cleaner = new Cleaner<Integer>() {
+ public void clean(Integer key) {
+ clean.offer(key);
+ }
+ };
+ Map<Integer, Contract> table = new ConcurrentHashMap<Integer, Contract>();
+ List<Lease> list = new ArrayList<Lease>();
+ ContractController controller = new ContractMaintainer(cleaner);
+
+ for (int i = 0; i < ITERATIONS; i++) {
+ long random = (long) (Math.random() * 1000) + 1000L;
+ Contract<Integer> 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 00000000..294cc9b3
--- /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 00000000..78fe8026
--- /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<Timer> list = new LinkedBlockingQueue<Timer>();
+
+ 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<Timer> queue;
+
+ private Timer timer;
+
+ public Task(LinkedBlockingQueue<Timer> 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 00000000..69941b2e
--- /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 00000000..2a079fc5
--- /dev/null
+++ b/simple/simple-http/pom.xml
@@ -0,0 +1,142 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.sonatype.oss</groupId>
+ <artifactId>oss-parent</artifactId>
+ <version>7</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple-http</artifactId>
+ <packaging>jar</packaging>
+ <version>6.0.1</version>
+ <name>Simple HTTP</name>
+ <url>http://www.simpleframework.org</url>
+ <description>Simple is a high performance asynchronous HTTP framework for Java</description>
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <scm>
+ <url>http://simpleweb.svn.sourceforge.net/viewvc/simpleweb.tags/simple-http-6.0.1</url>
+ <developerConnection>scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-http-6.0.1</developerConnection>
+ <connection>scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-http-6.0.1</connection>
+ </scm>
+ <developers>
+ <developer>
+ <id>niallg</id>
+ <name>Niall Gallagher</name>
+ <email>niallg@users.sf.net</email>
+ </developer>
+ </developers>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <file.encoding>UTF-8</file.encoding>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple-common</artifactId>
+ <version>6.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple-transport</artifactId>
+ <version>6.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>3.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>cobertura-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </reporting>
+ <build>
+ <extensions>
+ <extension>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-ssh-external</artifactId>
+ <version>1.0-alpha-5</version>
+ </extension>
+ </extensions>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.5</source>
+ <target>1.5</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <configuration>
+ <encoding>UTF-8</encoding>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-javadocs</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <profiles>
+ <profile>
+ <id>release-sign-artifacts</id>
+ <activation>
+ <property>
+ <name>performRelease</name>
+ <value>true</value>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-gpg-plugin</artifactId>
+ <version>1.1</version>
+ <executions>
+ <execution>
+ <id>sign-artifacts</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>sign</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
+
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 00000000..cd052808
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Address</code> 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
+ * <code><pre>
+ *
+ * URI = [scheme "://"] host [ ":" port ] [ path [ "?" query ]]
+ *
+ * </pre></code>
+ * 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.
+ * <p>
+ * 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 <code>http://domain/path</code> is
+ * a URI that is intended for the http protocol. The
+ * scheme is the string <code>http</code>.
+ *
+ * @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
+ * <code>http://domain/path?querypart</code>. 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 <code>http://host:port/path?querypart</code>. This
+ * will return the value of the port. If there is no port then
+ * this will return <code>-1</code> 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 <code>/</code>.
+ *
+ * @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 <code>Query</code> object. The query is
+ * an optional member of a URI and comes after the path part, it
+ * is preceded by a question mark, <code>?</code> character.
+ * For example the following URI contains <code>query</code> for
+ * its query part, <code>http://host:port/path?query</code>.
+ * <p>
+ * This returns a <code>org.simpleframework.http.Query</code>
+ * object that can be used to interact directly with the query
+ * values. The <code>Query</code> object is a read-only interface
+ * to the query parameters, and so will not affect the URI.
+ *
+ * @return a <code>Query</code> 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 <code>Map</code> instance.
+ * <p>
+ * 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 <code>http://domain/path1;x=y/path2;x=z</code>
+ * the value for the parameter named <code>x</code> would be
+ * <code>z</code>.
+ *
+ * @return this will return the parameter names found in the URI
+ */
+ KeyMap<String> getParameters();
+
+ /**
+ * This is used to convert this URI object into a <code>String</code>
+ * 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
+ * <pre>
+ * scheme://host:port/path/path?querypart
+ * </pre>
+ * <p>
+ * It can return <code>/path/path?querypart</code> 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.
+ * <pre>
+ * scheme://host:port/?querypart
+ * </pre>
+ *
+ * @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 00000000..579cb918
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContentDisposition</code> 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 <code>getName</code> can be used to determine the name, if
+ * it represents a file then <code>getFileName</code> 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 00000000..c62687d1
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>charset</code> parameter is one of many parameters that
+ * can be associated with a MIME type. This however this exposes this
+ * parameter with a typed method.
+ * <p>
+ * The <code>getCharset</code> 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 <code>text/plain; charset=UTF-8</code>. This will
+ * return the text value. If there is no primary type then this
+ * will return <code>null</code> 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 <code>text/html; charset=UTF-8</code>. This
+ * will return the HTML value. If there is no secondary type then
+ * this will return <code>null</code> otherwise the string value.
+ *
+ * @return the primary type part of this MIME type
+ */
+ String getSecondary();
+
+ /**
+ * This will set the <code>charset</code> to whatever value the
+ * string contains. If the string is null then this will not set
+ * the parameter to any value and the <code>toString</code> 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 <code>charset</code> 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 <code>boundary</code>
+ * 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 <code>boundary</code> 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 <code>charset</code> 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 00000000..a9ab422c
--- /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 <niallg@users.sf.net>
+ *
+ * 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
+ * <code>Cookie</code> is set to 1. The version can be configured
+ * using the <code>setVersion</code> method. The domain, path,
+ * security, and expiry of the cookie can also be set using their
+ * respective set methods.
+ * <p>
+ * The <code>toString</code> method allows the <code>Cookie</code>
+ * 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 <code>Cookie</code> 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 <code>Cookie</code> 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.
+ * <p>
+ * 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 <code>Cookie</code> 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.
+ * <p>
+ * 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 <code>Cookie</code> 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.
+ * <p>
+ * 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 <code>Cookie</code> 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.
+ * <p>
+ * 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 <code>Cookie</code> to be
+ * set. By default the version of the <code>Cookie</code> 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.
+ * <p>
+ * 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 <code>Cookie</code>
+ * is for, these values will always be present. These are
+ * mandatory for both the Cookie and Set-Cookie headers.
+ * <p>
+ * 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 <code>Cookie</code>
+ * 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 <code>some value</code>
+ * 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 <code>"some value"</code>.
+ *
+ * @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 <code>HttpOnly</code> 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 <code>HttpOnly</code> 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 <code>toString</code>
+ * or <code>toClientString</code> 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 <code>/pub/bin</code>, then requests for the
+ * resource <code>http://hostname:port/pub/bin/README</code>
+ * 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
+ * <code>toString</code> or <code>toClientString</code> 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 <code>Cookie</code> to be
+ * set. The form of the domain is specified by RFC 2109. The
+ * value can begin with a dot, like <code>.host.com</code>.
+ * This means that the cookie is visible within a specific
+ * DNS zone like <code>www.host.com</code>. 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 <code>toString</code> 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.
+ * <p>
+ * Netscape and IE-5.0 can't or wont handle <code>Path</code>
+ * it must be <code>path</code> also Netscape can not handle
+ * the path in quotations such as <code>"/path"</code> it must
+ * be <code>/path</code>. This value is never in quotations.
+ * <p>
+ * 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.
+ * <p>
+ * When setting the expiry time for the cookie it is important
+ * to set the <code>max-age</code> and <code>expires</code>
+ * attributes so that IE-5.0 and up can understand them. Old
+ * versions of IE do not understand <code>max-age</code>.
+ *
+ * @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 <code>CookieDate</code> 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.
+ * <pre>
+ *
+ * DAY, DD-MMM-YYYY HH:MM:SS GMT
+ *
+ * </pre>
+ * Support for this format is required as many browsers do
+ * not support <code>max-age</code> 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 <code>CookieDate</code> 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 00000000..5bb50270
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Method</code> 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 00000000..75660eaf
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Part</code> 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 <code>InputStream</code> 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 <code>InputStream</code> 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 00000000..fb07ef03
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Path</code> 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.
+ * <p>
+ * If the path that this represents is <code>/usr/bin/../etc/./README</code>
+ * then the actual path, normalized, is <code>/usr/etc/README</code>. 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 <code>file.en_US.extension</code>
+ * will produce an extension of <code>extension</code>. 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 <code>/usr/README</code> the name is <code>README</code>.
+ * 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 <code>/usr/../etc/./</code> the
+ * path is <code>/etc/</code>. 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
+ * <code>../</code> or <code>./</code>
+ */
+ 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 <code>substring</code> 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
+ * <code>../</code> or <code>./</code>
+ */
+ 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 <code>substring</code> 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
+ * <code>../</code> or <code>./</code>
+ */
+ 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 <code>/usr/bin/../etc</code> then the segments
+ * returned would be <code>usr</code> and <code>etc</code> 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 <code>/pub/./bin/README</code> would be to return
+ * the highest directory path <code>/pub/bin/</code>. 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 <code>getDirectory</code>. 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
+ * <code>/usr/share/rfc/rfc2396.txt</code> and the issued
+ * path was <code>/usr/share/text.txt</code> then this will
+ * return the path string <code>/rfc/rfc2396.txt</code>.
+ *
+ * @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 <code>/usr/../etc/./</code> the
+ * path is <code>/etc/</code>. 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
+ * <code>../</code> or <code>./</code>
+ */
+ 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 00000000..361e4c14
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Principal</code> interface is used to describe a
+ * user that has a name and password. This should not be
+ * confused with the <code>java.security.Principal</code>
+ * interface which does not provide <code>getPassword</code>.
+ *
+ * @author Niall Gallagher
+ */
+public interface Principal {
+
+ /**
+ * The <code>getPassword</code> 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 <code>getName</code> 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 00000000..295b6c65
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Request</code> and
+ * <code>Response</code> objects. This is used internally by the HTTP
+ * server to parse the incoming requests and also to submit response
+ * values for each conversation.
+ * <p>
+ * 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 00000000..5ab8afa0
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Query</code> 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<String, String> {
+
+ /**
+ * This method is used to acquire a <code>List</code> 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 <code>get(String)</code> 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<String> 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 <code>true</code> or
+ * <code>false</code> 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 <code>x-www-form-urlencoded</code>
+ * format is used to encode the attributes, see RFC 2616.
+ * <p>
+ * 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 00000000..2c83c28d
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Request</code> 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.
+ * <p>
+ * This can also maintain data during the request lifecycle as well
+ * as the session lifecycle. A <code>Session</code> 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.
+ * <p>
+ * It is important to note that the entity body can be read multiple
+ * times from the request. Calling <code>getInputStream</code> 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 <code>Connection: close</code>
+ * header. If the close token is present then this stream is not
+ * a keep-alive connection. If this has no <code>Connection</code>
+ * 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 <code>Map</code>
+ * 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 <code>Form</code> object.
+ * This basically acquires the parameters object and invokes
+ * the <code>getParameters</code> 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 <code>Part</code> 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 <code>Part</code> 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<Part> 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 <code>InputStream</code> can be determined
+ * by the <code>getContentLength</code> 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 <code>Part</code> 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 <code>ReadableByteChannel</code> can be
+ * determined by the <code>getContentLength</code> 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 00000000..d1ca7d02
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Header</code> 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.
+ * <p>
+ * 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 <code>List</code> 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<String> 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 <code>Cookie</code> 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 <code>Cookie</code> 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<Cookie> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> getValues(String name);
+
+ /**
+ * This is used to acquire the locales from the request header. The
+ * locales are provided in the <code>Accept-Language</code> 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<Locale> 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 <code>Content-Type</code> 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 <code>Content-Length</code> 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 <code>CharSequence</code> 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 00000000..b5b1abcf
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestLine</code> 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 <code>Path</code> 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 00000000..be81f5ee
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestWrapper</code> object is used so that the original
+ * <code>Request</code> object can be wrapped in a filtering proxy
+ * object. This allows a <code>Container</code> 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 <code>RequestWrapper</code> object. This allows
+ * the original <code>Request</code> 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 <code>Path</code> 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 <code>List</code> 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<String> 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 <code>Cookie</code> 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 <code>Cookie</code> 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<Cookie> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> getValues(String name) {
+ return request.getValues(name);
+ }
+
+ /**
+ * This is used to acquire the locales from the request header. The
+ * locales are provided in the <code>Accept-Language</code> 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<Locale> 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 <code>Content-Type</code> 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 <code>Content-Length</code> 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 <code>Connection: close</code>
+ * header. If the close token is present then this stream is not
+ * a keep-alive connection. If this has no <code>Connection</code>
+ * 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 <code>Map</code>
+ * 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 <code>CharSequence</code> 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 <code>InputStream</code> can be determined
+ * by the <code>getContentLength</code> 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 <code>Part</code> 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 <code>ReadableByteChannel</code> can be
+ * determined by the <code>getContentLength</code> 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 <code>Form</code> object.
+ * This basically acquires the parameters object and invokes
+ * the <code>getParameters</code> 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 <code>Part</code> 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 <code>Part</code> 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<Part> 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 00000000..e9e54daf
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>OutputStream</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * It is important to note that the <code>Response</code> 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 <code>close</code> 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).
+ * <p>
+ * 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 <code>Response</code>. The
+ * semantics of this <code>OutputStream</code> will be determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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 <code>Response</code>. The
+ * semantics of this <code>OutputStream</code> will be determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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.
+ * <p>
+ * This will ensure that there is buffering done so that the output
+ * can be reset using the <code>reset</code> 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 <code>print</code> methods provided by
+ * the <code>PrintStream</code>. This will basically wrap the
+ * <code>getOutputStream</code> with a buffer size of zero.
+ * <p>
+ * The retrieved <code>PrintStream</code> 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 <code>text/plain; charset=UTF-8</code> the
+ * resulting <code>PrintStream</code> 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.
+ * <p>
+ * Implementations of the <code>Response</code> must guarantee
+ * that this can be invoked repeatedly without effecting any issued
+ * <code>OutputStream</code> or <code>PrintStream</code> 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 <code>print</code> methods provided by
+ * the <code>PrintStream</code>. This will basically wrap the
+ * <code>getOutputStream</code> with a specified buffer size.
+ * <p>
+ * The retrieved <code>PrintStream</code> 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 <code>text/plain; charset=UTF-8</code> the
+ * resulting <code>PrintStream</code> 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.
+ * <p>
+ * Implementations of the <code>Response</code> must guarantee
+ * that this can be invoked repeatedly without effecting any issued
+ * <code>OutputStream</code> or <code>PrintStream</code> 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 <code>Response</code>. The
+ * semantics of this <code>WritableByteChannel</code> are determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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 <code>Response</code>. The
+ * semantics of this <code>WritableByteChannel</code> are determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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.
+ * <p>
+ * This will ensure that there is buffering done so that the output
+ * can be reset using the <code>reset</code> 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 <code>Connection</code> 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 <code>Response</code>
+ * has been committed. This is true if the <code>Response</code>
+ * was committed, either due to an explicit invocation of the
+ * <code>commit</code> method or due to the writing of content. If
+ * the <code>Response</code> has committed the <code>reset</code>
+ * 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
+ * <code>Response</code>. Any further attempts to give headers
+ * to the <code>Response</code> will be futile as only the headers
+ * that were given at the time of the first commit will be used
+ * in the message header.
+ * <p>
+ * 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 <code>Response</code>
+ * has been committed. This is true if the <code>Response</code>
+ * was committed, either due to an explicit invocation of the
+ * <code>commit</code> method or due to the writing of content. If
+ * the <code>Response</code> has committed the <code>reset</code>
+ * 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 00000000..5b369943
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseHeader</code> 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 <code>StatusLine</code> object, which exposes the
+ * protocol version and response status code.
+ * <p>
+ * 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<String> 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 <code>getValue</code> 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 <code>getInteger</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>remove</code> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> getValues(String name);
+
+ /**
+ * The <code>setCookie</code> 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 <code>setCookie</code> 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 <code>Cookie</code> object stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> 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 <code>Cookie</code> by the given name
+ */
+ Cookie getCookie(String name);
+
+ /**
+ * This returns all <code>Cookie</code> objects stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> methods. If there are no
+ * cookies then this will return an empty list.
+ *
+ * @return returns all the <code>Cookie</code> in the response
+ */
+ List<Cookie> 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 <code>Content-Type</code> 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 <code>Transfer-Encoding</code> 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 <code>Content-Length</code> 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 <code>CharSequence</code> 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 00000000..240384c0
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseWrapper</code> object is used so that the original
+ * <code>Response</code> 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 <code>Response</code> object in a concurrent environment.
+ * <pre>
+ *
+ * public void handle(Request req, Response resp) {
+ * handler.handle(req, new ZipResponse(resp));
+ * }
+ *
+ * </pre>
+ * The above is an example of how the <code>ResponseWrapper</code> can
+ * be used to provide extra functionality to a <code>Response</code>
+ * 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 <code>Container</code> 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 <code>ResponseWrapper</code> object. This allows
+ * the original <code>Response</code> 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 <code>Status</code> 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<String> 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 <code>getValue</code> 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 <code>getInteger</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>remove</code> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> getValues(String name) {
+ return response.getValues(name);
+ }
+
+ /**
+ * The <code>setCookie</code> 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 <code>setCookie</code> 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 <code>Cookie</code> object stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> 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 <code>Cookie</code> objects stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> methods. If there are no
+ * cookies then this will return an empty list.
+ *
+ * @return returns all the cookie objects for this response
+ */
+ public List<Cookie> 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 <code>Content-Type</code> 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 <code>Transfer-Encoding</code> 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 <code>Content-Length</code> 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).
+ * <p>
+ * 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 <code>OutputStream.close</code> 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 <code>CharSequence</code> 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 <code>Response</code>. The
+ * semantics of this <code>OutputStream</code> will be determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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 <code>OutputStream</code> 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 <code>Response</code>. The
+ * semantics of this <code>OutputStream</code> will be determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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 <code>OutputStream</code> issued must be thread safe so that
+ * it can be used in a concurrent environment.
+ * <p>
+ * This will ensure that there is buffering done so that the output
+ * can be reset using the <code>reset</code> 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 <code>print</code> methods provided by
+ * the <code>PrintStream</code>. This will basically wrap the
+ * <code>getOutputStream</code> with a buffer size of zero.
+ * <p>
+ * The retrieved <code>PrintStream</code> 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 <code>text/plain; charset=UTF-8</code> the
+ * resulting <code>PrintStream</code> 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.
+ * <p>
+ * Implementations of the <code>Response</code> must guarantee
+ * that this can be invoked repeatedly without effecting any issued
+ * <code>OutputStream</code> or <code>PrintStream</code> 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 <code>print</code> methods provided by
+ * the <code>PrintStream</code>. This will basically wrap the
+ * <code>getOutputStream</code> with a specified buffer size.
+ * <p>
+ * The retrieved <code>PrintStream</code> 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 <code>text/plain; charset=UTF-8</code> the
+ * resulting <code>PrintStream</code> 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.
+ * <p>
+ * Implementations of the <code>Response</code> must guarantee
+ * that this can be invoked repeatedly without effecting any issued
+ * <code>OutputStream</code> or <code>PrintStream</code> 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 <code>Response</code>. The
+ * semantics of this <code>WritableByteChannel</code> are determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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 <code>Response</code>. The
+ * semantics of this <code>WritableByteChannel</code> are determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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.
+ * <p>
+ * This will ensure that there is buffering done so that the output
+ * can be reset using the <code>reset</code> 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 <code>Connection</code> 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 <code>Response</code>
+ * has been committed. This is true if the <code>Response</code>
+ * was committed, either due to an explicit invocation of the
+ * <code>commit</code> method or due to the writing of content. If
+ * the <code>Response</code> has committed the <code>reset</code>
+ * 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
+ * <code>Response</code>. Any further attempts to give headers
+ * to the <code>Response</code> will be futile as only the headers
+ * that were given at the time of the first commit will be used
+ * in the message header.
+ * <p>
+ * 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 <code>Response</code>
+ * has been committed. This is true if the <code>Response</code>
+ * was committed, either due to an explicit invocation of the
+ * <code>commit</code> method or due to the writing of content. If
+ * the <code>Response</code> has committed the <code>reset</code>
+ * 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 00000000..b6df799f
--- /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 <niallg@users.sf.net>
+*
+* 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 <code>Scheme</code> 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 <code>Scheme</code> 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 <code>URI</code>. 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 00000000..7fa3b6f6
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Status</code> 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.
+ * <p>
+ * 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 <code>Status</code> 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 00000000..3ea0f162
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>StatusLine</code> 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 <code>Status</code> 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 00000000..a2cd6dd8
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BodyEncoder</code> 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 00000000..7a0a86ac
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BodyEncoderException</code> 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 <code>IOException</code>
+ * objects that are thrown from the underlying transport.
+ *
+ * @author Niall Gallagher
+ */
+class BodyEncoderException extends IOException {
+
+ /**
+ * Constructor for the <code>BodyEncoderException</code> 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 <code>BodyEncoderException</code> 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 00000000..93aab52f
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BodyEncoderFactory</code> 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 <code>BodyEncoderFactory</code> 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 <code>BodyEncoder</code> 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 <code>BodyEncoder</code> 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 00000000..eebefc71
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BodyObserver</code> 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.
+ * <p>
+ * If however the response is delivered successfully the monitor is
+ * notified of this event. On successful delivery the monitor will
+ * hand the <code>Channel</code> 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 00000000..5663d4b4
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ChunkedEncoder</code> 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 <code>CloseEncoder</code> 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 <code>ChunkedEncoder</code> 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 <code>ByteWriter</code> 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 00000000..8abd726a
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>CloseEncoder</code> 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 <code>CloseEncoder</code> 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 00000000..df5b4ca9
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Collector</code> 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 <code>Controller</code> 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 <code>Channel</code>
+ * 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 00000000..91a034c5
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Container</code> object is used to process HTTP requests
+ * and compose HTTP responses. The <code>Request</code> 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.
+ * <p>
+ * 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 <code>Response</code> object used to
+ * deliver the HTTP response will only commit and send once it has
+ * its <code>OutputStream</code> closed.
+ * <p>
+ * The <code>Container</code> 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 <code>Request</code> and <code>Response</code>
+ * 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.
+ * <p>
+ * 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 <code>Response</code> 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 00000000..16a63747
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContainerController</code> 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 <code>Container</code> 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 <code>ContainerController</code> 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 <code>Container</code> 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 <code>Selector</code> 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.
+ * <p>
+ * Here we stop the <code>Reactor</code> 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 <code>Executor</code> 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 00000000..ecd96a36
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContainerEvent</code> enum represents events that occur when
+ * processing a HTTP transaction. Here each phase of processing has a
+ * single event to represent it. If a <code>Trace</code> 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 00000000..0bf1a449
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContainerSocketProcessor</code> 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.
+ * <p>
+ * 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 <code>ContainerSocketProcessor</code> 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 <code>ContainerSocketProcessor</code> 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 <code>ContainerSocketProcessor</code> 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 <code>ContainerSocketProcessor</code> 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 <code>ContainerSocketProcessor</code> 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 <code>ContainerSocketProcessor</code> 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 <code>SSLEngine</code> 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 00000000..dd6df1ab
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContainerProcessor</code> object is used to create
+ * channels which can be used to consume and process requests. This
+ * is basically an adapter to the <code>Selector</code> 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 <code>ContainerProcessor</code> 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 <code>ContainerProcessor</code> 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 <code>Container</code> 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 00000000..3e152bdc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Controller</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>Container</code> 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 <code>Selector</code> 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 00000000..07318cea
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Conversation</code> 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 <code>Conversation</code> 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 <code>Request</code> 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 <code>Response</code> 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 <code>Response</code> 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 <code>isKeepAlive</code> 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 <code>isChunkable</code> 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 <code>isChunkedEncoded</code> 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 00000000..7ec78fb8
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>EmptyEncoder</code> 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 <code>EmptyEncoder</code> 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 00000000..86148eb9
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FixedLengthEncoder</code> 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 <code>FixedLengthEncoder</code> 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 00000000..7942d77b
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>QueryBuilder</code> 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 <code>QueryBuilder</code> 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 <code>IOException</code> 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 00000000..ed4c92ee
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>QueryCombimer</code> is used to parse several strings
+ * as a complete URL encoded parameter string. This will do the
+ * following concatenations.
+ *
+ * <pre>
+ * null + "a=b&amp;c=d&amp;e=f" = "a=b&amp;c=d&amp;e=f"
+ * "a=b" + "e=f&amp;g=h" = "a=b&amp;e=f&amp;g=h";
+ * "a=b&amp;c=d&amp;e=f" + "" = "a=b&amp;c=d&amp;e=f"
+ * </pre>
+ *
+ * This ensures that the <code>QueryForm</code> 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 <code>parse(String)</code> 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<String> keySet = query.keySet();
+
+ for(String key : keySet) {
+ List<String> 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 00000000..1da8b541
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestCertificate</code> represents a certificate for
+ * an HTTP request. It basically wraps the raw SSL certificate that
+ * comes with the <code>Channel</code>. 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 <code>RequestCertificate</code>. 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 <code>Runnable</code> 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 <code>Challenge</code> 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 <code>Challenge</code> 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<Certificate> 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<Certificate> 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 00000000..027318de
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestCollector</code> 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 <code>Selector</code> 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 <code>RequestCollector</code> object used to collect the data
+ * from the underlying transport. In order to collect a body this
+ * must be given an <code>Allocator</code> 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 <code>Channel</code>
+ * 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 <code>Part</code> 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 00000000..6b1531a8
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestDispatcher</code> 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 <code>RequestDispatcher</code> 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 <code>run</code> 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 <code>dispatch</code> 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 00000000..6060a4f0
--- /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 <niallg@users.sf.net>
+ *
+ * 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.
+ * <p>
+ * The <code>Request</code> is used to provide an interface to the
+ * HTTP <code>InputStream</code> and message header. The stream can
+ * have certain characteristics, these characteristics are available
+ * by this object. The <code>Request</code> provides methods that
+ * allow the <code>InputStream</code>'s semantics to be known, for
+ * example if the stream is keep-alive or if the stream has a length.
+ * <p>
+ * The <code>Request</code> origin is also retrievable from the
+ * <code>Request</code> as is the attributes <code>Map</code> object
+ * which defines specific connection attributes. And acts as a
+ * simple model for the request transaction.
+ * <p>
+ * It is important to note that the <code>Request</code> 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 <code>InputStream</code>. 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 <code>RequestEntity</code> 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 <code>InputStream</code> can be determined
+ * by the <code>getContentLength</code> 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 <code>Part</code> 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 <code>ReadableByteChannel</code> can be
+ * determined by the <code>getContentLength</code> 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 <code>Map</code>
+ * 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 <code>Form</code> object.
+ * This basically acquires the parameters object and invokes
+ * the <code>getParameters</code> 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 <code>Part</code> 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 <code>Part</code> 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<Part> 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 00000000..b22c74ef
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestMessage</code> 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 <code>RequestMessage</code> 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 <code>RequestMessage</code> 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 <code>Path</code> 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 <code>List</code> 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<String> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> getValues(String name) {
+ return header.getValues(name);
+ }
+
+ /**
+ * This is used to acquire the locales from the request header. The
+ * locales are provided in the <code>Accept-Language</code> 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<Locale> 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 <code>Cookie</code> 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 <code>Cookie</code> 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<Cookie> 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 <code>Content-Type</code> 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 <code>Content-Length</code> 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 <code>CharSequence</code> 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 00000000..6f8cbaa7
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestReader</code> 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 <code>RequestReader</code> 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 <code>SocketChannel</code> 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 <code>run</code> 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 <code>Selector</code> 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 00000000..d2a9f80f
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseBuffer</code> 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 <code>ResponseBuffer</code> 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 <code>ResponseBuffer</code> 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 00000000..0fbe39b3
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseEncoder</code> 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 <code>start</code> 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 <code>ResponseEncoder</code> object, this is
+ * used to create an object used to transfer a response body. This
+ * must be given a <code>Conversation</code> 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 <code>Producer</code> 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 <code>Producer</code> 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 <code>Producer</code> 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 <code>Producer</code> 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 00000000..ae7aed94
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>OutputStream</code> of the <code>Response</code> can be
+ * retrieved from this interface as can the I.P address of the client
+ * that will be receiving the <code>Response</code>. 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 <code>Response</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * It is important to note that the <code>Response</code> 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 <code>commit</code> method is invoked if there
+ * is NO content body. Committing with a content body is done only if
+ * correct content is given. The <code>OutputStream</code> acts as
+ * a client and commits the response once the specified content has
+ * been written to the issued <code>OutputStream</code>.
+ *
+ * @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 <code>ResponseEntity</code> 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 <code>Request</code> 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 <code>Attributes</code>
+ * 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 <code>Response</code>. The attributes contains certain
+ * properties about the <code>Response</code>. 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).
+ * <p>
+ * 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 <code>OutputStream.close</code> 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 <code>PrintStream</code> objects
+ * returned from the <code>getPrintStream</code> 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 <code>ISO-8859-1</code>,
+ * 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 <code>Response</code>. The
+ * semantics of this <code>OutputStream</code> will be determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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 <code>Response</code>. The
+ * semantics of this <code>OutputStream</code> will be determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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.
+ * <p>
+ * This will ensure that there is buffering done so that the output
+ * can be reset using the <code>reset</code> 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 <code>print</code> methods provided by
+ * the <code>PrintStream</code>. This will basically wrap the
+ * <code>getOutputStream</code> with a buffer size of zero.
+ * <p>
+ * The retrieved <code>PrintStream</code> 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 <code>text/plain; charset=UTF-8</code> the
+ * resulting <code>PrintStream</code> 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 <code>print</code> methods provided by
+ * the <code>PrintStream</code>. This will basically wrap the
+ * <code>getOutputStream</code> with a specified buffer size.
+ * <p>
+ * The retrieved <code>PrintStream</code> 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 <code>text/plain; charset=UTF-8</code> the
+ * resulting <code>PrintStream</code> 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 <code>getOutputStream</code> object in
+ * a <code>PrintStream</code>, which will write content using a
+ * specified charset. The <code>PrintStream</code> created will not
+ * buffer the content, it will write directly to the underlying
+ * <code>OutputStream</code> where it is buffered (if there is a
+ * buffer size greater than zero specified). In future the buffer
+ * of the <code>PrintStream</code> 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 <code>Response</code>. The
+ * semantics of this <code>WritableByteChannel</code> are determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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 <code>Response</code>. The
+ * semantics of this <code>WritableByteChannel</code> are determined
+ * by the HTTP version of the client, and whether or not the content
+ * length has been set, through the <code>setContentLength</code>
+ * 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.
+ * <p>
+ * This will ensure that there is buffering done so that the output
+ * can be reset using the <code>reset</code> 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 <code>Connection</code> 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 <code>Response</code>
+ * has been committed. This is true if the <code>Response</code>
+ * was committed, either due to an explicit invocation of the
+ * <code>commit</code> method or due to the writing of content. If
+ * the <code>Response</code> has committed the <code>reset</code>
+ * 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
+ * <code>Response</code>. Any further attempts to give headers
+ * to the <code>Response</code> will be futile as only the headers
+ * that were given at the time of the first commit will be used
+ * in the message header.
+ * <p>
+ * 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 <code>Response</code>
+ * has been committed. This is true if the <code>Response</code>
+ * was committed, either due to an explicit invocation of the
+ * <code>commit</code> method or due to the writing of content. If
+ * the <code>Response</code> has committed the <code>reset</code>
+ * 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 00000000..a02fe78d
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseException</code> 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 <code>IOException</code>
+ * objects that are thrown from the underlying transport.
+ *
+ * @author Niall Gallagher
+ */
+class ResponseException extends IOException {
+
+ /**
+ * Constructor for the <code>ResponseException</code> 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 <code>ResponseException</code> 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 00000000..4dcfb089
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseMessage</code> 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 <code>ResponseMessage</code> 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 <code>Status</code> 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 <code>Content-Type</code> 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 <code>Content-Length</code> 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 <code>Transfer-Encoding</code> 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 00000000..4a4b2c72
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseObserver</code> 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 <code>ResponseObserver</code> 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 00000000..c07d49b6
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Timer<code> 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 <code>Timer</code> 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 <code>set</code> 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 00000000..ef5db66b
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ArrayConsumer</code> 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 <code>scan</code> 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 <code>ArrayConsumer</code> 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 <code>ArrayConsumer</code> 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 <code>ArrayConsumer</code> 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 <code>ByteCursor</code> 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 <code>scan</code> 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 00000000..6b3599af
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Body</code> 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
+ * <code>Attachment</code> 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 <code>Part</code> 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 <code>Part</code> 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<Part> 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 00000000..d84d6edc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BodyConsumer</code> is used to consume the body of an
+ * HTTP message. Implementations of this consumer must provide the
+ * <code>Body</code> 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 <code>Part</code> 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 00000000..f519ce24
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BoundaryConsumer</code> 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 <code>BoundaryConsumer</code> 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 <code>-</code> 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 00000000..e0e8a752
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Body</code> 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
+ * <code>Attachment</code> 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 <code>BufferBody</code> 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 <code>BufferBody</code> 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 <code>BufferBody</code> 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 <code>Part</code> 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 <code>Part</code> 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<Part> 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 00000000..73a5ea00
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BufferPart</code> 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 <code>InputStream</code> 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 <code>BufferPart</code> 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 <code>InputStream</code> 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 00000000..886434dc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ByteConsumer</code> 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.
+ * <p>
+ * 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 <code>CRLF</code> 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 <code>ByteCursor</code> 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 00000000..41549d6a
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ChunkedConsumer</code> 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.
+ * <pre>
+ *
+ * length := 0
+ * read chunk-size, chunk-extension (if any) and CRLF
+ * while (chunk-size &gt; 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
+ * }
+ *
+ * </pre>
+ * 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 <code>ChunkedConsumer</code> 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 <code>ChunkedConsumer</code> 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 <code>Attachment</code> 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 <code>InputStream</code>, 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 <code>int</code>. 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 <code>byte</code>, is a hexidecimal
+ * char this will convert it into its integer equivelant. So a
+ * char of <code>A</code> is converted into <code>10</code>.
+ *
+ * @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 00000000..b3e5dc09
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ConsumerFactory</code> 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 <code>ConsumerFactory</code> 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 <code>Content-Length</code> 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 00000000..76742c26
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContentConsumer</code> 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 <code>ContentConsumer</code> 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 <code>Part</code> 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 <code>InputStream</code>, 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 00000000..56f64728
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContinueDispatcher</code> 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 <code>ContinueDispatcher</code> 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 00000000..9fb81454
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>EmptyConsumer</code> 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 <code>Attachment</code> 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 00000000..2d1f9ffa
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>EmptyInputStream</code> 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 00000000..6668eeb0
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Entity</code> object is used to represent the HTTP entity
+ * received from the client. The entity contains a header and body as
+ * well as the underlying <code>Channel</code> 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 <code>Part</code> 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 00000000..9cf12fbc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>EntityConsumer</code> 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 <code>Header</code> and <code>Body</code>
+ * 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 <code>EntityConsumer</code> 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 <code>Attachment</code> 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 00000000..83180138
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FileUploadConsumer</code> 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 <code>FileUploadConsumer</code> 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 <code>Part</code> 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 <code>ByteCursor</code> 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 <code>ByteCounter</code> 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 <code>Counter</code> 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 <code>reset</code> 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 <code>reset</code> 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 00000000..5358d4be
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FixedLengthConsumer</code> 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 <code>FixedLengthConsumer</code> 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 <code>Attachment</code> 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 <code>InputStream</code>, 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 00000000..4b79716e
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Header</code> 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.
+ * <p>
+ * 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 <code>CharSequence</code> 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 <code>Path</code> 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 <code>List</code> 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<String> 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 <code>Cookie</code> 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 <code>Cookie</code> 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<Cookie> getCookies();
+
+ /**
+ * This is used to acquire the locales from the request header. The
+ * locales are provided in the <code>Accept-Language</code> 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<Locale> 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 00000000..55dab407
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>HeaderConsumer</code> 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 <code>HeaderConsumer</code> object. This
+ * is used to create a consumer capable of reading a header from
+ * a provided cursor. All methods of the <code>Header</coder>
+ * 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 <code>List</code> 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<String> 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 <code>Cookie</code> 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 <code>Cookie</code> 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<Cookie> 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 00000000..ac01dff8
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Message</code> 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<String> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>remove</code> 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 <code>getValue</code> 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 <code>getInteger</code> 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 <code>Cookie</code> object stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> 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 <code>Cookie</code> by the given name
+ */
+ Cookie getCookie(String name);
+
+ /**
+ * This returns all <code>Cookie</code> objects stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> methods. If there are no
+ * cookies then this will return an empty list.
+ *
+ * @return returns all the <code>Cookie</code> in the response
+ */
+ List<Cookie> getCookies();
+
+ /**
+ * The <code>setCookie</code> 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 <code>setCookie</code> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> getValues(List<String> 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<String> 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 00000000..b809efe3
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Message</code> 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<Cookie> cookies;
+
+ /**
+ * This is used to store multiple header values for a name.
+ */
+ private final KeyMap<Series> values;
+
+ /**
+ * This is used to store the individual names for the header.
+ */
+ private final KeyMap<String> names;
+
+ /**
+ * This is used to parse all date headers added to the message.
+ */
+ private final DateParser parser;
+
+ /**
+ * Constructor for the <code>Message</code> 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<Cookie>();
+ this.values = new KeyMap<Series>();
+ this.names = new KeyMap<String>();
+ 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<String> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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<String> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>getValue</code> in combination with the get methods.
+ * This will perform a <code>remove</code> 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 <code>remove</code> 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 <code>getValue</code> 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<String> 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 <code>getInteger</code> 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<String> 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 <code>Cookie</code> object stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> 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 <code>Cookie</code> by the given name
+ */
+ public Cookie getCookie(String name) {
+ return cookies.get(name);
+ }
+
+ /**
+ * This returns all <code>Cookie</code> objects stored under the
+ * specified name. This is used to retrieve cookies that have been
+ * set with the <code>setCookie</code> methods. If there are no
+ * cookies then this will return an empty list.
+ *
+ * @return returns all the <code>Cookie</code> in the response
+ */
+ public List<Cookie> getCookies() {
+ return cookies.getValues();
+ }
+
+ /**
+ * The <code>setCookie</code> 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 <code>setCookie</code> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> getValues(List<String> 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<String> 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<String> 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 <code>Series</code> 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<String> value;
+
+ /**
+ * Constructor for the <code>Entry</code> 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<String>();
+ }
+
+ /**
+ * 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<String> 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 00000000..b30f28f7
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartBodyConsumer</code> 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 <code>PartBodyConsumer</code> 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 <code>PartBodyConsumer</code> 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 <code>Attachment</code> 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 <code>Part</code> 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 00000000..cc545581
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartConsumer</code> 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 <code>PartConsumer</code> 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 <code>Part</code> 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 00000000..cf7a90ad
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartData</code> 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 <code>Part</code> 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<Part> map;
+
+ /**
+ * This is the list of attachments for this part list object.
+ */
+ private final List<Part> list;
+
+ /**
+ * Constructor for the <code>PartData</code> 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<Part>();
+ this.map = new KeyMap<Part>();
+ }
+
+ /**
+ * 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<Part> 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 <code>Part</code> 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 00000000..8e1a38ca
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartEntryConsumer</code> 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 <code>PartEntryConsumer</code> 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 <code>Part</code> 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 00000000..aba57388
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartEntryFactory</code> 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 <code>PartEntryFactory</code> 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 00000000..f394ba67
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartFactory</code> 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 <code>PartFactory</code> 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 00000000..7612d8dc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartHeaderConsumer</code> 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 <code>PartHeaderConsumer</code> 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 00000000..c971d97d
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartSeries</code> 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 <code>Part</code> 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<Part> 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 <code>Part</code> 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 00000000..c3a04175
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PartSeriesConsumer</code> 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 <code>PartSeriesConsumer</code> 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 <code>PartSeriesConsumer</code> 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 <code>PartSeriesConsumer</code> 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 <code>PartSeriesConsumer</code> 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 <code>Attachment</code> 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 <code>PartSeries</code> 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 00000000..0687271c
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestConsumer</code> 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 <code>RequestConsumer</code> 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 <code>Path</code> 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 <code>List</code> 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<String> 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 <code>Address</code> 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 <code>CharSequence</code> 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 <code>CharSequence</code>.
+ * 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 <code>ByteSequence</code> 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 00000000..915b231e
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Segment</code> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> 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 <code>Content-Type</code> 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 <code>Content-Disposition</code> 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 <code>Transfer-Encoding</code> 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 <code>Content-Length</code> 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 00000000..5c994ce5
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>SegmentConsumer</code> 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 <code>SegmentConsumer</code> 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 <code>SegmentConsumer</code> 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 <code>Content-Type</code> 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 <code>Content-Length</code> 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 <code>Transfer-Encoding</code> 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 <code>Content-Disposition</code> 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 <code>Accept-Language</code> 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<Locale> 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 <code>substring</code> and <code>trim</code> calls.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String> 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 <code>Cookie</code> 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 <code>Accept-Language</code> 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 <code>ContentType</code> 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 <code>Disposition<code> 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 <code>Token</code> 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 00000000..2b48b7c2
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>TokenConsumer</code> 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 <code>TokenConsumer</code> 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 00000000..5d514c9b
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>UpdateConsumer</code> 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 <code>UpdateConsumer</code> 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 <code>UpdateConsumer</code> 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 <code>ByteCursor</code> 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 00000000..06d5509e
--- /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 <niallg@users.sf.net>
+ *
+ * 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
+ * <pre>
+ *
+ * URI = (absoluteURI | relativeURI)
+ * absoluteURI = scheme ":" ("//" netpath | relativeURI)
+ * relativeURI = path ["?" querypart]
+ * netpath = domain [":" port] relativeURI
+ * path = *("/" segment)
+ * segment = *pchar *( ";" param )
+ *
+ * </pre>
+ * This implements the <code>Address</code> 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 <code>http://domain/path1;x=y/path2;x=z</code> the value for
+ * the parameter named <code>x</code> would be <code>z</code>.
+ * <p>
+ * 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.
+ * <p>
+ * The <code>setPath</code> 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 <code>AddressParser</code>
+ * that contains no specifics. The instance will return
+ * <code>null</code> for all the get methods. The parsers
+ * get methods are populated by using the <code>parse</code>
+ * 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 <code>String</code> given to extract the specifics. This
+ * could be achieved by calling the default no-arg constructor
+ * and then using the instance to invoke the <code>parse</code>
+ * method on that <code>String</code> to extract the parts.
+ *
+ * @param text a <code>String</code> 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 <code>gopher://domain/path</code> is
+ * a URI that is intended for the gopher protocol. The
+ * scheme is the string <code>gopher</code>.
+ *
+ * @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
+ * <code>http://domain/path?querypart</code>. 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 <code>/</code> to indicate the root.
+ * <p>
+ * The <code>Path</code> object returned by this will contain
+ * no path parameters. The path parameters are available using
+ * the <code>Address</code> 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 <code>Query</code> object. The query is
+ * an optional member of a URI and comes after the path part, it
+ * is preceded by a question mark, <code>?</code> character.
+ * For example the following URI contains <code>query</code> for
+ * its query part, <code>http://host:port/path?query</code>.
+ * <p>
+ * This returns a <code>org.simpleframework.http.Query</code>
+ * object that can be used to interact directly with the query
+ * values. The <code>Query</code> object is a read-only interface
+ * to the query parameters, and so will not affect the URI.
+ *
+ * @return a <code>Query</code> 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 <code>http://host:port/path?querypart</code>. This
+ * will return the value of the port. If there is no port then
+ * this will return <code>-1</code> 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 <code>Map</code> instance.
+ * <p>
+ * 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 <code>http://domain/path1;x=y/path2;x=z</code>
+ * the value for the parameter named <code>x</code> would be
+ * <code>z</code>.
+ *
+ * @return this will return the parameter names found in the URI
+ */
+ public KeyMap<String> 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 <code>://</code> identifier
+ * to ensure that the <code>Address.toString</code> will
+ * produce the correct syntax.
+ * <p>
+ * Caution must be taken to ensure that the port and
+ * the scheme are consistent. So if the original URI
+ * was <code>http://domain:80/path</code> and the scheme
+ * was changed to <code>ftp</code> 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 <code>toString</code> method will not contain
+ * the domain. The result of the <code>toString</code>
+ * method will be <code>/path/path?query</code>. 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 <code>toString</code> method will not contain
+ * the domain. The result of the <code>toString</code>
+ * method will be <code>/path/path?query</code>. 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 <code>toString</code> will
+ * will not contain the optional port. If port number is above
+ * 0 then the <code>toString</code> method will produce a URI
+ * like <code>http://host:123/path</code> 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 <code>Address.toString</code> method will
+ * not contain the path, that is if path is null then it will be
+ * interpreted as <code>/</code>.
+ * <p>
+ * 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 <code>getPath</code> 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 <code>Address.toString</code> method
+ * will not contain the path, that is if path is null then it will
+ * be interpreted as <code>/</code>.
+ * <p>
+ * 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 <code>getPath</code> 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 <code>setPath</code>
+ * 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 <code>Address</code> 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 <code>Address.toString</code> method
+ * will not contain the query. If the query was <code>abc</code>
+ * then the <code>toString</code> method would produce a string
+ * like <code>http://host:port/path?abc</code>. If the query is
+ * null this URI would have no query part. The query must not
+ * contain the <code>?</code> 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 <code>Address.toString</code> method
+ * will not contain the query. If the <code>Query.toString</code>
+ * returns null then the query will be empty. This is basically
+ * the <code>setQuery(String)</code> method with the string value
+ * from the issued <code>Query.toString</code> method.
+ *
+ * @param query a <code>Query</code> 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
+ * <code>absoluteURI</code> or a <code>relativeURI</code>. 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 <code>toString</code>
+ * method has been called. Thus when the <code>toString</code>
+ * method is called then the token depends on the value of the
+ * cache alone in further calls to <code>toString</code>.
+ * 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
+ * <code><pre>
+ *
+ * absoluteURI = scheme ":" ("//" netpath | relativeURI)
+ * relativeURI = path ["?" querypart]
+ * netpath = domain [":" port] relativeURI
+ * path = *("/" segment)
+ * segment = *pchar *( ";" param )
+ *
+ * </pre></code>
+ * 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 <code>absoluteURI</code>.
+ */
+ 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.
+ * <p>
+ * The syntax for the scheme is given in RFC 2396 as follows
+ * <code><pre>
+ *
+ * scheme = alpha *( alpha | digit | "+" | "-" | "." )
+ *
+ * </pre></code>
+ * This will however also skips the "://" from the tag
+ * so of the URI was <code>gopher://domain/path</code> then
+ * the URI would be <code>domain/path</code> 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
+ * <code>"+", "-", "."</code>.
+ *
+ * @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
+ * <code><pre>
+ *
+ * netpath = domain [":" port] relativeURI
+ *
+ * </pre></code>
+ * 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 <code>relativeURI</code>.
+ */
+ 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 <code>String</code> 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 <code>toString</code> method to
+ * return the extracted value of the token rather than getting
+ * confused with previous values set by a previous parse method.
+ * <p>
+ * This uses the following delimiters to determine the end of the
+ * domain <code>?</code>,<code>:</code> and <code>/<code>. This
+ * ensures that the read offset does not go out of bounds and
+ * consequently throw an <code>IndexOutOfBoundsException</code>.
+ */
+ 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 <code>toString</code> method to
+ * return the extracted value of the token rather than getting
+ * confused with previous values set by a previous parse method.
+ * <p>
+ * 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 <code>IndexOutOfBoundsException</code>.
+ */
+ 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 <code>toString</code> 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.
+ * <p>
+ * 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
+ * <code>Token</code> 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 <code>getQuery</code> 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 <code>getQuery</code> 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.
+ * <p>
+ * 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 <code>"%" HEX HEX</code>.
+ * 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 <code>char</code> can only hold UCS-2, the UCS-4
+ * characters will have only the low order octets stored.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>"%" HEX HEX</code>
+ * 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.
+ * <p>
+ * 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 <code>"%" HEX HEX</code>, 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
+ * <code>char</code> or not. A hexidecimal character is consdered
+ * to be a character within the range of <code>0 - 9</code> and
+ * between <code>a - f</code> and <code>A - F</code>. This will
+ * return <code>true</code> 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 <code>chars</code>
+ * within a Uniform Resource Identifier. RFC 2396 defines
+ * an unreserved char as <code>alphanum | mark</code>.
+ *
+ * @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 <code>0 - 9</code> and between
+ * <code>a - z</code> it uses <code>iso-8859-1</code> 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
+ * <code>iso-8859-1</code> 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 <code>"-",
+ * "_", ".", "!", "~", "*", "'", "(", ")"</code>.
+ *
+ * @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 <code>":", "@", "=",
+ * "&amp;", "+", "$", ","</code> this will not check to see if the
+ * char is an escaped char, that is <code>% HEX HEX</code>. 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 <code>";", "/", "?",
+ * ":", "@", "&amp;", "=", "+", "$", ","</code>.
+ *
+ * @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 <code>String</code>
+ * 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
+ * <pre>
+ * scheme://host:port/path/path?querypart
+ * </pre>
+ * <p>
+ * It can return <code>/path/path?querypart</code> style relative
+ * URI's. If any of the parts are set to null then that part will be
+ * missing, for example if <code>setDomain</code> 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
+ * <code>setPath</code> then the path will be <code>/</code>. An
+ * example URI with the path part of null would be
+ * <pre>
+ * scheme://host:port/?querypart
+ * </pre>
+ *
+ * @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 <code>ParameterMap</code> 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<String> {
+
+ /**
+ * 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 <code>ParseBuffer</code>
+ * 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 <code>String</code> 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 <code>Token</code> 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 <code>Token</code> into it's
+ * <code>String</code> 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 <code>String</code> 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 00000000..b7fe6c2b
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ContentDispositionParser</code> 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 <code>ContentDispositionParser</code> 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 <code>ContentDispositionParser</code> 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 <code>Parser</code> when it is ready
+ * to parse a new <code>String</code>. 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
+ * <code>String</code> excluding the whitespace within literals.
+ * The definition of a literal can be found in RFC 2616.
+ * <p>
+ * 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 <code>filename</code> parameter within the set of parameters
+ * which are given to the type. The <code>filename</code> param and the
+ * the <code>name</code> 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 <code>filename</code> parameter within the set of parameters
+ * which are given to the type. The <code>filename</code> param and the
+ * the <code>name</code> 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 <code>IndexOutOfBoundsException</code> 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
+ * <code>char</code>'s upto but excluding the first terminal <code>char</code>
+ * 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 00000000..f42c073d
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>charset</code> parameter is one of many parameters that
+ * can be associated with a MIME type. This however this exposes this
+ * parameter with a typed method.
+ * <p>
+ * The <code>getCharset</code> 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<String> map;
+
+ /**
+ * The default constructor will create a <code>ContentParser</code>
+ * 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 <code>parse(String)</code>
+ * 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<String>();
+ }
+
+ /**
+ * This is primarily a convenience constructor. This will parse
+ * the <code>String</code> 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 <code>parse</code>
+ * method on that <code>String</code>.
+ *
+ * @param header <code>String</code> 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 <code>text/plain; charset=UTF-8</code>. This will
+ * return the text value. If there is no primary type then this
+ * will return <code>null</code> 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 <code>text/html; charset=UTF-8</code>. This
+ * will return the HTML value. If there is no secondary type then
+ * this will return <code>null</code> otherwise the string value.
+ *
+ * @return the primary type part of this MIME type
+ */
+ public String getSecondary(){
+ return secondary.toString();
+ }
+
+ /**
+ * This will set the <code>charset</code> to whatever value the
+ * string contains. If the string is null then this will not set
+ * the parameter to any value and the <code>toString</code> 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 <code>charset</code> 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 <code>boundary</code>
+ * 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 <code>boundary</code> 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 <code>String</code>. This will reset the parser to a
+ * ready state. The init method is invoked by the parser when
+ * the <code>Parser.parse</code> 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 <code>String</code>
+ * object. This uses the syntax defined by RFC 2616 for the media-type
+ * syntax. This parser is only concerned with one parameter, the
+ * <code>charset</code> parameter. The syntax for the media type is
+ * <pre>
+ * media-type = token "/" token *( ";" parameter )
+ * parameter = token | literal
+ * </pre>
+ */
+ protected void parse(){
+ primary();
+ off++;
+ secondary();
+ parameters();
+ }
+
+ /**
+ * This is used to remove all whitespace characters from the
+ * <code>String</code> excluding the whitespace within literals.
+ * The definition of a literal can be found in RFC 2616.
+ * <p>
+ * 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 <code>ParseBuffer</code>. 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
+ * <code>type/subtype;param=val;param2=val</code>.
+ */
+ 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 <code>ParseBuffer</code>. 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
+ * <code>type/subtype;param=val;param2=val</code>.
+ */
+ 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 <code>charset</code> parameter within the set of parameters
+ * which are given to the type. The <code>charset</code> param is the
+ * only parameter that this parser will tokenize.
+ * <p>
+ * This will remove any parameters that preceed the charset parameter.
+ * Once the <code>charset</code> 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. <code>type/subtype;param=val</code> 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 <code>charset</code> 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 <code>IndexOutOfBoundsException</code> 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
+ * <code>char</code>'s upto but excluding the first terminal <code>char</code>
+ * 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 <code>charset</code> param.
+ * This will fill the <code>charset</code> <code>ParseBuffer</code> and with
+ * the <code>charset</code> value. This will read a literal or a token as
+ * the <code>charset</code> value. If the <code>charset</code> 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 <code>charset</code> 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 <code>charset</code> 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 <code>charset</code> 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 00000000..1d07c047
--- /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 <niallg@users.sf.net>
+ *
+ * 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.
+ * <pre>
+ *
+ * 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
+ *
+ * </pre>
+ * 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.
+ * <p>
+ * Each cookie has a <code>$Version</code> attribute followed by multiple
+ * cookies. Each contains a name and a value, followed by an optional
+ * <code>$Path</code> and <code>$Domain</code> attribute. This will parse
+ * a given cookie header and return each cookie extracted as a
+ * <code>Cookie</code> object.
+ *
+ * @author Niall Gallagher
+ */
+public class CookieParser extends Parser implements Iterable<Cookie> {
+
+ /**
+ * Determines when the <code>Parser</code> has finished.
+ */
+ private boolean finished;
+
+ /**
+ * Used so the <code>Parser</code> does not parse twice.
+ */
+ private boolean parsed;
+
+ /**
+ * Version of the <code>Cookie</code> being parsed.
+ */
+ private int version;
+
+ /**
+ * Used to store the name of the <code>Cookie</code>.
+ */
+ private Token name;
+
+ /**
+ * Used to store the value of the <code>Cookie</code>.
+ */
+ private Token value;
+
+ /**
+ * Used to store the <code>$Path</code> values.
+ */
+ private Token path;
+
+ /**
+ * Used to store the <code>$Domain</code> values.
+ */
+ private Token domain;
+
+ /**
+ * Create a <code>CookieParser</code> that contains no cookies.
+ * the instance will return <code>false</code> for the
+ * <code>hasNext</code> method. cookies may be parsed using
+ * this instance by using the <code>parse</code> 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
+ * <code>String</code> given to extract the cookies. This could be
+ * achived by calling the default no-arg constructor and then using
+ * the instance to invoke the <code>parse</code> method on that
+ * <code>String</code>.
+ *
+ * @param header a <code>String</code> containing a cookie value
+ */
+ public CookieParser(String header){
+ this();
+ parse(header);
+ }
+
+ /**
+ * Resets the cookie and the buffer variables for this
+ * <code>CookieParser</code>. 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 <code>Cookie</code> 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 <code>Cookie</code>.
+ */
+ protected void parse() {
+ if(!finished){
+ cookie();
+ parsed=true;
+ }
+ }
+
+ /**
+ * This is used to skip an arbitrary <code>String</code> within the
+ * <code>char</code> buf. It checks the length of the <code>String</code>
+ * first to ensure that it will not go out of bounds. A comparison
+ * is then made with the buffers contents and the <code>String</code>
+ * if the reigon in the buffer matched the <code>String</code> then the
+ * offset within the buffer is increased by the <code>String</code>'s
+ * length so that it has effectively skipped it.
+ * <p>
+ * This <code>skip</code> 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 <code>String</code> value to be skipped
+ *
+ * @return true if the <code>String</code> 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<Cookie> iterator() {
+ return new Sequence();
+ }
+
+ /**
+ * This is used so that the collection of <code>Cookies</code>
+ * can be reiterated. This allows the collection to be reused.
+ * The <code>reset</code> method will invoke the super classes
+ * <code>init</code> method. This will reinitialize this
+ * <code>Parser</code> so the cookie will be reparsed.
+ */
+ public void reset() {
+ init();
+ parse();
+ }
+
+ /**
+ * Creates the <code>Cookie</code> from the token objects. It is
+ * assumed that the <code>Cookie</code> <code>String</code> has
+ * been parsed when this is called. This should only be used after
+ * the <code>parse</code> method has been called.
+ * <p>
+ * If there is no <code>$Domain</code> or <code>$Path</code>
+ * within the <code>Cookie</code> <code>String</code> then the
+ * <code>getDomain</code> and <code>getPath</code> are null.
+ *
+ * @return the <code>Cookie</code> that was just parsed
+ */
+ private Cookie getCookie() {
+ return getCookie(name.toString(),
+ value.toString());
+ }
+
+ /**
+ * Creates the <code>Cookie</code> from the token objects. It is
+ * assumed that the <code>Cookie</code> <code>String</code> has
+ * been parsed when this is called. This should only be used after
+ * the <code>parse</code> method has been called.
+ * <p>
+ * If there is no <code>$Domain</code> or <code>$Path</code>
+ * within the <code>Cookie</code> <code>String</code> then the
+ * <code>getDomain</code> and <code>getPath</code> are null.
+ *
+ * @param name the name that the <code>Cookie</code> contains
+ * @param value the value that the <code>Cookie</code> contains
+ *
+ * @return the <code>Cookie</code> 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 <code>Cookie</code> from the buffer
+ * that contains the <code>Cookie</code> values. This will first
+ * try to remove any trailing value after the version/prev
+ * <code>Cookie</code> once this is removed it will extract the
+ * name/value pair from the <code>Cookie</code>. The name and
+ * value of the <code>Cookie</code> 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
+ * <code>Cookie</code>. The offset and length of the name will be
+ * saved in the name token. This will read all <code>char</code>'s
+ * upto but excluding the first '=' <code>char</code> encountered
+ * from the <code>off</code> 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 <code>NAME '='</code>
+ * within a <code>Cookie</code>. This extracts the <code>Cookie</code>
+ * value the <code>$Path</code> and <code>$Domain</code> attributes
+ * if they exist (i.e. <code>$Path</code> and <code>$Domain</code>
+ * are optional in a cookie see RFC 2109).
+ * <p>
+ * The path method reads the terminal found before it as does the
+ * <code>domain</code> 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 <code>Cookie</code>.
+ */
+ private void value() {
+ data();
+ path();
+ domain();
+ }
+
+ /**
+ * This initializes the value token and extracts the value of this
+ * <code>Cookie</code>. The offset and length of the value will be
+ * saved in the value token. This will read all <code>char</code>'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 <code>$Path</code>
+ * of this <code>Cookie</code>. The offset and length of the path will
+ * be saved in the path token. This will read all <code>char</code>'s
+ * up to but excluding the first terminal <code>char</code> encountered
+ * from the <code>off</code> 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 '\').
+ * <p>
+ * This reads the terminal before the <code>$Path</code> so that if
+ * there is no <code>$Path</code> for the <code>Cookie</code> 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 <code>$Domain</code>
+ * of this <code>Cookie</code>. 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 <code>char</code> 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 '\').
+ * <p>
+ * This reads the terminal before the <code>$Domain</code> so that
+ * if there is no <code>$Domain</code> for the <code>Cookie</code>
+ * 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 <code>$Version</code> of this <code>Cookie</code>.
+ * The version is parsed and converted into a decimal int from the digit
+ * characters that make up a version.
+ * <p>
+ * This will read all digit <code>char</code>'s up to but excluding the
+ * first non digit <code>char</code> 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 <code>Iterator</code> 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<Cookie> {
+
+ /**
+ * Extracts the next <code>Cookie</code> object from the string
+ * given. This will return <code>null</code> when there are no
+ * more cookies left in the <code>String</code> being parsed.
+ * <p>
+ * To find out when there are no more cookies left use the
+ * <code>hasNext</code> method. This will only set the name,
+ * value, path, domain name version of the <code>cookie</code>
+ * because as of RFC 2109 these are the only attributes a
+ * <code>Cookie</code> may have, the path and domain are
+ * optional.
+ *
+ * @return an initialized <code>Cookie</code> object
+ */
+ public Cookie next(){
+ if(!hasNext()) {
+ return null;
+ }
+ parsed = false;
+ return getCookie();
+ }
+
+
+ /**
+ * Determine whether or not there are any <code>Cookie</code>s
+ * left in the <code>String</code>. This will attempt to extract
+ * another <code>Cookie</code> from the <code>String</code> and
+ * cache the result so the <code>next</code> method will produce
+ * this <code>Cookie</code>. If another <code>Cookie</code> cannot
+ * be parsed from the remainder of the <code>String</code> then
+ * this will return <code>false</code> otherwise it will return
+ * <code>true</code>.
+ *
+ * @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 <code>reset</code> 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 <code>CookieParser.buf</code>
+ * array. The <code>toString</code> method of this token will
+ * produce the <code>String</code> 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 <code>String</code>.
+ * This converts the region only if there is a sufficient length.
+ *
+ * @return the <code>String</code> 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 00000000..7efea9c3
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Parser</code> 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
+ * <pre>
+ * 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
+ * </pre>
+ * <p>
+ * This can also parse the date in ms as retrived from the <code>System</code>'s
+ * <code>System.currentTimeMillis</code> method. This has a parse method for a
+ * <code>long</code> which will do the same as the <code>parse(String)</code>.
+ * Once the date has been parsed there are two methods that allow the date
+ * to be represented, the <code>toLong</code> method converts the date to a
+ * <code>long</code> and the <code>toString</code> method will convert the date
+ * into a <code>String</code>.
+ * <p>
+ * This produces the same string as the <code>SimpleDateFormat.format</code>
+ * using the pattern <code>"EEE, dd MMM yyyy hh:mm:ss 'GMT'"</code>. 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
+ * <code>String</code>s 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 <code>long</code> argument
+ * in the constructor. This can also be done by first calling the no-arg
+ * constructor and then using the parse method.
+ * <p>
+ * 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 <code>String</code>
+ * argument in the constructor. This can also be done by first calling
+ * the no-arg constructor and then using the parse method.
+ * <p>
+ * 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 <code>long</code>. If this
+ * method is given the value of the date as a <code>long</code> it will
+ * construct the RFC 1123 date as required by RFC 2616 sec 3.3.
+ * <p>
+ * This saves time on parsing a <code>String</code> that is encoded in
+ * the HTTP-date format. The date given must be positive, if the date
+ * given is not a positive '<code>long</code>' 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 <code>Date</code> 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 <code>DateParser</code>. 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 <code>buf</code>. 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
+ * <pre>
+ * rfc1123 = 'wkday "," SP date1 SP time SP GMT'.
+ * date1 = '2DIGIT SP month SP 4DIGIT' and finally
+ * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'.
+ * </pre>
+ */
+ 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
+ * <pre>
+ * rfc850 = 'weekday "," SP date2 SP time SP GMT'.
+ * date2 = '2DIGIT "-" month "-" 2DIGIT' and finally
+ * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'.
+ * </pre>
+ */
+ 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
+ * <pre>
+ * asctime = 'weekday SP date3 SP time SP 4DIGIT'.
+ * date3 = 'month SP (2DIGIT | (SP 1DIGIT))' and
+ * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'.
+ * </pre>
+ */
+ 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
+ * <pre>
+ * date1 = '2DIGIT SP month SP 4DIGIT'.
+ * example '02 Jun 1982'.
+ * </pre>
+ */
+ 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
+ * <pre>
+ * date2 = '2DIGIT "-" month "-" 2DIGIT'
+ * example '02-Jun-82'.
+ * </pre>
+ */
+ 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
+ * <pre>
+ * date3 = 'month SP (2DIGIT | (SP 1DIGIT))'
+ * example 'Jun 2'.
+ * <pre>
+ */
+ 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.
+ * <p>
+ * 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).
+ * <p>
+ * 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 <code>00:00:00</code> that is
+ * <pre>
+ * time = '2DIGIT ":" 2DIGIT ":" 2DIGIT' so this will
+ * read only a time of that form, although this will
+ * parse time = '2DIGIT CHAR 2DIGIT CHAR 2DIGIT'.
+ * </pre>
+ */
+ 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <pre>
+ * Tue, 02 Jun 1982
+ * </pre>.
+ * 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.
+ * <pre>
+ * 23:59:59
+ * </pre>.
+ * 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 <code>long</code>, given the exact
+ * time this will use the <code>java.util.Date</code> to parse this date
+ * into a <code>long</code>. The <code>GregorianCalendar</code> uses
+ * the method <code>getTime</code> which produces the <code>Date</code>
+ * object from this the <code>getTime</code> returns the <code>long</code>
+ *
+ * @return the date parsed as a <code>long</code>
+ */
+ 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
+ * <pre>
+ * Tue, 02 Jun 1982 23:59:59 GMT
+ * </pre>.
+ * This uses a <code>StringBuffer</code> to accumulate the various
+ * <code>String</code>s/<code>int</code>s to form the resulting date
+ * value. The resulting date value is the one required by RFC 2616.
+ * <p>
+ * 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 00000000..0a2d215c
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Accept-Language</code>
+ * header. This takes in an <code>Accept-Language</code> header and parses
+ * it according the RFC 2616 BNF for the <code>Accept-Language</code> header.
+ * This also has the ability to sequence the language tokens in terms of
+ * the most preferred and the least preferred.
+ * <p>
+ * 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<Locale> {
+
+ /**
+ * This is used to create a <code>LanguageParser</code> for the
+ * <code>Accept-Language</code> 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 <code>parse(String)</code>.
+ */
+ public LanguageParser() {
+ super();
+ }
+
+ /**
+ * This is used to create a <code>LanguageParser</code> for the
+ * <code>Accept-Language</code> 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 <code>parse(String)</code>.
+ *
+ * @param text value of a <code>Accept-Language</code> header
+ */
+ public LanguageParser(String text) {
+ super(text);
+ }
+
+ /**
+ * This is used to create a <code>LanguageParser</code> for the
+ * <code>Accept-Language</code> 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 <code>parse(String)</code>.
+ *
+ * @param list value of a <code>Accept-Language</code> header
+ */
+ public LanguageParser(List<String> 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 <code>Locale</code> 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 <code>Locale</code> 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 00000000..166a2aa6
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ListParser</code> 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.
+ * <pre>
+ *
+ * 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
+ *
+ * </pre>
+ * 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.
+ * <p>
+ * 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 <code>list</code>
+ * 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<T> extends Parser {
+
+ /**
+ * Provides a quick means of sorting the values extracted.
+ */
+ private PriorityQueue<Entry> order;
+
+ /**
+ * Contains all the values extracted from the header(s).
+ */
+ private List<T> 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 <code>ListParser</code>. This creates a
+ * parser with no initial parse data, if there are headers to
+ * be parsed then the <code>parse(String)</code> method or
+ * <code>parse(List)</code> method can be used. This will
+ * parse a delimited list according so RFC 2616 section 4.2.
+ */
+ public ListParser(){
+ this.order = new PriorityQueue<Entry>();
+ this.list = new ArrayList<T>();
+ this.text = new char[0];
+ }
+
+ /**
+ * Constructor for the <code>ListParser</code>. 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 <code>list</code>
+ * 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 <code>ListParser</code>. 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 <code>list</code>
+ * method, which will also sort and trim the tokens.
+ *
+ * @param list a list of comma separated lists to be parsed
+ */
+ public ListParser(List<String> 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<String> 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<T> list() {
+ return list;
+ }
+
+ /**
+ * This is used to remove the <code>String</code> tokens from
+ * the priority queue and place those tokens in an array. The
+ * The <code>String</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <pre>
+ *
+ * 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,,
+ *
+ * </pre>
+ * 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 <code>PriorityQueue</code>. 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 <code>String.trim</code>
+ * 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 <code>PriorityQueue</code>. 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 <code>qvalue</code>
+ * 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 <code>Entry</code> 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<Entry> {
+
+ /**
+ * 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 <code>Entry</code> 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 00000000..7055e4e3
--- /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 <niallg@users.sf.net>
+ *
+ * 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.
+ * <p>
+ * If the path that this represents is <code>/usr/bin/../etc/./README</code>
+ * then the actual path, normalized, is <code>/usr/etc/README</code>. 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.
+ * <p>
+ * 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 <code>AddressParser</code> 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 <code>PathParser</code> that
+ * contains no specifics. The instance will return <code>null</code>
+ * for all the get methods. The <code>PathParser</code>'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
+ * <code>String</code> given to extract the specifics. This could be
+ * achived by calling the default no-arg constructor and then using
+ * the instance to invoke the <code>parse</code> method on that
+ * <code>String</code> to extract the parts.
+ *
+ * @param path a <code>String</code> 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
+ * <code>PathParser</code> will create the same <code>String</code>
+ * path given a set of paths that have different back references. For
+ * example the paths <code>/path/../path</code> and <code>/path</code>
+ * are the same path but different <code>String</code>'s.
+ * <p>
+ * This will NOT parse an immediate back reference as this signifies
+ * a path that cannot exist. So a path such as <code>/../</code> will
+ * result in a null for all methods. Paths such as <code>../bin</code>
+ * 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
+ * <code>Parser.parse</code> 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 <code>file.en_US.extension</code>
+ * will produce an extension of <code>extension</code>. 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 <code>/usr/README</code> the name is <code>README</code>.
+ * 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 <code>/usr/../etc/./</code> the
+ * path is <code>/etc/</code>. 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
+ * <code>../</code> or <code>./</code>
+ */
+ 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 <code>substring</code> 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
+ * <code>../</code> or <code>./</code>
+ */
+ 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 <code>substring</code> 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
+ * <code>../</code> or <code>./</code>
+ */
+ 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 <code>/pub/./bin/README</code> would be to return
+ * the highest directory path <code>/pub/bin/</code>. 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 <code>/usr/bin/../etc</code> then the segments
+ * returned would be <code>usr</code> and <code>etc</code> 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 <code>getDirectory</code>. 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
+ * <code>/usr/share/rfc/rfc2396.txt</code> and the issued
+ * path was <code>/usr/share/text.txt</code> then this will
+ * return the path string <code>/rfc/rfc2396.txt</code>.
+ *
+ * @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 <code>getRelative(String)</code> 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 <code>../</code>, 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 <code>getDirectory</code>. 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
+ * <code>/usr/share/rfc/rfc2396.txt</code> and the issued
+ * path was <code>/usr/share/text.txt</code> then this will
+ * return the path string <code>/rfc/rfc2396.txt</code>.
+ *
+ * @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 <code>String</code>
+ * 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 <code>null</code>.
+ */
+ 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 <code>/usr/bin/../etc/./README</code> then the actual
+ * path, the normalized path, is <code>/usr/etc/README</code>.
+ * <p>
+ * 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 <code>../</code> or <code>/..</code> 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 <code>/usr/README</code> the name is <code>README</code>.
+ * 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 <code>extract</code> 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 <code>/usr/../etc/./</code> the
+ * path is <code>/etc/</code>. 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
+ * <code>../</code> or <code>./</code>
+ */
+ public String toString(){
+ return getPath();
+ }
+
+ /**
+ * This is used so that the <code>PathParser</code> can speed
+ * up the parsing of the data. Rather than using a buffer like
+ * a <code>ParseBuffer</code> or worse a <code>StringBuffer</code>
+ * 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 <code>String</code> 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 <code>Token</code> 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 <code>Token</code> into it's
+ * <code>String</code> 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 <code>String</code> 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 <code>TokenList</code> class is used to store a list of
+ * tokens. This provides an <code>add</code> 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
+ * <code>list</code> 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 <code>TokenList</code> 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 <code>list</code> 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 <code>add</code> 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 <code>add</code> 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 <code>init</code> 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 00000000..52aeff8b
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>base64</code> encoding of the user and
+ * password pair.
+ * <p>
+ * This follows the parsing tree of RFC 2617. The goal of this parser
+ * is to decode the <code>base64</code> 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 <code>Basic</code> authorization scheme. The format of the basic
+ * scheme can be found in RFC 2617 and is of the form
+ * <pre>
+ * Basic SP base64-encoding.
+ * </pre>
+ *
+ * @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 <code>bytes</code> 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 <code>Parser</code> 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 <code>Parser</code> 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 <code>String</code> 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 <code>null</code>
+ *
+ * @return the password for the user or <code>null</code>
+ */
+ public String getPassword(){
+ if(password.length() == 0){
+ return null;
+ }
+ return password.toString();
+ }
+
+ /**
+ * Gets the users name from the Authorization header value.
+ * This will return <code>null</code> 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 <code>Parser</code> when it is ready
+ * to parse a new <code>String</code>. This will reset the
+ * <code>Parser</code> to a ready state. The <code>init</code> method
+ * is invoked by the <code>Parser</code> when the <code>parse</code>
+ * 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
+ * <code>String</code> excluding the whitespace within literals.
+ * The definition of a literal can be found in RFC 2616.
+ * <p>
+ * 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 <len){ /* trim start*/
+ if(!space(buf[seek])){
+ break;
+ }
+ seek++;
+ }
+ while(seek < len){
+ ch = buf[seek++];
+ if(space(ch)){
+ while(seek < len){ /* skip spaces */
+ if(!space(buf[seek])){
+ break;
+ }
+ seek++;
+ }
+ }
+ buf[pos++] = ch;
+ }
+ if(space(ch)){ /* trim end */
+ pos--;
+ }
+ count = pos;
+ }
+
+ /**
+ * Extracts the name and password of the user from the
+ * <code>name : password</code> 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 <code>base64</code> encoding of
+ * the user name and password. This uses a standart <code>base64</code>
+ * decoding scheme.
+ * <p>
+ * For information on the decoding scheme used for <code>base64</code>
+ * 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 <code>byte</code> character to the
+ * <code>byte</code> number.
+ * <p>
+ * 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 <code>byte</code> from the set of four
+ * <code>bytes</code> given. This method is used to isolate the correct
+ * bits that corrospond to an actual character withing the
+ * <code>base64</code> data.
+ *
+ * @param four this is the four <code>bytes</code> 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 <code>byte</code> from the set of four
+ * <code>bytes</code> given. This method is used to isolate the correct
+ * bits that corrospond to an actual character withing the
+ * <code>base64</code> data.
+ *
+ * @param four this is the four <code>bytes</code> 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 <code>byte</code> from the set of four
+ * <code>bytes</code> given. This method is used to isolate the correct
+ * bits that corrospond to an actual character withing the
+ * <code>base64</code> data.
+ *
+ * @param four this is the four <code>bytes</code> 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
+ * <code>TEXT</code> character according to the HTTP specification,
+ * that is RFC 2616 specifies a <code>TEXT</code> 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 <code>TEXT</code>
+ */
+ 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 00000000..56b67880
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ParameterParser</code> is used to parse data encoded in
+ * the <code>application/x-www-form-urlencoded</code> 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 <code>org.simpleframework.http.net.Query</code> interface. The
+ * syntax of the parsed parameters is described below in BNF.
+ * <pre>
+ *
+ * params = *(pair [ "&amp;" params])
+ * pair = name "=" value
+ * name = *(text | escaped)
+ * value = *(text | escaped)
+ * escaped = % HEX HEX
+ *
+ * </pre>
+ * 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 "=", "&amp;", and "%" as having special values.
+ * The "=" character delimits the name from the value and the "&amp;"
+ * 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<String> 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 <code>ParameterParser</code>. 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
+ * <code>org.simpleframework.util.net.Query</code>.
+ */
+ public QueryParser(){
+ this.name = new Token();
+ this.value = new Token();
+ }
+
+ /**
+ * Constructor for the <code>ParameterParser</code>. 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
+ * <code>org.simpleframework.util.net.Query</code>.
+ *
+ * @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 <code>true</code> or
+ * <code>false</code> 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 <code>parse(String)</code> is
+ * invoked the status of the <code>Query</code> 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 "&amp;".
+ * 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
+ * <code>Token</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>"%" HEX HEX</code>.
+ * 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 <code>char</code> can only hold UCS-2, the UCS-4
+ * characters will have only the low order octets stored.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>"%" HEX HEX</code>
+ * 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.
+ * <p>
+ * 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 <code>"%" HEX HEX</code>, 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
+ * <code>char</code> or not. A hexadecimal character is considered
+ * to be a character within the range of <code>0 - 9</code> and
+ * between <code>a - f</code> and <code>A - F</code>. This will
+ * return <code>true</code> 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 <code>encode</code> 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 <code>encode</code> 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 <code>toString</code> method is used to compose an string
+ * in the <code>application/x-www-form-urlencoded</code> MIME type.
+ * This will encode the tokens specified in the <code>Set</code>.
+ * 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 <code>toString</code> method is used to compose an string
+ * in the <code>application/x-www-form-urlencoded</code> 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 <code>ParseBuffer</code>
+ * 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 <code>Token</code> as a value
+ * that can be used this converts it to a <code>String</code>.
+ * 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 00000000..99b16b9d
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ValueParser</code> 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.
+ * <pre>
+ *
+ * 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
+ *
+ * </pre>
+ * 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.
+ * <p>
+ * 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 <code>list</code>
+ * 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<String> {
+
+ /**
+ * Constructor for the <code>ValueParser</code>. This creates
+ * a parser with no initial parse data, if there are headers to
+ * be parsed then the <code>parse(String)</code> method or
+ * <code>parse(List)</code> method can be used. This will
+ * parse a delimited list according so RFC 2616 section 4.2.
+ */
+ public ValueParser(){
+ super();
+ }
+
+ /**
+ * Constructor for the <code>ValueParser</code>. 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 <code>list</code>
+ * 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 <code>ValueParser</code>. 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 <code>list</code>
+ * method, which will also sort and trim the tokens.
+ *
+ * @param list a list of comma separated lists to be parsed
+ */
+ public ValueParser(List<String> 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 00000000..bea3c632
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>BinaryData</code> 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 <code>BinaryData</code> 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 00000000..c64c6056
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>CloseCode</code> 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 <code>CloseCode</code> 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 00000000..bb798306
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Data</code> 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 00000000..5713fd63
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>DataConverter</code> 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 <code>DataConverter</code> 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 <code>DataConverter</code> 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 00000000..b51cd2b8
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>DataFrame</code> 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 <code>FrameType</code> 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 <code>DataFrame</code> 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 <code>DataFrame</code> 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 <code>DataFrame</code> 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 <code>DataFrame</code> 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 <code>DataFrame</code> 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 <code>DataFrame</code> 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 <code>DataFrame</code> 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 00000000..7f5ad0fc
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Frame</code> 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 <code>FrameType</code> 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 00000000..bcacc434
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameChannel</code> 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.
+ * <p>
+ * 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 <code>FrameListener</code> 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 <code>FrameListener</code> 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 00000000..6892e9cf
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameListener</code> is used to listen for incoming frames
+ * on a <code>WebSocket</code>. 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 00000000..82377011
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameType</code> 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 <code>Frame</code> 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 00000000..c7438e55
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Reason</code> 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 <code>Reason</code> 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 <code>Reason</code> 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 00000000..7c9a7dbb
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Session</code> 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 <code>Map</code>
+ * 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 <code>FrameChannel</code> that can be used to communicate
+ * with the connected client. Communication is full duplex and also
+ * asynchronous through the use of a <code>FrameListener</code> that
+ * can be registered with the channel.
+ *
+ * @return a web socket for full duplex communication
+ */
+ FrameChannel getChannel();
+
+ /**
+ * Provides the <code>Request</code> 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 <code>Response</code> 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 00000000..24ee97d7
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>TextData</code> 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 <code>TextData</code> 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 00000000..2fe25215
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>AcceptToken</code> 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 <code>Sec-WebSocket-Key</code> header
+ * field in the client handshake, the second is the globally unique
+ * identifier <code>258EAFA5-E914-47DA-95CA-C5AB0DC85B11</code>. 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 <code>AcceptToken</code> object. This is
+ * to create an object that can generate a token from the client
+ * key available from the <code>Sec-WebSocket-Key</code> header.
+ *
+ * @param request this is the session initiating request
+ */
+ public AcceptToken(Request request) throws Exception {
+ this(request, "SHA-1");
+ }
+
+ /**
+ * Constructor for the <code>AcceptToken</code> object. This is
+ * to create an object that can generate a token from the client
+ * key available from the <code>Sec-WebSocket-Key</code> 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 <code>AcceptToken</code> object. This is
+ * to create an object that can generate a token from the client
+ * key available from the <code>Sec-WebSocket-Key</code> 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 <code>Sec-WebSocket-Key</code> 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 00000000..0c09063c
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>DirectRouter</code> 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 <code>DirectRouter</code> 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 <code>DirectRouter</code> 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 00000000..6ab224a8
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameBuilder</code> 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 <code>FrameBuilder</code> 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 00000000..89876205
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameCollector</code> operation is used to collect frames
+ * from a channel and dispatch them to a <code>FrameListener</code>.
+ * 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 <code>FrameCollector</code> 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 <code>FrameListener</code> 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 <code>FrameListener</code> 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 00000000..b9041302
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameConnection</code> 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.
+ * <p>
+ * 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 <code>FrameConnection</code> 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 <code>FrameChannel</code> 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 <code>FrameListener</code> 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 <code>FrameListener</code> 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 00000000..579d6ef6
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameConsumer</code> 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 <code>FrameConsumer</code> 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 00000000..1a99d308
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameEncoder</code> 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 <code>FrameEncoder</code> 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 <code>FrameEncoder</code> 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 00000000..a246451a
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameHeader</code> 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 00000000..d651ea91
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameHeaderConsumer</code> 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 <code>FrameHeaderConsumer</code> 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 00000000..c7528d4d
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>FrameProcessor</code> 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 <code>FrameListener</code>
+ * 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<FrameListener> 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 <code>FrameProcessor</code> 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<FrameListener>();
+ 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 <code>FrameListener</code> 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 <code>FrameListener</code> 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 <code>FrameListener</code> 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 00000000..3da2635a
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>OutputBarrier</code> 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 <code>OutputBarrier</code> object. This
+ * is used to ensure that if there is currently a blocking write
+ * in place that the <code>SessionChecker</code> 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 00000000..9deb66ac
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>PathRouter</code> 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<String, Service> registry;
+
+ /**
+ * This is the default service chosen if there is no match.
+ */
+ private final Service primary;
+
+ /**
+ * Constructor for the <code>PathRouter</code> 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<String, Service> 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<String> 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 00000000..54060c93
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ProtocolRouter</code> 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<String, Service> registry;
+
+ /**
+ * This is the default service chosen if there is no match.
+ */
+ private final Service primary;
+
+ /**
+ * Constructor for the <code>ProtocolRouter</code> object. This is
+ * used to create a router using a selection of services that can
+ * be selected using the <code>Sec-WebSocket-Protocol</code> 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<String, Service> 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<String> 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 00000000..fb6ce880
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ReasonExtractor</code> object is used to extract the close
+ * reason from a frame payload. If their is no close reason this will
+ * return a <code>Reason</code> 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 <code>ReasonExtractor</code> 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 00000000..7e47dc37
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RequestValidator</code> 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 <code>RequestValidator</code> 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 <code>RequestValidator</code> 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<String> 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 00000000..0ba780ee
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ResponseBuilder</code> 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 <code>ResponseBuilder</code> 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 00000000..3b466f55
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Router</code> 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 <code>Connection</code> header with the <code>websocket</code>
+ * 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 <code>Service</code> will not be resolved.
+ * <p>
+ * If a service has been successfully chosen from the initiating
+ * request the the value of <code>Sec-WebSocket-Protocol</code> 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 00000000..3b018a9c
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>RouterContainer</code> is used to route requests that
+ * satisfy a WebSocket opening handshake to a specific service. Each
+ * request intercepted by this <code>Container</code> 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 <code>Router</code> 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 <code>RouterContainer</code> object. This
+ * requires a container to delegate traditional requests to and
+ * a <code>Router</code> 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 <code>RouterContainer</code> object. This
+ * requires a container to delegate traditional requests to and
+ * a <code>Router</code> 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 <code>Session</code> 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 00000000..d95c01f4
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>Service</code> 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 00000000..ad5325ca
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ServiceChannel</code> 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.
+ * <p>
+ * 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 <code>ServiceChannel</code> 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 <code>FrameListener</code> 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 <code>FrameListener</code> 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 00000000..509495f2
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ServiceDispatcher</code> object is used to perform the
+ * opening handshake for a WebSocket session. Once the session has been
+ * established it is connected to a <code>Service</code> 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 <code>ServiceDispatcher</code> object. The
+ * dispatcher created will dispatch WebSocket sessions to a service
+ * using the provided <code>Router</code> 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 <code>ServiceDispatcher</code> object. The
+ * dispatcher created will dispatch WebSocket sessions to a service
+ * using the provided <code>Router</code> 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 <code>Session</code> 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 00000000..a5d00792
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ServiceEvent</code> 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 <code>TraceAnalyzer</code> 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 00000000..b8fc0838
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>ServiceSession</code> 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 <code>ServiceSession</code> object. This is used
+ * to create the session that will be used by a <code>Service</code> 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 <code>Map</code>
+ * 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 <code>WebSocket</code> that can be used to communicate
+ * with the connected client. Communication is full duplex and also
+ * asynchronous through the use of a <code>FrameListener</code> that
+ * can be registered with the socket.
+ *
+ * @return a web socket for full duplex communication
+ */
+ public FrameChannel getChannel() {
+ return channel;
+ }
+
+ /**
+ * Provides the <code>Request</code> 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 <code>Response</code> 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 00000000..59864ee7
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>SessionBuilder</code> 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 <code>SessionBuilder</code> 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 <code>Session</code> 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 00000000..6543be0b
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>SessionDispatcher</code> object is used to perform the
+ * opening handshake for a WebSocket session. Once the session has been
+ * established it is connected to a <code>Service</code> 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 <code>SessionDispatcher</code> object. The
+ * dispatcher created will dispatch WebSocket sessions to a service
+ * using the provided <code>Router</code> 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 <code>Session</code> 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 00000000..1f4f0d7f
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>StatusChecker</code> 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 <code>StatusChecker</code> 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 00000000..2b2a0497
--- /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 <niallg@users.sf.net>
+ *
+ * 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 <code>StatusResultListener</code> 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 <code>StatusResultListener</code> 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 00000000..9047de55
--- /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<String> 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<String> list;
+ private final List<java.net.Socket> sockets;
+
+ public Pinger(int port, boolean socket, int count) throws Exception {
+ this.executor = new ConcurrentExecutor(Pinger.class, count);
+ this.list = new Vector<String>(count);
+ this.sockets = new Vector<java.net.Socket>(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<String> 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 00000000..9fd44414
--- /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 00000000..b916494a
--- /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 00000000..76bfb6ef
--- /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<Certificate> 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<SelectableChannel, Integer> map;
+ private final AtomicInteger count;
+
+ public ConsoleAgent() {
+ this.map = new ConcurrentHashMap<SelectableChannel, Integer>();
+ 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 00000000..be11f594
--- /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 00000000..2f22a0a0
--- /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 00000000..0ee0e7d4
--- /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 00000000..6bb8ef5f
--- /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 00000000..9ca5fe7c
--- /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 00000000..70db4a30
--- /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 00000000..9f7a6bb5
--- /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 00000000..8f5ef139
--- /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 00000000..13d2f30f
--- /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 00000000..d002c865
--- /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 00000000..00855c29
--- /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 00000000..14f2768c
--- /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 00000000..f0011ce5
--- /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 00000000..f7b8f335
--- /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 00000000..b972f039
--- /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 00000000..92a4f5d4
--- /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 00000000..b6318036
--- /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 00000000..e0ec8966
--- /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 00000000..cb4b41ed
--- /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 00000000..614a7aa8
--- /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 00000000..a7f12b64
--- /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<String> 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 00000000..f382a321
--- /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<Part> 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<String> 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<String> 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 00000000..43c0b869
--- /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 00000000..eb209301
--- /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 00000000..5ac7a14f
--- /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 00000000..6b23fab6
--- /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<Part> 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 00000000..d81370ed
--- /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 00000000..92c9d64d
--- /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 00000000..9b0bdcde
--- /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<String, StopWatch> timers = new ConcurrentHashMap<String, StopWatch>();
+
+ private LinkedBlockingQueue<StopWatch> finished = new LinkedBlockingQueue<StopWatch>();
+
+ 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<Part> 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 00000000..b0aae802
--- /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<Part> 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 00000000..ae9672f8
--- /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 00000000..47cbf35a
--- /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<Part> 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 00000000..c48b2489
--- /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<Cookie> cookies;
+ private String response;
+ private byte[] body;
+ private Map map;
+
+ public Result(String response, byte[] body, Map map, List<Cookie> cookies) {
+ this.cookies = cookies;
+ this.response = response;
+ this.body = body;
+ this.map = map;
+ }
+
+ public List<Cookie> 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 00000000..67751b89
--- /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 00000000..d6f6a099
--- /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 00000000..85960eda
--- /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<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces();
+ return generateDump(stackTraces);
+ }
+
+ public static String dumpCurrentThread() {
+ Thread currentThread = Thread.currentThread();
+ StackTraceElement[] stackTrace = currentThread.getStackTrace();
+ Map<Thread, StackTraceElement[]> stackTraces = Collections.singletonMap(currentThread, stackTrace);
+ return generateDump(stackTraces);
+
+ }
+
+ private static String generateDump(Map<Thread, StackTraceElement[]> stackTraces) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("<pre>");
+ builder.append("<b>Full Java thread dump</b>");
+ builder.append("\n");
+
+ Set<Thread> threads = stackTraces.keySet();
+
+ for (Thread thread : threads) {
+ StackTraceElement[] stackElements = stackTraces.get(thread);
+
+ generateDescription(thread, builder);
+ generateStackFrames(stackElements, builder);
+ }
+ builder.append("</pre>");
+ 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("<b>");
+ builder.append(threadName);
+ builder.append("</b> 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 00000000..60fcb5c1
--- /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 00000000..4636cc7f
--- /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 00000000..0d0d73dc
--- /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 00000000..ea6e313c
--- /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<Response> responses = new LinkedBlockingQueue<Response>();
+
+ 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 00000000..8f52100b
--- /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 00000000..3b860202
--- /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 00000000..a6f4f628
--- /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", "<IDENTITY>");
+
+ 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 00000000..d668a4a9
--- /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"+
+ "<stage version=\"2.0\" keygen_seq=\"1\"><pageObj print_grid=\"0\" border=\"0\" gr=\"1\" width=\"5000\" highResImage=\"1\" height=\"5000\" drawingHeight=\"379\" print_paper=\"LETTER\" istt=\"false\" guides=\"0\" print_layout=\"0\" print_scale=\"0\" drawingWidth=\"188\" fill=\"16777215\" pb=\"0\"><styles><shapeStyle lineColor=\"global:0x333333\" lineWidth=\"-1\" gradientOn=\"true\" dropShadowOn=\"true\" fillColor=\"global:0xd1d1d1\"/><lineStyle borderLine=\"false\" connType=\"right\" width=\"1\" roundCorners=\"true\" begin=\"0\" color=\"0x000000\" end=\"0\" pattern=\"0\"/><textStyle face=\"Arial\" size=\"12\" color=\"0\" style=\"\"/></styles><objects><object shp_id=\"0\" x=\"158\" order=\"0\" y=\"361.5\" linec=\"3355443\" dsy=\"4\" height=\"75\" symbol_id=\"\" gradon=\"true\" text-vertical-pos=\"middle\" width=\"100\" dshad=\"true\" class=\"rectangle\" dsx=\"4\" linew=\"2\" fill=\"0xd1d1d1\" fixed-aspect=\"false\" rot=\"0\" lock=\"false\" libraryid=\"com.gliffy.symbols.basic\" text-horizontal-pos=\"center\"><text/><connlines/></object></objects></pageObj></stage>\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 00000000..36c03092
--- /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 00000000..4a4ee756
--- /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<Part> 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 00000000..67ea0910
--- /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 00000000..7f49acb1
--- /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<String> 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<String> 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 00000000..4f96405b
--- /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 00000000..448dad50
--- /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 00000000..a7fbfe5f
--- /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 00000000..1c6916e7
--- /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 00000000..de7461c2
--- /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 00000000..0b93a900
--- /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 00000000..e5b02665
--- /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 00000000..863440ec
--- /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 00000000..9c0c7b89
--- /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<Cookie> cookies = new ArrayList<Cookie>();
+
+ 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 00000000..f262d10a
--- /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 00000000..2d382f04
--- /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 00000000..100bf275
--- /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 00000000..5c14d00c
--- /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 00000000..4ae4d606
--- /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 00000000..5c03ebd7
--- /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<Entry> {
+
+ 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<Entry> queue = new PriorityQueue<Entry>();
+ 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 00000000..aae92ff7
--- /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 00000000..037627d2
--- /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 00000000..3e896525
--- /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<SelectableChannel, Integer> map;
+ private final AtomicInteger count;
+ private final boolean debug;
+
+ public WebSocketAnalyzer() {
+ this(true);
+ }
+
+ public WebSocketAnalyzer(boolean debug) {
+ this.map = new ConcurrentHashMap<SelectableChannel, Integer>();
+ 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 00000000..99018e39
--- /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 00000000..7d27b9f3
--- /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 00000000..47114949
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatLogin.html
@@ -0,0 +1,12 @@
+ <html>
+ <head>
+ <title>Login Page</title>
+ </head>
+ <body>
+ <h1>Please Login</h1>
+ <form action='/login' method='POST'>
+ <input type='text' name='user'/>
+ <input type='submit' value='Sign In'/>
+ </form>
+ </body>
+</html> \ 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 00000000..1443c691
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/WebSocketChatRoom.html
@@ -0,0 +1,29 @@
+<html>
+ <head>
+ <title>WebSocket Echo Test</title>
+ <script>
+ function init() {
+ websocket = new WebSocket("ws://localhost:6060/talk");
+
+ websocket.onopen = function() { document.getElementById("output").innerHTML += "<p>> CONNECTED</p>"; };
+
+ websocket.onmessage = function(evt) { document.getElementById("output").innerHTML += "<p style='color: blue;'>> RECEIVE: " + evt.data + "</p>"; };
+
+ websocket.onerror = function(evt) { document.getElementById("output").innerHTML += "<p style='color: red;'>> ERROR: " + evt.data + "</p>"; };
+ }
+
+ function sendMessage(message) {
+ document.getElementById("output").innerHTML += "<p>> SEND: " + message + "</p>";
+
+ websocket.send(message);
+ }
+
+ window.addEventListener("load", init, false);
+ </script>
+ </head>
+ <body>
+ <h2>Welcome %1</h2><small>Refresh browser to clear page and resubscribe</small><br/><br/>
+ <input onkeypress="if(this.value) {if (window.event.keyCode == 13) { sendMessage(this.value); this.value = null; }}"/>
+ <div id="output"></div>
+ </body>
+</html> \ 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 00000000..6ca855a5
--- /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<String, FrameChannel> sockets;
+ private final Set<String> users;
+
+ public WebSocketChatRoom() {
+ this.listener = new WebSocketChatRoomListener(this);
+ this.sockets = new ConcurrentHashMap<String, FrameChannel>();
+ this.users = new CopyOnWriteArraySet<String>();
+ }
+
+ 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 00000000..e976b780
--- /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<String, String> cache;
+ private final Pattern pattern;
+
+ public CertificateUserExtractor(String pattern) {
+ this.cache = new ConcurrentHashMap<String, String>();
+ 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 00000000..5e6f81e4
--- /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 00000000..21c4abcb
--- /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 00000000..95bc6ef1
--- /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<String, Service> services = new HashMap<String, Service>();
+
+ 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 00000000..fbabeeb8
--- /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<FrameChannel> sockets;
+ private final MessageCounter listener;
+ private final AtomicInteger counter;
+ private final AtomicBoolean begin;
+
+ public MessageGeneratorService() {
+ this.sockets = new CopyOnWriteArraySet<FrameChannel>();
+ 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<TraceRecord> queue;
+ private final AtomicLong count;
+ private final String filter;
+
+ public ConsoleAnalyzer() {
+ this(null);
+ }
+
+ public ConsoleAnalyzer(String filter) {
+ this.queue = new ConcurrentLinkedQueue<TraceRecord>();
+ 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 00000000..d6cd4011
--- /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<WebSocketTableRow> 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<WebSocketTableRow>();
+ 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<String, String> map = new HashMap<String, String>();
+ 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<String, String> data) {
+ WebSocketTableRow row = rows.get(index);
+ if(row != null) {
+ Set<String> 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<String, String> map = new HashMap<String, String>();
+ 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<String, String> data) {
+ Set<String> 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 00000000..24dfbecd
--- /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 00000000..5a77537d
--- /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<String, Integer> currentRows;
+ private final WebSocketValueEncoder encoder;
+ private final WebSocketTable table;
+
+ public WebSocketTableChanger(WebSocketTable table) {
+ this.currentRows = new ConcurrentHashMap<String, Integer>();
+ this.encoder = new WebSocketValueEncoder();
+ this.table = table;
+ }
+
+ public void onChange(Map<String, Object> values) {
+ Map<String, String> row = new HashMap<String, String>();
+ Map<String, String> header = new HashMap<String, String>();
+ Set<String> 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 00000000..966d92ff
--- /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 00000000..f514295f
--- /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 00000000..d099f27a
--- /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 00000000..bb8a6db5
--- /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<String, WebSocketTableCell> cells;
+ private final WebSocketValueEncoder encoder;
+ private final WebSocketTableSchema schema;
+ private final int index;
+
+ public WebSocketTableRow(WebSocketTableSchema schema, int index) {
+ this.cells = new ConcurrentHashMap<String, WebSocketTableCell>();
+ 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<String> 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<String> 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 00000000..0ef307ef
--- /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<String, String> pairs = new LinkedHashMap<String, String>();
+ Map<String, String> values = new LinkedHashMap<String, String>();
+
+ 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<String> 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 00000000..bdc6755d
--- /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<String> products = new ArrayList<String>();
+
+ 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<String, Object> map = new HashMap<String, Object>();
+
+ 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 00000000..c8f4a684
--- /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<String, WebSocketTableColumnStyle> columns;
+
+ public WebSocketTableSchema(Map<String, WebSocketTableColumnStyle> columns) {
+ this.columns = columns;
+ }
+
+ public List<String> columnNames(){
+ return new ArrayList<String>(columns.keySet());
+ }
+
+ public boolean validColumn(String name) {
+ return columns.containsKey(name);
+ }
+
+ public String createStyle() {
+ StringBuilder builder = new StringBuilder();
+ Set<String> 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 00000000..b8402eb6
--- /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<Integer> 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<Integer>();
+ this.socket = socket;
+ }
+
+ public Set<Integer> 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 00000000..5f09efa2
--- /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<WebSocketTableUpdateType, String> sweep(long time, long count) {
+ Map<WebSocketTableUpdateType, String> messages = new LinkedHashMap<WebSocketTableUpdateType, String>();
+
+ 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 00000000..d7780e76
--- /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 00000000..0c4ec4de
--- /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<WebSocketTableSubscription> 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<WebSocketTableSubscription>();
+ 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<WebSocketTableUpdateType, String> messages = sweeper.sweep(time - 1000, count);
+ Set<WebSocketTableUpdateType> 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<String, WebSocketTableColumnStyle> columns = new LinkedHashMap<String, WebSocketTableColumnStyle>();
+
+ WebSocketTableSchema schema = new WebSocketTableSchema(columns);
+ columns.put("id", new WebSocketTableColumnStyle("id", "Id", "{id}", true, true));
+ columns.put("bidOutrightVolume", new WebSocketTableColumnStyle("bidOutrightVolume", "$ B", "<div style='font-weight: bold; color: #0000ff; text-decoration: underline;'>{bidOutrightVolume}</a>", true, false));
+ columns.put("bidOutright", new WebSocketTableColumnStyle("bidOutright", "Bid", "<div style='font-weight: bold; color: #0000ff; text-decoration: underline;'>{bidOutright}</a>", true, false));
+ columns.put("offerOutright", new WebSocketTableColumnStyle("offerOutright", "Offer", "<div style='font-weight: bold; color: #ff0000; text-decoration: underline;'>{offerOutright}</a>", true, false));
+ columns.put("offerOutrightVolume", new WebSocketTableColumnStyle("offerOutrightVolume", "$ O", "<div style='font-weight: bold; color: #ff0000; text-decoration: underline;'>{offerOutrightVolume}</a>", true, false));
+ columns.put("product", new WebSocketTableColumnStyle("product", "Security", "<div style='font-weight: bold;'>{product}</div>", true, true));
+ columns.put("bidEFPVolume", new WebSocketTableColumnStyle("bidEFPVolume", "$ B", "<div style='font-weight: bold; color: #0000ff; text-decoration: underline;'>{bidEFPVolume}</a>", true, false));
+ columns.put("bidEFP", new WebSocketTableColumnStyle("bidEFP", "Bid", "<div style='font-weight: bold; color: #0000ff; text-decoration: underline;'>{bidEFP}</a>", true, false));
+ columns.put("offerEFP", new WebSocketTableColumnStyle("offerEFP", "Offer", "<div style='font-weight: bold; color: #ff0000; text-decoration: underline;'>{offerEFP}</a>", true, false));
+ columns.put("offerEFPVolume", new WebSocketTableColumnStyle("offerEFPVolume", "$ O", "<div style='font-weight: bold; color: #ff0000; text-decoration: underline;'>{offerEFPVolume}</a>", 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 00000000..586c82f4
--- /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 00000000..7afa93b3
--- /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 00000000..20bf9619
--- /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 00000000..0e33fb16
--- /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<r.length-1&&a++,~a||(a=0),r.eq(a).focus()}},e.fn.dropdown=function(t){return this.each(function(){var r=e(this),i=r.data("dropdown");i||r.data("dropdown",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.dropdown.Constructor=n,e(function(){e("html").on("click.dropdown.data-api touchstart.dropdown.data-api",r),e("body").on("click.dropdown touchstart.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("click.dropdown.data-api touchstart.dropdown.data-api",t,n.prototype.toggle).on("keydown.dropdown.data-api touchstart.dropdown.data-api",t+", [role=menu]",n.prototype.keydown)})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=n,this.$element=e(t).delegate('[data-dismiss="modal"]',"click.dismiss.modal",e.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};t.prototype={constructor:t,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,n=e.Event("show");this.$element.trigger(n);if(this.isShown||n.isDefaultPrevented())return;e("body").addClass("modal-open"),this.isShown=!0,this.escape(),this.backdrop(function(){var n=e.support.transition&&t.$element.hasClass("fade");t.$element.parent().length||t.$element.appendTo(document.body),t.$element.show(),n&&t.$element[0].offsetWidth,t.$element.addClass("in").attr("aria-hidden",!1).focus(),t.enforceFocus(),n?t.$element.one(e.support.transition.end,function(){t.$element.trigger("shown")}):t.$element.trigger("shown")})},hide:function(t){t&&t.preventDefault();var n=this;t=e.Event("hide"),this.$element.trigger(t);if(!this.isShown||t.isDefaultPrevented())return;this.isShown=!1,e("body").removeClass("modal-open"),this.escape(),e(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),e.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var t=this;e(document).on("focusin.modal",function(e){t.$element[0]!==e.target&&!t.$element.has(e.target).length&&t.$element.focus()})},escape:function(){var e=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(t){t.which==27&&e.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var t=this,n=setTimeout(function(){t.$element.off(e.support.transition.end),t.hideModal()},500);this.$element.one(e.support.transition.end,function(){clearTimeout(n),t.hideModal()})},hideModal:function(e){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var n=this,r=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var i=e.support.transition&&r;this.$backdrop=e('<div class="modal-backdrop '+r+'" />').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:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',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:'<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'})}(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<this.options.minLength?this.shown?this.hide():this:(n=e.isFunction(this.source)?this.source(this.query,e.proxy(this.process,this)):this.source,n?this.process(n):this)},process:function(t){var n=this;return t=e.grep(t,function(e){return n.matcher(e)}),t=this.sorter(t),t.length?this.render(t.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(e){return~e.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(e){var t=[],n=[],r=[],i;while(i=e.shift())i.toLowerCase().indexOf(this.query.toLowerCase())?~i.indexOf(this.query)?n.push(i):r.push(i):t.push(i);return t.concat(n,r)},highlighter:function(e){var t=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return e.replace(new RegExp("("+t+")","ig"),function(e,t){return"<strong>"+t+"</strong>"})},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:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',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 00000000..ee1178ad
--- /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 = '<img src="';
+
+ image += status;
+ image += '"';
+ image += 'style="';
+ image += ' max-width: 100%;';
+ image += ' max-height: 100%;';
+ image += ' padding-top: 4px;';
+ image += ' padding-bottom: 4px;';
+ image += ' padding-left: 4px;';
+ image += ' padding-right: 8px;';
+ image += '"/>';
+
+ document.getElementById("connection").innerHTML = image;
+ document.getElementById("rows").innerHTML = height;
+ document.getElementById("changes").innerHTML = change;
+ document.getElementById("duration").innerHTML = duration;
+ socket.send("status:rows="+height+",change="+change+",duration="+duration+",sequence="+sequence+",method="+method);
+
+}
+
+function schemaUpdate(socket, table, message) {
+ var cells = message.split('|');
+ var minimum = cells.length;
+ var width = schema.length;
+
+ for ( var i = 0; i < cells.length; i++) {
+ var values = cells[i].split(',');
+ var name = values[0];
+ var caption = decodeValue(values[1]);
+ var template = decodeValue(values[2]);
+ var resizable = values[3];
+ var sortable = values[4];
+ var style = {};
+
+ style['name'] = name;
+ style['caption'] = caption;
+ style['template'] = template;
+ style['resizable'] = resizable;
+ style['sortable'] = sortable;
+
+ schema[i] = style;
+ }
+ if(width < minimum) {
+ expandWidth(table);
+ requestRefresh(socket, 'schemaUpdate');
+ }
+}
+
+function requestRefresh(socket, message) {
+ socket.send('refresh:everything=true,message='+message);
+}
+
+function deltaUpdate(socket, table, message, method) {
+ var header = message.indexOf(':');
+ var sequence = 0;
+
+ if(header > 0) {
+ sequence = message.substring(0, header);
+ message = message.substring(header + 1);
+ }
+ var rows = message.split('|');
+ var length = message.length;
+ var start = currentTime();
+
+ if(schema.length > 0) {
+ method(socket, table, rows);
+ }
+ var finish = currentTime();
+ var duration = finish - start;
+ var height = table.total;
+ var change = rows.length;
+ var operation = method.name;
+
+ reportStatus(socket, "success.png", height, length, change, duration, sequence, operation);
+}
+
+function currentTime() {
+ var date = new Date()
+ return date.getTime();
+}
+
+function findRow(table, row) {
+ var record = table.find({ recid: row });
+ var height = table.total;
+ var index = 0;
+
+ if(record.length > 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 00000000..e560b088
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/failure.png
Binary files 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 00000000..417f2c9a
--- /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 00000000..69b546a0
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/grid.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bond Semi Central</title>
+ <link href="font-awesome.min.css" rel="stylesheet">
+ <link href="bootstrap.css" rel="stylesheet">
+ <script src="jquery-2.1.1.min.js"></script>
+ <script src="bootstrap.min.js"></script>
+ <link rel="stylesheet" type="text/css" href="w2ui-1.4.min.css" />
+ <script type="text/javascript" src="w2ui-1.4.js"></script>
+ <script type="text/javascript" src="delta.js"></script>
+</head>
+<style>
+.update {
+ background-color: #5cacee;
+}
+.highlight {
+ background-color: #00ff00;
+}
+#grid_mainGrid_body > .w2ui-grid-records table tr.w2ui-even {
+ background-color: #ffddf1;
+}
+#grid_mainGrid_body > .w2ui-grid-records table tr.w2ui-odd {
+ background-color: #ffc0cb;
+}
+#grid_litEFPGrid_body > .w2ui-grid-records table tr.w2ui-even {
+ background-color: #0000ee;
+}
+#grid_litEFPGrid_body > .w2ui-grid-records table tr.w2ui-odd {
+ background-color: #0000cd;
+}
+#grid_litSwitchGrid_body > .w2ui-grid-records table tr.w2ui-even {
+ background-color: #0000ee;
+}
+#grid_litSwitchGrid_body > .w2ui-grid-records table tr.w2ui-odd {
+ background-color: #0000cd;
+}
+</style>
+<body style="height: 100%; margin: 0; background-color: #ff0000;">
+<div id="mainLayout" style="position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px;"></div>
+<script>
+$(function () {
+
+ // -- LAYOUT
+
+ var pstyle = 'background-color: #F5F6F7; overflow: hidden;';
+ $('#mainLayout').w2layout({
+ name: 'mainLayout',
+ padding: 0,
+ panels: [
+ { type: 'left', size: '40%', style: pstyle, resizable: true },
+ { type: 'right', size: '60%', style: pstyle, resizable: true },
+ { type: 'bottom', size: '25px', style: pstyle, resizable: true }
+ ]
+ });
+
+ var pstyle = 'background-color: #F5F6F7; overflow: hidden;';
+ $('#blueLayout').w2layout({
+ name: 'blueLayout',
+ padding: 0,
+ panels: [
+ { type: 'left', size: '50%', style: pstyle, resizable: true },
+ { type: 'right', size: '50%', style: pstyle, resizable: true },
+ { type: 'bottom', size: '30%', style: pstyle, resizable: true,
+ tabs: {
+ active: 'tab1',
+ tabs: [
+ { id: 'tab1', caption: 'Market Monitor' },
+ { id: 'tab2', caption: 'Blotter' }
+ ],
+ onClick: function (event) {
+ this.owner.content('main', event);
+ }
+ }
+ }
+ ]
+ });
+
+ $().w2grid({
+ name: 'mainGrid'
+ });
+
+ $().w2grid({
+ name: 'litEFPGrid'
+ });
+
+ $().w2grid({
+ name: 'litSwitchGrid'
+ });
+
+ w2ui['mainLayout'].content('left', w2ui['mainGrid']);
+ w2ui['mainLayout'].content('right', w2ui['blueLayout']);
+
+ w2ui['blueLayout'].content('left', w2ui['litEFPGrid']);
+ w2ui['blueLayout'].content('right', w2ui['litSwitchGrid']);
+
+ w2ui['mainLayout'].content('bottom', '<div style="background-color: #eee; padding: 2px 2px; font-family: Verdana,Arial,sans-serif; font-size: 11px;"> <span id="connection"><img style="max-width: 100%; max-height: 100%; padding-top: 4px; padding-bottom: 4px;" src="failure.png"/></span> Rows - <span id="rows"></span> Changes - <span id="changes"></span> Duration - <span id="duration"></span></div>');
+
+});
+</script>
+</body>
+</html>
+
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 00000000..53770604
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/index.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bond Semi Central</title>
+ <link href="font-awesome.min.css" rel="stylesheet">
+ <link href="bootstrap.css" rel="stylesheet">
+ <script src="jquery-2.1.1.min.js"></script>
+ <script src="bootstrap.min.js"></script>
+ <link rel="stylesheet" type="text/css" href="w2ui-1.4.min.css" />
+ <script type="text/javascript" src="w2ui-1.4.min.js"></script>
+</head>
+<body>
+<div id="layout" style="width: 100%; height: 400px;"></div>
+<div id="grid" style="width: 100%; height: 350px;"></div>
+<script type="text/javascript">
+$(function () {
+ var pstyle = 'border: 1px solid #dfdfdf; padding: 5px;';
+ $('#layout').w2layout({
+ name: 'layout',
+ panels: [
+ { type: 'left', size: 200, resizable: true, style: pstyle, content: 'left' },
+ { type: 'main', style: pstyle, content: 'main' },
+ { type: 'right', size: 200, resizable: true, style: pstyle, content: 'right' }
+ ]
+ });
+});
+$(function () {
+ $('#grid').w2grid({
+ name: 'grid',
+ url: 'data/list2.json',
+ columns: [
+ { field: 'fname', caption: 'First Name', size: '30%' },
+ { field: 'lname', caption: 'Last Name', size: '70%' },
+ { field: 'sdate', caption: 'Dates', size: '120px', attr: "align=center" },
+ ]
+ });
+});
+</script>
+</body>
+</html>
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 00000000..9ed2acc6
--- /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="<div class='a'></div><div class='a i'></div>",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="<select msallowclip=''><option selected=''></option></select>",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;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;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 href='#'></a>","#"===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="<input/>",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.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",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<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/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*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_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></$2>")+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></$2>");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("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec,fc,gc=/#.*$/,hc=/([?&])_=[^&]*/,ic=/^(.*?):[ \t]*([^\r\n]*)$/gm,jc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,kc=/^(?:GET|HEAD)$/,lc=/^\/\//,mc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,nc={},oc={},pc="*/".concat("*");try{fc=location.href}catch(qc){fc=l.createElement("a"),fc.href="",fc=fc.href}ec=mc.exec(fc.toLowerCase())||[];function rc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function sc(a,b,c,d){var e={},f=a===oc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function tc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function uc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function vc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:fc,type:"GET",isLocal:jc.test(ec[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":pc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?tc(tc(a,n.ajaxSettings),b):tc(n.ajaxSettings,a)},ajaxPrefilter:rc(nc),ajaxTransport:rc(oc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=ic.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||fc)+"").replace(gc,"").replace(lc,ec[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=mc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===ec[1]&&h[2]===ec[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(ec[3]||("http:"===ec[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),sc(nc,k,b,v),2===t)return v;i=k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!kc.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=hc.test(d)?d.replace(hc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+pc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=sc(oc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=uc(k,v,f)),u=vc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var wc=/%20/g,xc=/\[\]$/,yc=/\r?\n/g,zc=/^(?:submit|button|image|reset|file)$/i,Ac=/^(?:input|select|textarea|keygen)/i;function Bc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||xc.test(a)?d(a,e):Bc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Bc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Bc(c,a[c],b,e);return d.join("&").replace(wc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&Ac.test(this.nodeName)&&!zc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(yc,"\r\n")}}):{name:b.name,value:c.replace(yc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Cc=0,Dc={},Ec={0:200,1223:204},Fc=n.ajaxSettings.xhr();a.ActiveXObject&&n(a).on("unload",function(){for(var a in Dc)Dc[a]()}),k.cors=!!Fc&&"withCredentials"in Fc,k.ajax=Fc=!!Fc,n.ajaxTransport(function(a){var b;return k.cors||Fc&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Cc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Dc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Ec[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Dc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Gc=[],Hc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Gc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Hc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Hc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Hc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Gc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Ic=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Ic)return Ic.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Jc=a.document.documentElement;function Kc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Kc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Jc;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Jc})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Kc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Lc=a.jQuery,Mc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Mc),b&&a.jQuery===n&&(a.jQuery=Lc),n},typeof b===U&&(a.jQuery=a.$=n),n}); \ No newline at end of file
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/login.html b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/login.html
new file mode 100644
index 00000000..47114949
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/login.html
@@ -0,0 +1,12 @@
+ <html>
+ <head>
+ <title>Login Page</title>
+ </head>
+ <body>
+ <h1>Please Login</h1>
+ <form action='/login' method='POST'>
+ <input type='text' name='user'/>
+ <input type='submit' value='Sign In'/>
+ </form>
+ </body>
+</html> \ No newline at end of file
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/main.html b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/main.html
new file mode 100644
index 00000000..3b898f7f
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/main.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bond Semi Central</title>
+ <link href="font-awesome.min.css" rel="stylesheet">
+ <link href="bootstrap.css" rel="stylesheet">
+ <script src="jquery-2.1.1.min.js"></script>
+ <script src="bootstrap.min.js"></script>
+ <link rel="stylesheet" type="text/css" href="w2ui-1.4.min.css" />
+ <script type="text/javascript" src="w2ui-1.4.min.js"></script>
+</head>
+<body>
+<div id="layout" style="width: 100%; height: 400px;"></div>
+<div id="grid" style="width: 100%; height: 350px;"></div>
+<script>
+$(function () {
+
+ // -- LAYOUT
+
+ var pstyle = 'background-color: #F5F6F7; overflow: hidden';
+ $('#layout').w2layout({
+ name: 'layout',
+ padding: 0,
+ panels: [
+ { type: 'left', size: '40%', style: pstyle, resizable: true },
+ { type: 'right', size: '60%', style: pstyle, resizable: true }
+ ]
+ });
+
+ var pstyle = 'background-color: #F5F6F7; overflow: hidden';
+ $('#layout2').w2layout({
+ name: 'layout2',
+ padding: 0,
+ panels: [
+ { type: 'left', size: '50%', style: pstyle, resizable: true },
+ { type: 'right', size: '50%', style: pstyle, resizable: true },
+ { type: 'bottom', size: '30%', style: pstyle, resizable: true }
+ ]
+ });
+
+
+ // -- GRID
+
+ $().w2grid({
+ name: 'pinkGrid',
+ sortData: [ { field: 'recid', direction: 'asc' } ],
+ columns: [
+ { field: 'recid', caption: 'ID', size: '50px', sortable: true, resizable: true },
+ { field: 'lname', caption: 'Last Name', size: '30%', sortable: true, resizable: true },
+ { field: 'fname', caption: 'First Name', size: '30%', sortable: true, resizable: true },
+ { field: 'email', caption: 'Email', size: '40%', resizable: true, sortable: true },
+ { field: 'sdate', caption: 'Start Date', size: '120px', resizable: true },
+ { field: 'sdate', caption: 'End Date', size: '120px', resizable: true },
+ { field: 'sdate', caption: 'Release Date', size: '120px', resizable: true },
+ ],
+ records: [
+ { recid: 1, fname: 'John', lname: 'doe', email: 'vitali@gmail.com', sdate: '1/3/2012' },
+ { recid: 2, fname: 'Stuart', lname: 'Motzart', email: 'jdoe@gmail.com', sdate: '2/4/2012' },
+ { recid: 3, fname: 'Jin', lname: 'Franson', email: 'jdoe@gmail.com', sdate: '4/23/2012' },
+ { recid: 4, fname: 'Susan', lname: 'Ottie', email: 'jdoe@gmail.com', sdate: '5/3/2012' },
+ { recid: 5, fname: 'Kelly', lname: 'Silver', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 6, fname: 'Francis', lname: 'Gatos', email: 'vitali@gmail.com', sdate: '2/5/2012' },
+ { recid: 7, fname: 'Mark', lname: 'Welldo', email: 'jdoe@gmail.com', sdate: '8/25/2012' },
+ { recid: 8, fname: 'Thomas', lname: 'Bahh', email: 'jdoe@gmail.com', sdate: '9/3/2012' },
+ { recid: 9, fname: 'Sergei', lname: 'Rachmaninov', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 20, fname: 'Jill', lname: 'Doe', email: 'jdoe@gmail.com', sdate: '10/3/2012' },
+ { recid: 21, fname: 'Frank', lname: 'Motzart', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 22, fname: 'Peter', lname: 'Franson', email: 'vitali@gmail.com', sdate: '4/3/2012' },
+ { recid: 23, fname: 'Andrew', lname: 'Ottie', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 24, fname: 'Manny', lname: 'Silver', email: 'jim@gmail.com', sdate: '12/3/2012' },
+ { recid: 25, fname: 'Ben', lname: 'Gatos', email: 'jim@gmail.com', sdate: '4/3/2012' },
+ { recid: 26, fname: 'Doer', lname: 'Welldo', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 27, fname: 'Shashi', lname: 'bahh', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 28, fname: 'Av', lname: 'Rachmaninov', email: 'jdoe@gmail.com', sdate: '4/3/2012' }
+ ],
+ onClick: function (event) {
+ var rec = this.get(event.recid);
+ w2ui['form'].record = rec;
+ w2ui['form'].refresh();
+ event.onComplete = function () {
+ if (this.getSelection().length == 0) $('#delete').attr('disabled', true); else $('#delete').removeAttr('disabled');
+ }
+ }
+ });
+
+ $().w2grid({
+ name: 'blueOutrightGrid',
+ sortData: [ { field: 'recid', direction: 'asc' } ],
+ columns: [
+ { field: 'recid', caption: 'ID', size: '50px', sortable: true, resizable: true },
+ { field: 'lname', caption: 'Last Name', size: '30%', sortable: true, resizable: true },
+ { field: 'fname', caption: 'First Name', size: '30%', sortable: true, resizable: true },
+ { field: 'email', caption: 'Email', size: '40%', resizable: true, sortable: true },
+ { field: 'sdate', caption: 'Start Date', size: '120px', resizable: true },
+ { field: 'sdate', caption: 'End Date', size: '120px', resizable: true },
+ { field: 'sdate', caption: 'Release Date', size: '120px', resizable: true },
+ ],
+ records: [
+ { recid: 1, fname: 'John', lname: 'doe', email: 'vitali@gmail.com', sdate: '1/3/2012' },
+ { recid: 2, fname: 'Stuart', lname: 'Motzart', email: 'jdoe@gmail.com', sdate: '2/4/2012' },
+ { recid: 3, fname: 'Jin', lname: 'Franson', email: 'jdoe@gmail.com', sdate: '4/23/2012' },
+ { recid: 4, fname: 'Susan', lname: 'Ottie', email: 'jdoe@gmail.com', sdate: '5/3/2012' },
+ { recid: 5, fname: 'Kelly', lname: 'Silver', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 6, fname: 'Francis', lname: 'Gatos', email: 'vitali@gmail.com', sdate: '2/5/2012' },
+ { recid: 7, fname: 'Mark', lname: 'Welldo', email: 'jdoe@gmail.com', sdate: '8/25/2012' },
+ { recid: 8, fname: 'Thomas', lname: 'Bahh', email: 'jdoe@gmail.com', sdate: '9/3/2012' },
+ { recid: 9, fname: 'Sergei', lname: 'Rachmaninov', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 20, fname: 'Jill', lname: 'Doe', email: 'jdoe@gmail.com', sdate: '10/3/2012' },
+ { recid: 21, fname: 'Frank', lname: 'Motzart', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 22, fname: 'Peter', lname: 'Franson', email: 'vitali@gmail.com', sdate: '4/3/2012' },
+ { recid: 23, fname: 'Andrew', lname: 'Ottie', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 24, fname: 'Manny', lname: 'Silver', email: 'jim@gmail.com', sdate: '12/3/2012' },
+ { recid: 25, fname: 'Ben', lname: 'Gatos', email: 'jim@gmail.com', sdate: '4/3/2012' },
+ { recid: 26, fname: 'Doer', lname: 'Welldo', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 27, fname: 'Shashi', lname: 'bahh', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 28, fname: 'Av', lname: 'Rachmaninov', email: 'jdoe@gmail.com', sdate: '4/3/2012' }
+ ],
+ onClick: function (event) {
+ var rec = this.get(event.recid);
+ w2ui['form'].record = rec;
+ w2ui['form'].refresh();
+ event.onComplete = function () {
+ if (this.getSelection().length == 0) $('#delete').attr('disabled', true); else $('#delete').removeAttr('disabled');
+ }
+ }
+ });
+
+ $().w2grid({
+ name: 'blueSwitchGrid',
+ sortData: [ { field: 'recid', direction: 'asc' } ],
+ columns: [
+ { field: 'recid', caption: 'ID', size: '50px', sortable: true, resizable: true },
+ { field: 'lname', caption: 'Last Name', size: '30%', sortable: true, resizable: true },
+ { field: 'fname', caption: 'First Name', size: '30%', sortable: true, resizable: true },
+ { field: 'email', caption: 'Email', size: '40%', resizable: true, sortable: true },
+ { field: 'sdate', caption: 'Start Date', size: '120px', resizable: true },
+ { field: 'sdate', caption: 'End Date', size: '120px', resizable: true },
+ { field: 'sdate', caption: 'Release Date', size: '120px', resizable: true },
+ ],
+ records: [
+ { recid: 1, fname: 'John', lname: 'doe', email: 'vitali@gmail.com', sdate: '1/3/2012' },
+ { recid: 2, fname: 'Stuart', lname: 'Motzart', email: 'jdoe@gmail.com', sdate: '2/4/2012' },
+ { recid: 3, fname: 'Jin', lname: 'Franson', email: 'jdoe@gmail.com', sdate: '4/23/2012' },
+ { recid: 4, fname: 'Susan', lname: 'Ottie', email: 'jdoe@gmail.com', sdate: '5/3/2012' },
+ { recid: 5, fname: 'Kelly', lname: 'Silver', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 6, fname: 'Francis', lname: 'Gatos', email: 'vitali@gmail.com', sdate: '2/5/2012' },
+ { recid: 7, fname: 'Mark', lname: 'Welldo', email: 'jdoe@gmail.com', sdate: '8/25/2012' },
+ { recid: 8, fname: 'Thomas', lname: 'Bahh', email: 'jdoe@gmail.com', sdate: '9/3/2012' },
+ { recid: 9, fname: 'Sergei', lname: 'Rachmaninov', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 20, fname: 'Jill', lname: 'Doe', email: 'jdoe@gmail.com', sdate: '10/3/2012' },
+ { recid: 21, fname: 'Frank', lname: 'Motzart', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 22, fname: 'Peter', lname: 'Franson', email: 'vitali@gmail.com', sdate: '4/3/2012' },
+ { recid: 23, fname: 'Andrew', lname: 'Ottie', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 24, fname: 'Manny', lname: 'Silver', email: 'jim@gmail.com', sdate: '12/3/2012' },
+ { recid: 25, fname: 'Ben', lname: 'Gatos', email: 'jim@gmail.com', sdate: '4/3/2012' },
+ { recid: 26, fname: 'Doer', lname: 'Welldo', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 27, fname: 'Shashi', lname: 'bahh', email: 'jdoe@gmail.com', sdate: '4/3/2012' },
+ { recid: 28, fname: 'Av', lname: 'Rachmaninov', email: 'jdoe@gmail.com', sdate: '4/3/2012' }
+ ],
+ onClick: function (event) {
+ var rec = this.get(event.recid);
+ w2ui['form'].record = rec;
+ w2ui['form'].refresh();
+ event.onComplete = function () {
+ if (this.getSelection().length == 0) $('#delete').attr('disabled', true); else $('#delete').removeAttr('disabled');
+ }
+ }
+ });
+
+ w2ui['layout'].content('left', w2ui['pinkGrid']);
+ w2ui['layout'].content('right', w2ui['layout2']);
+ w2ui['layout2'].content('left', w2ui['blueOutrightGrid']);
+ w2ui['layout2'].content('right', w2ui['blueSwitchGrid']);
+ w2ui['pinkGrid'].get(1).getCell(1).style = 'bgcolor: #ff0000';
+});
+</script>
+</head>
+<body>
+ <div id="layout" style="width: 100%; height: 500px;"></div>
+</body>
+</html>
+
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/pending.png b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/pending.png
new file mode 100644
index 00000000..da9ebb84
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/pending.png
Binary files differ
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/success.png b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/success.png
new file mode 100644
index 00000000..b46c7f24
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/success.png
Binary files differ
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/table.html b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/table.html
new file mode 100644
index 00000000..97fc5284
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/table.html
@@ -0,0 +1,347 @@
+<html>
+ <head>
+ <style type="text/css">
+ .page {
+ background-color: #ebebeb;
+ }
+ .grid {
+ table-layout:fixed;
+ font-family: verdana,arial,sans-serif;
+ font-size:11px;
+ color:#333333;
+ border-width: 1px;
+ border-color: #666666;
+ border-collapse: collapse;
+ }
+ .header {
+ border-width: 1px;
+ padding: 8px;
+ border-style: solid;
+ border-color: #666666;
+ background-color: #dedede;
+ white-space: nowrap;
+ word-break: keep-all;
+ font-weight: bold;
+ }
+ .normal {
+ border-width: 1px;
+ padding: 8px;
+ border-style: solid;
+ border-color: #666666;
+ background-color: #ffffff;
+ white-space: nowrap;
+ word-break: keep-all;
+ font-weight: normal;
+ }
+ .highlight {
+ border-width: 1px;
+ padding: 8px;
+ border-style: solid;
+ border-color: #666666;
+ background-color: #efefef;
+ white-space: nowrap;
+ word-break: keep-all;
+ font-weight: normal;
+ }
+ .update {
+ border-width: 1px;
+ padding: 8px;
+ border-style: solid;
+ border-color: #666666;
+ background-color: #5cacee;
+ white-space: nowrap;
+ word-break: keep-all;
+ font-weight: normal;
+ }
+ .input {
+ border: 1px solid #666666;
+ padding: 5px;
+ font-family: verdana,arial,sans-serif;
+ font-size:11px;
+ }
+ .label {
+ font-family: verdana,arial,sans-serif;
+ font-size:14px;
+ font-weight: normal;
+ }
+ .debug {
+ width: 800px;
+ height: 120px;
+ border: 1px solid #666666;
+ padding: 5px;
+ font-family: verdana,arial,sans-serif;
+ font-size:11px;
+ }
+ </style>
+ <title>Table Subscription</title>
+ <script>
+ var translate = new Array();
+ var dirty = new Array();
+ var connections = 0;
+ var attempts = 0;
+ var total = 1;
+
+ function connect() {
+ socket = new WebSocket("ws://localhost:9090/update");
+
+ socket.onopen = function () {
+ attempts = 1;
+ connections++;
+ reportStatus("CONNECTED", "0", "0", "0", "0");
+ };
+
+ socket.onerror = function (message) {
+ reportStatus("ERROR", "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("CLOSED", "0", "0", "0", "0");
+ };
+
+ socket.onmessage = function (message) {
+ var table = document.getElementById("grid");
+ var data = message.data.substring(1);
+
+ if (message.data.charAt(0) == 'D') {
+ deltaUpdate(table, data);
+ } else if (message.data.charAt(0) == 'S') {
+ schemaUpdate(table, data);
+ }
+ };
+ }
+
+ function reportDebug(tag, text) {
+ document.getElementById("debug").value = text;
+ }
+
+ function reportStatus(status, height, delta, change, duration) {
+ document.getElementById("connection").innerHTML = status;
+ document.getElementById("rows").innerHTML = height;
+ document.getElementById("delta").innerHTML = delta;
+ document.getElementById("changes").innerHTML = change;
+ document.getElementById("duration").innerHTML = duration;
+ }
+
+ function schemaUpdate(table, message) {
+ var cells = message.split(',');
+ var height = table.rows.length;
+
+ if (height < 1) {
+ expandHeight(table, 1);
+ }
+ var width = table.rows[0].cells.length;
+ var minimum = cells.length;
+
+ if (width <= minimum) {
+ expandWidth(table, minimum);
+ }
+ for (var i = 0; i < cells.length; i++) {
+ table.rows[0].cells[i].innerHTML = cells[i];
+ table.rows[0].cells[i].className = 'header'
+ }
+ }
+
+ function deltaUpdate(table, message) {
+ var rows = message.split('|');
+ var length = message.length;
+ var start = currentTime();
+
+ clearHighlight(table);
+ updateTable(table, rows);
+
+ var finish = currentTime();
+ var duration = finish - start;
+ var height = table.rows.length;
+ var change = rows.length;
+
+ reportDebug("Delta", message);
+ reportStatus("CONNECTED", height, length, change, duration);
+ }
+
+ function currentTime() {
+ var date = new Date()
+ return date.getTime();
+ }
+
+ function translateRow(row) {
+ if (translate[row] === undefined) {
+ translate[row] = total++;
+ }
+ return translate[row];
+ }
+
+ function updateTable(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 = translateRow(index);
+
+ if (pair != null && pair.length > 1) {
+ var cells = pair[1].split(',');
+
+ if (cells.length > 0) {
+ updateRow(table, index, cells);
+ }
+ }
+ }
+ }
+ }
+
+ function updateRow(table, row, cells) {
+ var height = table.rows.length;
+
+ if (height <= row) {
+ expandHeight(table, row);
+ }
+ var width = table.rows[row].cells.length;
+ var minimum = cells.length;
+
+ if (width <= minimum) {
+ expandWidth(table, minimum);
+ }
+ dirty[row] = 'dirty';
+
+ for (var i = 0; i < cells.length; i++) {
+ var cell = cells[i].split('=');
+ var column = cell[0];
+ var value = cell[1];
+ var decoded = decodeValue(value);
+
+ if (table.rows[row].cells.length < column) {
+ expandWidth(table, column);
+ }
+ table.rows[row].cells[column].innerHTML = decoded;
+
+ if (row > 0) {
+ table.rows[row].cells[column].className = 'update';
+ }
+ }
+ }
+
+ 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 clearHighlight(table) {
+ var height = table.rows.length;
+
+ for (var i = 0; i < height; i++) {
+ if(dirty[i] === undefined || dirty[i] == 'dirty') {
+ var width = table.rows[i].cells.length;
+
+ for (var j = 0; j < width; j++) {
+ if (i > 0) {
+ if (i % 2 == 0) {
+ table.rows[i].cells[j].className = 'highlight';
+ } else {
+ table.rows[i].cells[j].className = 'normal';
+ }
+ } else {
+ table.rows[i].cells[j].className = 'header';
+ }
+ }
+ dirty[i] = 'clean';
+ }
+ }
+ }
+
+ function expandHeight(table, row) {
+ var height = table.rows.length;
+
+ for (var i = height; i <= row; i++) {
+ table.insertRow(i);
+
+ if (i > 0) {
+ var width = table.rows[i - 1].cells.length;
+
+ for (var j = 0; j < width; j++) {
+ table.rows[i].insertCell(j);
+ }
+ }
+ }
+ }
+
+ function expandWidth(table, column) {
+ for (var i = 0; i < table.rows.length; i++) {
+ var width = table.rows[i].cells.length;
+
+ for (var j = width; j < column; j++) {
+ table.rows[i].insertCell(j);
+
+ if (i > 0) {
+ table.rows[i].cells[j].className = 'header';
+ }
+ }
+ }
+ }
+ window.addEventListener("load", connect, false);
+ </script>
+ </head>
+ <body class="page">
+ <form action='/table' method='POST'>
+ <table>
+ <tr>
+ <td class="label">Type</td>
+ </tr>
+ <tr>
+ <td><input class="input" size='100' type='text' name='type' value='${type}'/></td>
+ </tr>
+ <tr>
+ <td class="label">Predicate</td>
+ </tr>
+ <tr>
+ <td><input class="input" size='100' type='text' name='predicate' value='${predicate}'/><input type='submit' value='Change'/></td>
+ </tr>
+ <tr>
+ <td class="label">Delta</td>
+ </tr>
+ <tr>
+ <td><textarea id="debug" class="debug"></textarea></td>
+ </tr>
+ </table>
+ </form>
+ <table class="grid">
+ <tr>
+ <td class="header">connection</td>
+ <td class="header">rows</td>
+ <td class="header">delta</td>
+ <td class="header">changes</td>
+ <td class="header">duration</td>
+ </tr>
+ <tr>
+ <td class="normal" id="connection"></td>
+ <td class="normal" id="rows"></td>
+ <td class="normal" id="delta"></td>
+ <td class="normal" id="changes"></td>
+ <td class="normal" id="duration"></td>
+ </tr>
+ </table>
+ <br/>
+ <table id="grid" class="grid"/>
+ </body>
+</html>
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.css b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.css
new file mode 100644
index 00000000..4fbb4788
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.css
@@ -0,0 +1,2750 @@
+/* w2ui 1.4 (c) http://w2ui.com, vitmalina@gmail.com */
+@font-face {
+ font-family: "w2ui-font";
+ src: url("data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAWIAAoAAAAACAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAAA9AAAAEMAAABWQLxMsmNtYXAAAAE4AAAAOgAAAUriGRC2Z2x5ZgAAAXQAAAH9AAACgLu4vTRoZWFkAAADdAAAADAAAAA2AOYXBGhoZWEAAAOkAAAAIAAAACQD8wHHaG10eAAAA8QAAAAWAAAAIA7dAABsb2NhAAAD3AAAABIAAAASAngBuG1heHAAAAPwAAAAHwAAACABFQA2bmFtZQAABBAAAAEtAAACIsTQ/zJwb3N0AAAFQAAAAEgAAABi4/7ZEHicY2BkvMM4gYGVgYPRhTGNgYHBHUp/ZZBkaGFgYGJgZWbACgLSXFMYHD4yfmRnPPD/AIMe4wEGR6AwI0gOANHZC/IAeJxjYGBgZoBgGQZGBhBwAfIYwXwWBg0gzQakGRmYGBg+sv//D1LwkRFE8zNA1QMBIxvDiAcAddwGvgAAeJxFkLFv01AQxu97LrHdRImjpnaS1gnEia3IoqAktkkiEhaEOiCsDkEo8dyBDkytKpYKVWwssKKKAYmBREKMLJSFoRJ/AGJhY0NZGFgSzrUinvR+d++7T+/ePQLRci4IJ3SFCLIjG8CH+ZPwHHdwkkSS2PMXP3DGmUxpoo123lrt5nj8fjyejsc4W0zwNtl8FfHCKV5QnaOhF3IJUrUbkGPYnSGcGH6risBvGQhdVY0iVXXVkhJN1JL6/6xOIqWk4tRlJiVFiSJFSUrsZ+tkoqpEgt/6Hb/wjtZog2gonBx24AyFJUuNwMvha+/CvLi3vq1f785GsxGuTqfWc5O1N/r2+lNrOl38ZHnWpdUMr/CaLKLGZiHl4hI1+zasGB2/Dy9GSzfRbul4qaWPtHSQ0Y7SWpxmgnSc/mblMKNpmcOVEhfj+5d/8BGfqMl9bKuWFYWKaLf8YIAq9IKc5TY7ojNgTTcC7iEjaN4yPdswbM+81t8UimRLojq66YbdWq0bus375t21bwgcw/H6nmNUyhIkR/DsTasXPgx7VtV8oDx6XIxHE8vl8rMAzqlEDZ7QseUBPP6tLOQKDH5HqooKfLvB2gABa1lgflzI60VxsLd3IJj1YRn5/Wx9Syy++LvArn/JzH4e5WE98TCLer5wnBNb9WcrB5PoH084dg8AAAB4nGNgZGBgAOKMsPib8fw2Xxm4mRhA4PzjbBcY/f////1MjIwHgFwOBrA0AFcuDPF4nGNgZGBgPPD/AIMeEwMDw/9/TEwMQBEUwAEAe34EvHicY2JgYGCCYsbJCJpxO4QNABdTAesAAAAAAAAAEgAsAGgAjgC+AP4BQAAAeJxjYGRgYOBg0GJgZgABJiDmAkIGhv9gPgMADYEBTAB4nG2PTW7CMBCFXyBQFaQKtVKl7qwuuqkIPwsWHAD2LNiH4ARQEkeOQeICPUHP0DP0BF32DD1KX8IoixZbHn/z5o1/AAzwBQ/V8HBbx2q0cMPswm3SQNgnPwl30MezcJf6ULiHV8yE+3hAyBM8vzrtHk64hTu8Cbepvwv75A/hDh7xKdyl/i3cwxo/wn28eLN9ZPJhbHK30skxDW2TN7DWttybXE2CcaMtda5t6PRWbc6qPCVT52IVW5OpBas6TY0qrDnoyAU754r5aBSLHkQmwx4RDHL+Oq53hxU0EhyR8sf2Sv2/smaHRclKlStMEGB8xbekL6+9ITONLb0bnBlLnHjnlKqjW3FZ9mSkhfRqviclKxR17UAloh5gV3cVmGPEGf/xB/Ursl9uDmByAAAAeJxtwUEOgCAMBMAu0sI3SdMEIwKh8n8PXp2hQB+mf5kIAQciGIKEzFpNr6Sj7bs76xruMq3r2eJs22XZtPKIW1laiV6rCBDA") format("woff");
+ font-weight: normal;
+ font-style: normal;
+}
+[class^="w2ui-icon-"]:before,
+[class*=" w2ui-icon-"]:before {
+ font-family: "w2ui-font";
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 1;
+ font-weight: normal;
+ font-style: normal;
+ speak: none;
+ text-decoration: inherit;
+ text-transform: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+/* Icons */
+.w2ui-icon-check:before {
+ content: "\f101";
+}
+.w2ui-icon-columns:before {
+ content: "\f102";
+}
+.w2ui-icon-cross:before {
+ content: "\f103";
+}
+.w2ui-icon-pencil:before {
+ content: "\f104";
+}
+.w2ui-icon-plus:before {
+ content: "\f105";
+}
+.w2ui-icon-reload:before {
+ content: "\f106";
+}
+.w2ui-icon-search:before {
+ content: "\f107";
+}
+/*************************************************
+* --- Reset (used for all w2ui wdigetes)
+* --- The reset is needed to coexist with other CSS
+* --- on the same page (for example bootstrap)
+*/
+.w2ui-reset {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+ font-family: Verdana, Arial, sans-serif;
+ font-size: 11px;
+}
+.w2ui-reset * {
+ color: default;
+ line-height: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+ margin: 0px;
+ padding: 0px;
+}
+.w2ui-reset table {
+ font-family: Verdana, Arial, sans-serif;
+ font-size: 11px;
+ max-width: none;
+ background-color: transparent;
+ border-collapse: separate;
+ border-spacing: 0;
+}
+.w2ui-reset input,
+.w2ui-reset textarea {
+ width: auto;
+ height: auto;
+ vertical-align: baseline;
+ padding: 4px;
+}
+.w2ui-reset select {
+ padding: 1px;
+ height: 23px;
+}
+.w2ui-centered {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ -moz-transform: translateY(-50%);
+ -ms-transform: translateY(-50%);
+ -o-transform: translateY(-50%);
+ transform: translateY(-50%);
+ max-height: 100%;
+ margin: 0px;
+ padding: 0px 10px;
+ text-align: center;
+}
+.w2ui-disabled,
+.w2ui-readonly {
+ background-color: #f1f1f1 !important;
+ color: #777 !important;
+}
+/*************************************************
+* ---- Input Controls ----
+*/
+input:not([type=button]),
+select,
+textarea {
+ padding: 4px;
+ border: 1px solid #bbbbbb;
+ border-radius: 3px;
+ color: #000000;
+ background-color: #ffffff;
+}
+input:not([type=button]):focus,
+select:focus,
+textarea:focus {
+ outline-color: #72b2ff;
+}
+input:not([type=button]):disabled,
+select:disabled,
+textarea:disabled,
+input:not([type=button])[readonly],
+select[readonly],
+textarea[readonly] {
+ background-color: #f1f1f1;
+ color: #777;
+}
+/* IE9-11 specific classes */
+/* needs doblue :: */
+input::-ms-clear {
+ display: none;
+}
+input:-ms-input-placeholder {
+ color: #aaa !important;
+}
+select {
+ padding: 2px;
+}
+/* On/Off switch */
+input[type="checkbox"].w2ui-toggle {
+ position: absolute;
+ opacity: 0;
+ width: 46px;
+ height: 22px;
+ padding: 0px;
+ margin: 0px;
+ margin-left: 2px;
+}
+/* Track */
+input[type="checkbox"].w2ui-toggle + div {
+ display: inline-block;
+ width: 46px;
+ height: 22px;
+ border: 1px solid #bbb;
+ border-radius: 30px;
+ background-color: #eee;
+ -webkit-transition-duration: .3s;
+ -webkit-transition-property: background-color, box-shadow;
+ -moz-transition-duration: .3s;
+ -moz-transition-property: background-color, box-shadow;
+ box-shadow: inset 0 0 0 0px rgba(0, 0, 0, 0.4);
+ margin-left: 2px;
+}
+input[type="checkbox"].w2ui-toggle:disabled + div {
+ opacity: 0.3;
+}
+/* Knob */
+input[type="checkbox"].w2ui-toggle + div > div {
+ float: left;
+ width: 22px;
+ height: 22px;
+ border-radius: inherit;
+ background: #f5f5f5;
+ -webkit-transition-duration: 0.3s;
+ -webkit-transition-property: transform, background-color, box-shadow;
+ -moz-transition-duration: 0.3s;
+ -moz-transition-property: transform, background-color;
+ box-shadow: 0px 0px 1px #323232, 0 0 0 1px rgba(200, 200, 200, 0.6);
+ pointer-events: none;
+ margin-top: -1px;
+ margin-left: -1px;
+}
+/* Default Green */
+input[type="checkbox"].w2ui-toggle:checked + div {
+ border: 1px solid #00a23f;
+ box-shadow: inset 0 0 0 12px #54B350;
+}
+input[type="checkbox"].w2ui-toggle:checked + div > div {
+ -webkit-transform: translate3d(24px, 0, 0);
+ -moz-transform: translate3d(24px, 0, 0);
+ background-color: #ffffff;
+ box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3), 0 0 0 1px #00a23f;
+}
+/* Blue */
+input[type="checkbox"].w2ui-toggle.blue:checked + div {
+ border: 1px solid #206FAD;
+ box-shadow: inset 0 0 0 12px #35A6EB;
+}
+input[type="checkbox"].w2ui-toggle.blue:checked + div > div {
+ box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3), 0px 0px 0 1px #206fad;
+}
+input[type=checkbox].w2ui-toggle:focus {
+ outline: none;
+}
+/*************************************************
+* ---- Overlay and Bubble ----
+*/
+.w2ui-overlay {
+ position: absolute;
+ margin-top: 6px;
+ margin-left: -17px;
+ display: none;
+ z-index: 1300;
+ color: inherit;
+ background-color: #fbfbfb;
+ border: 3px solid #777777;
+ box-shadow: 0px 2px 10px #999999;
+ border-radius: 4px;
+ text-align: left;
+}
+.w2ui-overlay table td {
+ color: inherit;
+}
+.w2ui-overlay:before {
+ content: "";
+ position: absolute;
+ -webkit-transform: rotate(-45deg);
+ -moz-transform: rotate(-45deg);
+ -ms-transform: rotate(-45deg);
+ -o-transform: rotate(-45deg);
+ transform: rotate(-45deg);
+ width: 12px;
+ height: 12px;
+ border: 3px solid #777777;
+ border-color: inherit;
+ background-color: inherit;
+ border-left: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-bottom-left-radius: 50px;
+ margin: -9px 0 0 30px;
+}
+.w2ui-overlay:after {
+ display: none;
+ content: "";
+ position: absolute;
+ -webkit-transform: rotate(135deg);
+ -moz-transform: rotate(135deg);
+ -ms-transform: rotate(135deg);
+ -o-transform: rotate(135deg);
+ transform: rotate(135deg);
+ width: 12px;
+ height: 12px;
+ border: 3px solid #777777;
+ border-color: inherit;
+ background-color: inherit;
+ border-left: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-bottom-left-radius: 50px;
+ margin: -7px 0 0 30px;
+}
+.w2ui-overlay.w2ui-overlay-popup {
+ z-index: 1700;
+}
+.w2ui-tag {
+ position: absolute;
+ z-index: 1300;
+ opacity: 0;
+ -webkit-transition: opacity 0.3s;
+ -moz-transition: opacity 0.3s;
+ -ms-transition: opacity 0.3s;
+ -o-transition: opacity 0.3s;
+ transition: opacity 0.3s;
+}
+.w2ui-tag .w2ui-tag-body {
+ background-color: rgba(60, 60, 60, 0.82);
+ display: inline-block;
+ position: absolute;
+ border-radius: 4px;
+ padding: 4px 10px;
+ margin-left: 10px;
+ margin-top: 0px;
+ color: #ffffff !important;
+ box-shadow: 1px 1px 3px #000000;
+ line-height: 100%;
+ font-size: 11px;
+ font-family: Verdana, Arial, sans-serif;
+}
+.w2ui-tag .w2ui-tag-body:before {
+ content: "";
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-top: 5px solid transparent;
+ border-right: 5px solid rgba(60, 60, 60, 0.82);
+ border-bottom: 5px solid transparent;
+ margin: 2px 0 0 -15px;
+}
+.w2ui-tag.w2ui-tag-popup {
+ z-index: 1700;
+}
+/*
+* Drop down menu
+*/
+.w2ui-overlay table.w2ui-drop-menu {
+ width: 100%;
+ color: #000000;
+ background-color: #ffffff;
+ padding: 5px 0px;
+ cursor: default;
+}
+.w2ui-overlay table.w2ui-drop-menu td {
+ white-space: nowrap;
+}
+.w2ui-overlay table.w2ui-drop-menu .w2ui-item-even {
+ color: inherit;
+ background-color: #ffffff;
+}
+.w2ui-overlay table.w2ui-drop-menu .w2ui-item-odd {
+ color: inherit;
+ background-color: #f3f6fa;
+}
+.w2ui-overlay table.w2ui-drop-menu .w2ui-item-group {
+ color: #444;
+ font-weight: bold;
+ background-color: #ECEDF0;
+ border-bottom: 1px solid #D3D2D4;
+}
+.w2ui-overlay table.w2ui-drop-menu td.menu-icon {
+ padding: 3px 0px 4px 6px;
+ width: 20px;
+}
+.w2ui-overlay table.w2ui-drop-menu td.menu-text {
+ padding: 8px 10px 8px 5px;
+ width: auto;
+}
+.w2ui-overlay table.w2ui-drop-menu td.menu-count {
+ text-align: right;
+}
+.w2ui-overlay table.w2ui-drop-menu td.menu-count > span {
+ border: 1px solid #9da4af;
+ border-radius: 20px;
+ width: auto;
+ height: 18px;
+ padding: 2px 7px;
+ margin: 3px 5px 0px 5px;
+ background-color: #e7f0fc;
+ color: #667274;
+ box-shadow: 0 0 2px #ffffff;
+ text-shadow: 1px 1px 1px #e6e6e6;
+}
+.w2ui-overlay table.w2ui-drop-menu tr:hover {
+ color: inherit;
+ background-color: #e6f0ff;
+}
+.w2ui-overlay table.w2ui-drop-menu tr.w2ui-selected {
+ background-color: #b6d5fb;
+}
+.w2ui-overlay table.w2ui-drop-menu tr.w2ui-selected td {
+ color: inherit;
+}
+.w2ui-overlay table.w2ui-drop-menu tr.w2ui-disabled {
+ opacity: 0.4;
+ background-color: white !important;
+}
+.w2ui-overlay table.w2ui-drop-menu .w2ui-icon {
+ font-size: 14px;
+ color: #8d99a7;
+ display: inline-block;
+ padding-top: 4px;
+}
+/*************************************************
+* ---- Common Classes ----
+*/
+.w2ui-marker {
+ color: #444;
+ background-color: rgba(252, 244, 161, 0.48);
+}
+.w2ui-spinner {
+ display: inline-block;
+ background-size: 100%;
+ background-repeat: no-repeat;
+ background-image: url(data:image/gif;base64,R0lGODlhgACAAKIAAP///93d3bu7u5mZmQAA/wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFBQAEACwCAAIAfAB8AAAD/0i63P4wygYqmDjrzbtflvWNZGliYXiubKuloivPLlzReD7al+7/Eh5wSFQIi8hHYBkwHUmD6CD5YTJLz49USuVYraRsZ7vtar7XnQ1Kjpoz6LRHvGlz35O4nEPP2O94EnpNc2sef1OBGIOFMId/inB6jSmPdpGScR19EoiYmZobnBCIiZ95k6KGGp6ni4wvqxilrqBfqo6skLW2YBmjDa28r6Eosp27w8Rov8ekycqoqUHODrTRvXsQwArC2NLF29UM19/LtxO5yJd4Au4CK7DUNxPebG4e7+8n8iv2WmQ66BtoYpo/dvfacBjIkITBE9DGlMvAsOIIZjIUAixliv9ixYZVtLUos5GjwI8gzc3iCGghypQqrbFsme8lwZgLZtIcYfNmTJ34WPTUZw5oRxdD9w0z6iOpO15MgTh1BTTJUKos39jE+o/KS64IFVmsFfYT0aU7capdy7at27dw48qdS7eu3bt480I02vUbX2F/JxYNDImw4GiGE/P9qbhxVpWOI/eFKtlNZbWXuzlmG1mv58+gQ4seTbq06dOoU6vGQZJy0FNlMcV+czhQ7SQmYd8eMhPs5BxVdfcGEtV3buDBXQ+fURxx8oM6MT9P+Fh6dOrH2zavc13u9JXVJb520Vp8dvC76wXMuN5Sepm/1WtkEZHDefnzR9Qvsd9+/wi8+en3X0ntYVcSdAE+UN4zs7ln24CaLagghIxBaGF8kFGoIYV+Ybghh841GIyI5ICIFoklJsigihmimJOLEbLYIYwxSgigiZ+8l2KB+Ml4oo/w8dijjcrouCORKwIpnJIjMnkkksalNeR4fuBIm5UEYImhIlsGCeWNNJphpJdSTlkml1jWeOY6TnaRpppUctcmFW9mGSaZceYopH9zkjnjUe59iR5pdapWaGqHopboaYua1qije67GJ6CuJAAAIfkEBQUABAAsCgACAFcAMAAAA/9Iutz+ML5Ag7w46z0r5WAoSp43nihXVmnrdusrv+s332dt4Tyo9yOBUJD6oQBIQGs4RBlHySSKyczVTtHoidocPUNZaZAr9F5FYbGI3PWdQWn1mi36buLKFJvojsHjLnshdhl4L4IqbxqGh4gahBJ4eY1kiX6LgDN7fBmQEJI4jhieD4yhdJ2KkZk8oiSqEaatqBekDLKztBG2CqBACq4wJRi4PZu1sA2+v8C6EJexrBAD1AOBzsLE0g/V1UvYR9sN3eR6lTLi4+TlY1wz6Qzr8u1t6FkY8vNzZTxaGfn6mAkEGFDgL4LrDDJDyE4hEIbdHB6ESE1iD4oVLfLAqPETIsOODwmCDJlv5MSGJklaS6khAQAh+QQFBQAEACwfAAIAVwAwAAAD/0i63P5LSAGrvTjrNuf+YKh1nWieIumhbFupkivPBEzR+GnnfLj3ooFwwPqdAshAazhEGUXJJIrJ1MGOUamJ2jQ9QVltkCv0XqFh5IncBX01afGYnDqD40u2z76JK/N0bnxweC5sRB9vF34zh4gjg4uMjXobihWTlJUZlw9+fzSHlpGYhTminKSepqebF50NmTyor6qxrLO0L7YLn0ALuhCwCrJAjrUqkrjGrsIkGMW/BMEPJcphLgDaABjUKNEh29vdgTLLIOLpF80s5xrp8ORVONgi8PcZ8zlRJvf40tL8/QPYQ+BAgjgMxkPIQ6E6hgkdjoNIQ+JEijMsasNY0RQix4gKP+YIKXKkwJIFF6JMudFEAgAh+QQFBQAEACw8AAIAQgBCAAAD/kg0PPowykmrna3dzXvNmSeOFqiRaGoyaTuujitv8Gx/661HtSv8gt2jlwIChYtc0XjcEUnMpu4pikpv1I71astytkGh9wJGJk3QrXlcKa+VWjeSPZHP4Rtw+I2OW81DeBZ2fCB+UYCBfWRqiQp0CnqOj4J1jZOQkpOUIYx/m4oxg5cuAaYBO4Qop6c6pKusrDevIrG2rkwptrupXB67vKAbwMHCFcTFxhLIt8oUzLHOE9Cy0hHUrdbX2KjaENzey9Dh08jkz8Tnx83q66bt8PHy8/T19vf4+fr6AP3+/wADAjQmsKDBf6AOKjS4aaHDgZMeSgTQcKLDhBYPEswoA1BBAgAh+QQFBQAEACxOAAoAMABXAAAD7Ei6vPOjyUkrhdDqfXHm4OZ9YSmNpKmiqVqykbuysgvX5o2HcLxzup8oKLQQix0UcqhcVo5ORi+aHFEn02sDeuWqBGCBkbYLh5/NmnldxajX7LbPBK+PH7K6narfO/t+SIBwfINmUYaHf4lghYyOhlqJWgqDlAuAlwyBmpVnnaChoqOkpaanqKmqKgGtrq+wsbA1srW2ry63urasu764Jr/CAb3Du7nGt7TJsqvOz9DR0tPU1TIA2ACl2dyi3N/aneDf4uPklObj6OngWuzt7u/d8fLY9PXr9eFX+vv8+PnYlUsXiqC3c6PmUUgAACH5BAUFAAQALE4AHwAwAFcAAAPpSLrc/m7IAau9bU7MO9GgJ0ZgOI5leoqpumKt+1axPJO1dtO5vuM9yi8TlAyBvSMxqES2mo8cFFKb8kzWqzDL7Xq/4LB4TC6bz1yBes1uu9uzt3zOXtHv8xN+Dx/x/wJ6gHt2g3Rxhm9oi4yNjo+QkZKTCgGWAWaXmmOanZhgnp2goaJdpKGmp55cqqusrZuvsJays6mzn1m4uRAAvgAvuBW/v8GwvcTFxqfIycA3zA/OytCl0tPPO7HD2GLYvt7dYd/ZX99j5+Pi6tPh6+bvXuTuzujxXens9fr7YPn+7egRI9PPHrgpCQAAIfkEBQUABAAsPAA8AEIAQgAAA/lIutz+UI1Jq7026h2x/xUncmD5jehjrlnqSmz8vrE8u7V5z/m5/8CgcEgsGo/IpHLJbDqf0Kh0ShBYBdTXdZsdbb/Yrgb8FUfIYLMDTVYz2G13FV6Wz+lX+x0fdvPzdn9WeoJGAYcBN39EiIiKeEONjTt0kZKHQGyWl4mZdREAoQAcnJhBXBqioqSlT6qqG6WmTK+rsa1NtaGsuEu6o7yXubojsrTEIsa+yMm9SL8osp3PzM2cStDRykfZ2tfUtS/bRd3ewtzV5pLo4eLjQuUp70Hx8t9E9eqO5Oku5/ztdkxi90qPg3x2EMpR6IahGocPCxp8AGtigwQAIfkEBQUABAAsHwBOAFcAMAAAA/9Iutz+MMo36pg4682J/V0ojs1nXmSqSqe5vrDXunEdzq2ta3i+/5DeCUh0CGnF5BGULC4tTeUTFQVONYAs4CfoCkZPjFar83rBx8l4XDObSUL1Ott2d1U4yZwcs5/xSBB7dBMBhgEYfncrTBGDW4WHhomKUY+QEZKSE4qLRY8YmoeUfkmXoaKInJ2fgxmpqqulQKCvqRqsP7WooriVO7u8mhu5NacasMTFMMHCm8qzzM2RvdDRK9PUwxzLKdnaz9y/Kt8SyR3dIuXmtyHpHMcd5+jvWK4i8/TXHff47SLjQvQLkU+fG29rUhQ06IkEG4X/Rryp4mwUxSgLL/7IqFETB8eONT6ChCFy5ItqJomES6kgAQAh+QQFBQAEACwKAE4AVwAwAAAD/0i63A4QuEmrvTi3yLX/4MeNUmieITmibEuppCu3sDrfYG3jPKbHveDktxIaF8TOcZmMLI9NyBPanFKJp4A2IBx4B5lkdqvtfb8+HYpMxp3Pl1qLvXW/vWkli16/3dFxTi58ZRcChwIYf3hWBIRchoiHiotWj5AVkpIXi4xLjxiaiJR/T5ehoomcnZ+EGamqq6VGoK+pGqxCtaiiuJVBu7yaHrk4pxqwxMUzwcKbyrPMzZG90NGDrh/JH8t72dq3IN1jfCHb3L/e5ebh4ukmxyDn6O8g08jt7tf26ybz+m/W9GNXzUQ9fm1Q/APoSWAhhfkMAmpEbRhFKwsvCsmosRIHx444PoKcIXKkjIImjTzjkQAAIfkEBQUABAAsAgA8AEIAQgAAA/VIBNz+8KlJq72Yxs1d/uDVjVxogmQqnaylvkArT7A63/V47/m2/8CgcEgsGo/IpHLJbDqf0Kh0Sj0FroGqDMvVmrjgrDcTBo8v5fCZki6vCW33Oq4+0832O/at3+f7fICBdzsChgJGeoWHhkV0P4yMRG1BkYeOeECWl5hXQ5uNIAOjA1KgiKKko1CnqBmqqk+nIbCkTq20taVNs7m1vKAnurtLvb6wTMbHsUq4wrrFwSzDzcrLtknW16tI2tvERt6pv0fi48jh5h/U6Zs77EXSN/BE8jP09ZFA+PmhP/xvJgAMSGBgQINvEK5ReIZhQ3QEMTBLAAAh+QQFBQAEACwCAB8AMABXAAAD50i6DA4syklre87qTbHn4OaNYSmNqKmiqVqyrcvBsazRpH3jmC7yD98OCBF2iEXjBKmsAJsWHDQKmw571l8my+16v+CweEwum8+hgHrNbrvbtrd8znbR73MVfg838f8BeoB7doN0cYZvaIuMjY6PkJGSk2gClgJml5pjmp2YYJ6dX6GeXaShWaeoVqqlU62ir7CXqbOWrLafsrNctjIDwAMWvC7BwRWtNsbGFKc+y8fNsTrQ0dK3QtXAYtrCYd3eYN3c49/a5NVj5eLn5u3s6e7x8NDo9fbL+Mzy9/T5+tvUzdN3Zp+GBAAh+QQJBQAEACwCAAIAfAB8AAAD/0i63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdArcQK2TOL7/nl4PSMwIfcUk5YhUOh3M5nNKiOaoWCuWqt1Ou16l9RpOgsvEMdocXbOZ7nQ7DjzTaeq7zq6P5fszfIASAYUBIYKDDoaGIImKC4ySH3OQEJKYHZWWi5iZG0ecEZ6eHEOio6SfqCaqpaytrpOwJLKztCO2jLi1uoW8Ir6/wCHCxMG2x7muysukzb230M6H09bX2Nna29zd3t/g4cAC5OXm5+jn3Ons7eba7vHt2fL16tj2+QL0+vXw/e7WAUwnrqDBgwgTKlzIsKHDh2gGSBwAccHEixAvaqTYcFCjRoYeNyoM6REhyZIHT4o0qPIjy5YTTcKUmHImx5cwE85cmJPnSYckK66sSAAj0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gwxZJAAA7);
+}
+/* common icons */
+.w2ui-icon {
+ background-repeat: no-repeat;
+ height: 16px;
+ width: 16px;
+ overflow: hidden;
+ margin: 2px 2px;
+ display: inline-block;
+}
+.w2ui-icon.icon-search,
+.w2ui-icon.icon-search-down {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAgCAYAAAB+ZAqzAAACuElEQVRYw9WXSWhTQRjHR0UKLqhFaV0OUih68GAOWjyJKypCpAoV8aIiioIICiKiB1GMtE3MYmry2moXDz1UDx7sUXHBhQpSaRVxrYpWcMO9avx/8AJh/CbznHkxdeB3Cd/8589kvuUJkWcdjCTHghUgAi6DJ+AVeAqugSQIggniXywcNBJsB70g44EHYBcYXUhTM8EFj4ZkboKqQpiqAv2GprK8o7/f75t6pjn0M3gNPmri3vtycxAZA64qDvkJ2kENqAQTQQWoBg74qth3B4y3NbZDIX4fzNfsnQtuK/YfsjFVCh4pMq3Co0Y5uMVoUGkpy8aFT5xaeSzVEo45bXdBt4LeaLq1k0RXMYJfdDfFmAuAD4zWlty4UNyZEkm19MUb2zMw8Sfp1u+IWSrcIimLnTG8/SijdU6OO5poDESdtgHZVBzUHm/amhW7zoitMTS2mNHqASPk2FDCCcLMYK6p+obmulyxfiYLA4bGKFvfSnrUvkq5+Lpk8z4yRH8r3l/X4WiqJFfspSQ0CGYZGpsMnkt6L+h31Z76hpMdeOwPQ7H0NFnssST0C8wxNDaDKb6kP06150gsHahNNlVzYheZd7HJ0BiX4VRGhpmIhRixKyZilM2M1mnTArtIUbU3/qVO0H0GvmQ4CY4C3YopYYlHjXlggNG4R33Ypi2tVtwaPeTdNMkq9pVQZQdvFPs32zbx4aAjzxhDRfIAWAeWg7VgrzsY5ht/zoNJtubKwA3LITGjSKRyW3NTwaUCmKOSMd3WHH0ZJRQZZkOP1zFKZ3CB++4+aQ6kEeksWAb2a2L7qDv49S1Q6T72MOgEXa6RGFhP3wpS/B6NOWpRs0UxFg7eqTFHjX1hscxtAz/ymEuIYi0cvgF8Y0w5Ro3dZ3M1boJkTaXEUFlug6fsdsRQWzTj0cey+N/Xb2sj5lTh2M6OAAAAAElFTkSuQmCC) no-repeat center !important;
+ background-size: 14px 12px !important;
+ opacity: 0.9;
+}
+.w2ui-icon.icon-folder {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC) no-repeat center !important;
+}
+.w2ui-icon.icon-page {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAINSURBVBgZBcG/r55zGAfg6/4+z3va01NHlYgzEfE7MdCIGISFgS4Gk8ViYyM2Mdlsko4GSf8Do0FLRCIkghhYJA3aVBtEz3nP89wf11VJvPDepdd390+8Nso5nESBQoq0pfvXm9fzWf19453LF85vASqJlz748vInb517dIw6EyYBIIG49u+xi9/c9MdvR//99MPPZ7+4cP4IZhhTPbwzT2d+vGoaVRRp1rRliVvHq+cfvM3TD82+7mun0o/ceO7NT+/4/KOXjwZU1ekk0840bAZzMQ2mooqh0A72d5x/6sB9D5zYnff3PoYBoWBgFKPKqDKqjCpjKr//dcu9p489dra88cydps30KswACfNEKanSaxhlntjJ8Mv12Paie+vZ+0+oeSwwQ0Iw1xAR1CiFNJkGO4wu3ZMY1AAzBI0qSgmCNJsJUEOtJSMaCTBDLyQ0CknAGOgyTyFFiLI2awMzdEcSQgSAAKVUmAeNkxvWJWCGtVlDmgYQ0GFtgg4pNtOwbBcwQy/Rife/2yrRRVI0qYCEBly8Z+P4qMEMy7JaVw72N568e+iwhrXoECQkfH91kY7jwwXMsBx1L93ZruqrK6uuiAIdSnTIKKPLPFcvay8ww/Hh+ufeznTXu49v95IMoQG3784gYXdTqvRmqn/Wpa/ADFX58MW3L71SVU9ETgEIQQQIOOzub+fhIvwPRDgeVjWDahIAAAAASUVORK5CYII=) no-repeat center !important;
+}
+/*************************************************
+* ---- Locking portion of the screen (in grid, form, layout)
+*/
+.w2ui-lock {
+ display: none;
+ position: absolute;
+ z-index: 1400;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ background-color: #333333;
+}
+.w2ui-lock-msg {
+ display: none;
+ position: absolute;
+ z-index: 1400;
+ top: 45%;
+ left: 50%;
+ -webkit-transform: translateX(-50%) translateY(-50%);
+ -moz-transform: translateX(-50%) translateY(-50%);
+ -ms-transform: translateX(-50%) translateY(-50%);
+ -o-transform: translateX(-50%) translateY(-50%);
+ transform: translateX(-50%) translateY(-50%);
+ width: 200px;
+ height: 80px;
+ padding: 30px 8px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: 13px;
+ font-family: Verdana, Arial, sans-serif;
+ opacity: 0.8;
+ filter: alpha(opacity=80);
+ background-color: #555555;
+ color: #ffffff;
+ text-align: center;
+ border-radius: 5px;
+ border: 2px solid #444444;
+}
+.w2ui-lock-msg .w2ui-spinner {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ margin: -3px 8px -7px -10px;
+}
+button.btn {
+ display: inline-block;
+ border-radius: 4px;
+ margin: 0px 5px;
+ padding: 7px 12px 6px 12px !important;
+ color: #666;
+ font-size: 12px !important;
+ border: 1px solid #B6B6B6;
+ background-image: -webkit-linear-gradient(#ffffff 0%, #e7e7e7 100%);
+ background-image: -moz-linear-gradient(#ffffff 0%, #e7e7e7 100%);
+ background-image: -ms-linear-gradient(#ffffff 0%, #e7e7e7 100%);
+ background-image: -o-linear-gradient(#ffffff 0%, #e7e7e7 100%);
+ background-image: linear-gradient(#ffffff 0%, #e7e7e7 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffe7e7e7', endColorstr='#ffffffff', GradientType=0);
+ outline: none;
+ box-shadow: 0px 1px 0px white;
+ cursor: default;
+ min-width: 75px;
+ line-height: 100% !important;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+button.btn:hover {
+ text-decoration: none;
+ border: 1px solid #bbb;
+ background-image: -webkit-linear-gradient(#f7f7f7 0%, #dddddd 100%);
+ background-image: -moz-linear-gradient(#f7f7f7 0%, #dddddd 100%);
+ background-image: -ms-linear-gradient(#f7f7f7 0%, #dddddd 100%);
+ background-image: -o-linear-gradient(#f7f7f7 0%, #dddddd 100%);
+ background-image: linear-gradient(#f7f7f7 0%, #dddddd 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffdddddd', endColorstr='#fff7f7f7', GradientType=0);
+ color: #333;
+}
+button.btn:active,
+button.btn.clicked {
+ border: 1px solid #999;
+ background-image: -webkit-linear-gradient(#cccccc 0%, #cccccc 100%);
+ background-image: -moz-linear-gradient(#cccccc 0%, #cccccc 100%);
+ background-image: -ms-linear-gradient(#cccccc 0%, #cccccc 100%);
+ background-image: -o-linear-gradient(#cccccc 0%, #cccccc 100%);
+ background-image: linear-gradient(#cccccc 0%, #cccccc 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffcccccc', endColorstr='#ffcccccc', GradientType=0);
+ text-shadow: 1px 1px 1px #eee;
+}
+button.btn:disabled {
+ border: 1px solid #bbb !important;
+ background: #f7f7f7 !important;
+ color: #bdbcbc !important;
+ text-shadow: none !important;
+}
+button.btn-blue {
+ color: white;
+ background-image: -webkit-linear-gradient(#80c0f7 0%, #269df0 100%);
+ background-image: -moz-linear-gradient(#80c0f7 0%, #269df0 100%);
+ background-image: -ms-linear-gradient(#80c0f7 0%, #269df0 100%);
+ background-image: -o-linear-gradient(#80c0f7 0%, #269df0 100%);
+ background-image: linear-gradient(#80c0f7 0%, #269df0 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff269df0', endColorstr='#ff80c0f7', GradientType=0);
+ border: 1px solid #538AB7;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-blue:hover {
+ color: white;
+ background-image: -webkit-linear-gradient(#73b6f0 0%, #2391dd 100%);
+ background-image: -moz-linear-gradient(#73b6f0 0%, #2391dd 100%);
+ background-image: -ms-linear-gradient(#73b6f0 0%, #2391dd 100%);
+ background-image: -o-linear-gradient(#73b6f0 0%, #2391dd 100%);
+ background-image: linear-gradient(#73b6f0 0%, #2391dd 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff2391dd', endColorstr='#ff73b6f0', GradientType=0);
+ border: 1px solid #497BA3;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-blue:active,
+button.btn-blue.clicked {
+ color: white;
+ background-image: -webkit-linear-gradient(#1e83c9 0%, #1e83c9 100%);
+ background-image: -moz-linear-gradient(#1e83c9 0%, #1e83c9 100%);
+ background-image: -ms-linear-gradient(#1e83c9 0%, #1e83c9 100%);
+ background-image: -o-linear-gradient(#1e83c9 0%, #1e83c9 100%);
+ background-image: linear-gradient(#1e83c9 0%, #1e83c9 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff1e83c9', endColorstr='#ff1e83c9', GradientType=0);
+ border: 1px solid #1268A6;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-green {
+ color: white;
+ background-image: -webkit-linear-gradient(#81cf81 0%, #52a452 100%);
+ background-image: -moz-linear-gradient(#81cf81 0%, #52a452 100%);
+ background-image: -ms-linear-gradient(#81cf81 0%, #52a452 100%);
+ background-image: -o-linear-gradient(#81cf81 0%, #52a452 100%);
+ background-image: linear-gradient(#81cf81 0%, #52a452 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff52a452', endColorstr='#ff81cf81', GradientType=0);
+ border: 1px solid #479247;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-green:hover {
+ color: white;
+ background-image: -webkit-linear-gradient(#6abe68 0%, #3f8f3d 100%);
+ background-image: -moz-linear-gradient(#6abe68 0%, #3f8f3d 100%);
+ background-image: -ms-linear-gradient(#6abe68 0%, #3f8f3d 100%);
+ background-image: -o-linear-gradient(#6abe68 0%, #3f8f3d 100%);
+ background-image: linear-gradient(#6abe68 0%, #3f8f3d 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff3f8f3d', endColorstr='#ff6abe68', GradientType=0);
+ border: 1px solid #479247;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-green:active,
+button.btn-green.clicked {
+ color: white;
+ background-image: -webkit-linear-gradient(#377d36 0%, #377d36 100%);
+ background-image: -moz-linear-gradient(#377d36 0%, #377d36 100%);
+ background-image: -ms-linear-gradient(#377d36 0%, #377d36 100%);
+ background-image: -o-linear-gradient(#377d36 0%, #377d36 100%);
+ background-image: linear-gradient(#377d36 0%, #377d36 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff377d36', endColorstr='#ff377d36', GradientType=0);
+ border: 1px solid #555 !important;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-orange {
+ color: white;
+ background-image: -webkit-linear-gradient(#fcc272 0%, #fb8822 100%);
+ background-image: -moz-linear-gradient(#fcc272 0%, #fb8822 100%);
+ background-image: -ms-linear-gradient(#fcc272 0%, #fb8822 100%);
+ background-image: -o-linear-gradient(#fcc272 0%, #fb8822 100%);
+ background-image: linear-gradient(#fcc272 0%, #fb8822 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fffb8822', endColorstr='#fffcc272', GradientType=0);
+ border: 1px solid #B68B4C;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-orange:hover {
+ color: white;
+ background-image: -webkit-linear-gradient(#f4ad59 0%, #f1731f 100%);
+ background-image: -moz-linear-gradient(#f4ad59 0%, #f1731f 100%);
+ background-image: -ms-linear-gradient(#f4ad59 0%, #f1731f 100%);
+ background-image: -o-linear-gradient(#f4ad59 0%, #f1731f 100%);
+ background-image: linear-gradient(#f4ad59 0%, #f1731f 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff1731f', endColorstr='#fff4ad59', GradientType=0);
+ border: 1px solid #B68B4C;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-orange:active,
+button.btn-orange.clicked {
+ color: white;
+ border: 1px solid #666;
+ background-image: -webkit-linear-gradient(#b98747 0%, #b98747 100%);
+ background-image: -moz-linear-gradient(#b98747 0%, #b98747 100%);
+ background-image: -ms-linear-gradient(#b98747 0%, #b98747 100%);
+ background-image: -o-linear-gradient(#b98747 0%, #b98747 100%);
+ background-image: linear-gradient(#b98747 0%, #b98747 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffb98747', endColorstr='#ffb98747', GradientType=0);
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-red {
+ color: white;
+ background-image: -webkit-linear-gradient(#ff6e70 0%, #c72d2d 100%);
+ background-image: -moz-linear-gradient(#ff6e70 0%, #c72d2d 100%);
+ background-image: -ms-linear-gradient(#ff6e70 0%, #c72d2d 100%);
+ background-image: -o-linear-gradient(#ff6e70 0%, #c72d2d 100%);
+ background-image: linear-gradient(#ff6e70 0%, #c72d2d 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffc72d2d', endColorstr='#ffff6e70', GradientType=0);
+ border: 1px solid #BB3C3E;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-red:hover {
+ color: white;
+ background-image: -webkit-linear-gradient(#ee696c 0%, #ae2527 100%);
+ background-image: -moz-linear-gradient(#ee696c 0%, #ae2527 100%);
+ background-image: -ms-linear-gradient(#ee696c 0%, #ae2527 100%);
+ background-image: -o-linear-gradient(#ee696c 0%, #ae2527 100%);
+ background-image: linear-gradient(#ee696c 0%, #ae2527 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffae2527', endColorstr='#ffee696c', GradientType=0);
+ border: 1px solid #BB3C3E;
+ text-shadow: 1px 1px 1px #777777;
+}
+button.btn-red:active,
+button.btn-red.clicked {
+ color: white;
+ border: 1px solid #861C1E;
+ background-image: -webkit-linear-gradient(#9c2123 0%, #9c2123 100%);
+ background-image: -moz-linear-gradient(#9c2123 0%, #9c2123 100%);
+ background-image: -ms-linear-gradient(#9c2123 0%, #9c2123 100%);
+ background-image: -o-linear-gradient(#9c2123 0%, #9c2123 100%);
+ background-image: linear-gradient(#9c2123 0%, #9c2123 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff9c2123', endColorstr='#ff9c2123', GradientType=0);
+ text-shadow: 1px 1px 1px #777777;
+}
+/*************************************************
+* ---- Forms ----
+*/
+.w2ui-form {
+ position: relative;
+ color: #000000;
+ background-color: #f5f6f7;
+ border: 1px solid #c0c0c0;
+ border-radius: 3px;
+ padding: 0px;
+ overflow: hidden !important;
+}
+.w2ui-form > div {
+ position: absolute;
+ overflow: hidden;
+}
+.w2ui-form .w2ui-form-header {
+ position: absolute;
+ left: 0;
+ right: 0;
+ border-bottom: 1px solid #99bbe8 !important;
+ overflow: hidden;
+ color: #444444;
+ font-size: 13px;
+ text-align: center;
+ padding: 8px;
+ background-image: -webkit-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -moz-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -ms-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -o-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: linear-gradient(#dae6f3, #c2d5ed);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffdae6f3', endColorstr='#ffc2d5ed', GradientType=0);
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+}
+.w2ui-form .w2ui-form-toolbar {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ margin: 0px;
+ padding: 6px 3px;
+ border-bottom: 1px solid #d5d8d8;
+}
+.w2ui-form .w2ui-form-tabs {
+ margin: 0px;
+ padding: 0px;
+}
+.w2ui-form .w2ui-tabs {
+ position: absolute;
+ left: 0;
+ right: 0;
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ padding-top: 5px !important;
+ background-color: #fafafa;
+}
+.w2ui-form .w2ui-tabs .w2ui-tab.active {
+ background-color: #f5f6f7;
+}
+.w2ui-form .w2ui-page {
+ position: absolute;
+ left: 0;
+ right: 0;
+ overflow: auto;
+ padding: 10px;
+ border-left: 1px solid inherit;
+ border-right: 1px solid inherit;
+ background-color: inherit;
+ border-radius: 3px;
+}
+.w2ui-form .w2ui-buttons {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ text-align: center;
+ border-top: 1px solid #d5d8d8;
+ border-bottom: 0px solid #d5d8d8;
+ background-color: #fafafa;
+ padding: 15px 0px !important;
+ border-bottom-left-radius: 3px;
+ border-bottom-right-radius: 3px;
+}
+.w2ui-form .w2ui-buttons input[type="button"],
+.w2ui-form .w2ui-buttons button {
+ min-width: 80px;
+ margin-right: 5px;
+}
+.w2ui-form input[type=checkbox],
+.w2ui-form input[type=radio] {
+ margin-top: 4px;
+ margin-bottom: 4px;
+}
+.w2ui-form input[type=checkbox].w2ui-toggle {
+ margin: 0px;
+}
+.w2ui-group-title {
+ padding: 5px 2px;
+ color: #8D96A2;
+ text-shadow: 1px 1px 2px #fdfdfd;
+ font-size: 120%;
+}
+.w2ui-group {
+ background-color: #ebecef;
+ margin: 5px 0px 10px 0px;
+ padding: 10px 5px;
+ border-top: 1px solid #cedcea;
+ border-bottom: 1px solid #cedcea;
+}
+.w2ui-field > label {
+ display: block;
+ float: left;
+ margin-top: 7px;
+ margin-bottom: 3px;
+ width: 120px;
+ padding: 0px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: right;
+ min-height: 20px;
+ color: #666;
+}
+.w2ui-field > div {
+ /* do not include width */
+ margin-bottom: 3px;
+ margin-left: 128px;
+ padding: 3px;
+ min-height: 28px;
+ float: none;
+}
+.w2ui-field.w2ui-required > div {
+ position: relative;
+}
+.w2ui-field.w2ui-required > div::before {
+ content: '*';
+ position: absolute;
+ margin-top: 5px;
+ margin-left: -9px;
+ color: #ff0000;
+}
+.w2ui-field.w2ui-span1 > label {
+ width: 20px;
+}
+.w2ui-field.w2ui-span1 > div {
+ margin-left: 28px;
+}
+.w2ui-field.w2ui-span2 > label {
+ width: 40px;
+}
+.w2ui-field.w2ui-span2 > div {
+ margin-left: 48px;
+}
+.w2ui-field.w2ui-span3 > label {
+ width: 60px;
+}
+.w2ui-field.w2ui-span3 > div {
+ margin-left: 68px;
+}
+.w2ui-field.w2ui-span4 > label {
+ width: 80px;
+}
+.w2ui-field.w2ui-span4 > div {
+ margin-left: 88px;
+}
+.w2ui-field.w2ui-span5 > label {
+ width: 100px;
+}
+.w2ui-field.w2ui-span5 > div {
+ margin-left: 108px;
+}
+.w2ui-field.w2ui-span6 > label {
+ width: 120px;
+}
+.w2ui-field.w2ui-span6 > div {
+ margin-left: 128px;
+}
+.w2ui-field.w2ui-span7 > label {
+ width: 140px;
+}
+.w2ui-field.w2ui-span7 > div {
+ margin-left: 148px;
+}
+.w2ui-field.w2ui-span8 > label {
+ width: 160px;
+}
+.w2ui-field.w2ui-span8 > div {
+ margin-left: 168px;
+}
+.w2ui-field.w2ui-span9 > label {
+ width: 180px;
+}
+.w2ui-field.w2ui-span9 > div {
+ margin-left: 188px;
+}
+.w2ui-field.w2ui-span10 > label {
+ width: 200px;
+}
+.w2ui-field.w2ui-span10 > div {
+ margin-left: 208px;
+}
+.w2ui-error {
+ border: 1px solid #ffa8a8 !important;
+ background-color: #fff4eb !important;
+}
+.w2field {
+ padding: 3px;
+ border-radius: 3px;
+ border: 1px solid silver;
+}
+.w2ui-field-helper {
+ position: absolute;
+ display: inline-block;
+ line-height: 100%;
+ /* pointer-events: none; - do not use as IE does not support it */
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+}
+.w2ui-field-helper .w2ui-field-up {
+ position: absolute;
+ top: 0px;
+ padding: 2px 3px;
+}
+.w2ui-field-helper .w2ui-field-down {
+ position: absolute;
+ bottom: 0px;
+ padding: 2px 3px;
+}
+.w2ui-field-helper .arrow-up:hover {
+ border-bottom-color: #81C6FF;
+}
+.w2ui-field-helper .arrow-down:hover {
+ border-top-color: #81C6FF;
+}
+/*
+* ARROWS
+*/
+.arrow-up {
+ background: none;
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ /* left arrow slant */
+ border-right: 4px solid transparent;
+ /* right arrow slant */
+ border-bottom: 5px solid #777;
+ /* bottom, add background color here */
+ font-size: 0;
+ line-height: 0;
+}
+.arrow-down {
+ background: none;
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 5px solid #777;
+ font-size: 0;
+ line-height: 0;
+}
+.arrow-left {
+ background: none;
+ width: 0;
+ height: 0;
+ border-bottom: 4px solid transparent;
+ /* left arrow slant */
+ border-top: 4px solid transparent;
+ /* right arrow slant */
+ border-right: 5px solid #777;
+ /* bottom, add background color here */
+ font-size: 0;
+ line-height: 0;
+}
+.arrow-right {
+ background: none;
+ width: 0;
+ height: 0;
+ border-bottom: 4px solid transparent;
+ /* left arrow slant */
+ border-top: 4px solid transparent;
+ /* right arrow slant */
+ border-left: 5px solid #777;
+ /* bottom, add background color here */
+ font-size: 0;
+ line-height: 0;
+}
+/*
+* COLOR overlay
+*/
+.w2ui-color {
+ padding: 5px;
+ padding-top: 8px;
+ background-color: white;
+ border-radius: 3px;
+}
+.w2ui-color > table {
+ table-layout: fixed;
+ width: 160px;
+}
+.w2ui-color > table td {
+ width: 20px;
+ height: 20px;
+ text-align: center;
+}
+.w2ui-color > table td div {
+ cursor: pointer;
+ display: inline-block;
+ width: 16px;
+ height: 17px;
+ padding: 1px 4px;
+ border: 1px solid transparent;
+ color: white;
+ text-shadow: 0px 0px 2px #000;
+}
+.w2ui-color > table td div:hover {
+ outline: 1px solid #666;
+ border: 1px solid #fff;
+}
+/*
+* DATE overlay
+*/
+.w2ui-calendar {
+ margin: 0px;
+ padding: 1px;
+ line-height: 108%;
+}
+.w2ui-calendar .w2ui-calendar-title {
+ margin: 0px -1px;
+ padding: 7px 2px;
+ background-image: -webkit-linear-gradient(#f6f6f6, #d9d9d9);
+ background-image: -moz-linear-gradient(#f6f6f6, #d9d9d9);
+ background-image: -ms-linear-gradient(#f6f6f6, #d9d9d9);
+ background-image: -o-linear-gradient(#f6f6f6, #d9d9d9);
+ background-image: linear-gradient(#f6f6f6, #d9d9d9);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#ffd9d9d9', GradientType=0);
+ border-bottom: 1px solid #bbbbbb;
+ color: #555555;
+ text-align: center;
+ text-shadow: 1px 1px 1px #eeeeee;
+ cursor: pointer;
+}
+.w2ui-calendar .w2ui-calendar-jump {
+ position: absolute;
+ top: 27px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ background-color: #FaFaFa;
+}
+.w2ui-calendar .w2ui-calendar-jump > :first-child {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ bottom: 0px;
+ width: 110px;
+ overflow: hidden;
+ padding-top: 5px;
+ border-right: 1px solid #c0c0c0;
+}
+.w2ui-calendar .w2ui-calendar-jump > :last-child {
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ width: 88px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding-top: 5px;
+ text-align: center;
+}
+.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-month,
+.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year {
+ display: inline-block;
+ padding: 5px 0px;
+ text-align: center;
+ float: left;
+ margin: 2px;
+ width: 50px;
+ cursor: default;
+ border: 1px solid transparent;
+ border-radius: 2px;
+}
+.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year {
+ float: none;
+ width: 95%;
+}
+.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-month:hover,
+.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year:hover {
+ border: 1px solid #cccccc;
+ color: #000000;
+ background-color: #efefef;
+}
+.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-month.selected,
+.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year.selected {
+ border: 1px solid #cccccc;
+ color: #000000;
+ background-color: #dadada;
+}
+.w2ui-calendar .w2ui-calendar-previous,
+.w2ui-calendar .w2ui-calendar-next {
+ width: 24px;
+ height: 20px;
+ color: #666666;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ padding: 2px 3px 1px 2px;
+ margin: -4px 0px 0px 0px;
+ cursor: default;
+}
+.w2ui-calendar .w2ui-calendar-previous:hover,
+.w2ui-calendar .w2ui-calendar-next:hover {
+ border: 1px solid #c0c0c0;
+ background-color: #efefef;
+}
+.w2ui-calendar .w2ui-calendar-previous > div,
+.w2ui-calendar .w2ui-calendar-next > div {
+ position: absolute;
+ border-left: 4px solid #888;
+ border-top: 4px solid #888;
+ border-right: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ width: 0px;
+ height: 0px;
+ padding: 0px;
+ margin: 3px 0px 0px 0px;
+}
+.w2ui-calendar .w2ui-calendar-previous {
+ float: left;
+}
+.w2ui-calendar .w2ui-calendar-previous > div {
+ -webkit-transform: rotate(-45deg);
+ -moz-transform: rotate(-45deg);
+ -ms-transform: rotate(-45deg);
+ -o-transform: rotate(-45deg);
+ transform: rotate(-45deg);
+ margin-left: 6px;
+}
+.w2ui-calendar .w2ui-calendar-next {
+ float: right;
+}
+.w2ui-calendar .w2ui-calendar-next > div {
+ -webkit-transform: rotate(135deg);
+ -moz-transform: rotate(135deg);
+ -ms-transform: rotate(135deg);
+ -o-transform: rotate(135deg);
+ transform: rotate(135deg);
+ margin-left: 2px;
+ margin-right: 2px;
+}
+.w2ui-calendar table.w2ui-calendar-days {
+ padding: 0px;
+}
+.w2ui-calendar table.w2ui-calendar-days td {
+ border: 1px solid #ffffff;
+ color: #000000;
+ background-color: #f9f9f9;
+ padding: 6px;
+ cursor: default;
+ text-align: right;
+}
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-saturday,
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-sunday {
+ border: 1px solid #ffffff;
+ color: #c8493b;
+ background-color: #f9f9f9;
+}
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-saturday:hover,
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-sunday:hover {
+ border: 1px solid #cccccc;
+ color: #000000;
+ background-color: #e9e9e9;
+}
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-saturday.w2ui-blocked,
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-sunday.w2ui-blocked {
+ text-decoration: line-through;
+ border: 1px solid #ffffff;
+ color: #cccccc;
+ background-color: #ffffff;
+}
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-today {
+ border: 1px solid #8cb067;
+ color: #000000;
+ background-color: #e2f7cd;
+}
+.w2ui-calendar table.w2ui-calendar-days td:hover {
+ border: 1px solid #cccccc;
+ color: #000000;
+ background-color: #e9e9e9;
+}
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-blocked {
+ text-decoration: line-through;
+ border: 1px solid #ffffff;
+ color: #cccccc;
+ background-color: #ffffff;
+}
+.w2ui-calendar table.w2ui-calendar-days td.w2ui-day-empty {
+ border: 1px solid #ffffff;
+ background-color: #fdfdfd;
+}
+.w2ui-calendar table.w2ui-calendar-days tr.w2ui-day-title td {
+ border: 1px solid #ffffff;
+ color: #808080;
+ background-color: #ffffff;
+ text-align: center;
+ padding: 6px;
+}
+/*
+* Time
+*/
+.w2ui-calendar-time {
+ padding: 5px;
+ cursor: default;
+}
+.w2ui-calendar-time td div {
+ padding: 7px 10px;
+ text-align: center;
+ border: 1px solid transparent;
+ white-space: nowrap;
+}
+.w2ui-calendar-time td:nth-child(even) {
+ background-color: #f6f6f6;
+}
+.w2ui-calendar-time td div:hover {
+ border: 1px solid #cccccc;
+ color: #000000;
+ background-color: #e9e9e9;
+}
+.w2ui-calendar-time td div.w2ui-blocked {
+ text-decoration: line-through;
+ border: 1px solid #ffffff;
+ color: #cccccc;
+ background-color: #ffffff;
+}
+.w2ui-select {
+ cursor: default;
+ color: black !important;
+ background-image: -webkit-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: -moz-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: -ms-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: -o-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+}
+/*
+* ENUM items
+*/
+.w2ui-list {
+ color: inherit;
+ position: absolute;
+ padding: 0px;
+ margin: 0px;
+ min-height: 25px;
+ overflow: auto;
+ border: 1px solid #c0c0c0;
+ border-radius: 3px;
+ font-size: 6px;
+ line-height: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+ background-color: #ffffff;
+}
+.w2ui-list input[type=text] {
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ -ms-box-shadow: none;
+ -o-box-shadow: none;
+ box-shadow: none;
+}
+.w2ui-list ul {
+ list-style-type: none;
+ background-color: black;
+ margin: 0px;
+ padding: 0px;
+}
+.w2ui-list ul li {
+ float: left;
+ margin: 2px 1px 0px 2px;
+ border-radius: 3px;
+ width: auto;
+ padding: 3px 10px 1px 7px;
+ border: 1px solid #88b0d6;
+ background-color: #eff3f5;
+ white-space: nowrap;
+ cursor: default;
+ font-family: verdana;
+ font-size: 11px;
+ line-height: 100%;
+ height: 20px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-list ul li:hover {
+ background-color: #d0dbe1;
+}
+.w2ui-list ul li:last-child {
+ border-radius: 0px;
+ border: 1px solid transparent;
+ background-color: transparent;
+}
+.w2ui-list ul li:last-child input {
+ padding: 1px;
+ padding-top: 0px;
+ margin: 0px;
+ border: 0px;
+ outline: none;
+ height: auto;
+ line-height: 100%;
+ font-size: inherit;
+ font-family: inherit;
+ background-color: transparent;
+}
+.w2ui-list ul li .w2ui-list-remove {
+ float: right;
+ width: 15px;
+ height: 14px;
+ margin: -1px -9px 0px 3px;
+ border-radius: 15px;
+}
+.w2ui-list ul li .w2ui-list-remove:hover {
+ background-color: #D77F7F;
+ color: white;
+}
+.w2ui-list ul li .w2ui-list-remove:before {
+ position: relative;
+ top: 0px;
+ padding: 0px;
+ margin: 0px;
+ left: 5px;
+ color: inherit;
+ opacity: 0.7;
+ text-shadow: inherit;
+ font-size: inherit;
+ font-variant: small-caps;
+ content: 'x';
+ line-height: 100%;
+}
+.w2ui-list ul li > span.file-size {
+ pointer-events: none;
+ color: #777;
+}
+.w2ui-list .w2ui-enum-placeholder {
+ display: inline;
+ position: absolute;
+ pointer-events: none;
+ color: #999;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-list.w2ui-file-dragover {
+ background-color: #E4FFDA;
+ border: 1px solid #93E07D;
+}
+/*************************************************
+* ---- Layout ----
+*/
+.w2ui-layout {
+ overflow: hidden !important;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-layout * {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-layout > div {
+ position: absolute;
+ overflow: hidden;
+ border: 0px;
+ margin: 0px;
+ padding: 0px;
+ outline: 0px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-layout > div .w2ui-panel {
+ display: none;
+ position: absolute;
+ z-index: 120;
+}
+.w2ui-layout > div .w2ui-panel .w2ui-panel-title {
+ padding: 5px;
+ background-image: -webkit-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -moz-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -ms-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -o-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: linear-gradient(#dae6f3, #c2d5ed);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffdae6f3', endColorstr='#ffc2d5ed', GradientType=0);
+ border: 1px solid #b9cee9;
+ border-bottom: 1px solid #99bbe8;
+}
+.w2ui-layout > div .w2ui-panel .w2ui-panel-tabs {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ right: 0px;
+ z-index: 2;
+ display: none;
+ overflow: hidden;
+ background-color: #fafafa;
+ padding: 4px 0px;
+}
+.w2ui-layout > div .w2ui-panel .w2ui-panel-tabs > .w2ui-tab.active {
+ background-color: #f5f6f7;
+}
+.w2ui-layout > div .w2ui-panel .w2ui-panel-toolbar {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ right: 0px;
+ z-index: 2;
+ display: none;
+ overflow: hidden;
+ background-color: #fafafa;
+ border-bottom: 1px solid silver;
+ padding: 4px;
+}
+.w2ui-layout > div .w2ui-panel .w2ui-panel-content {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ z-index: 1;
+ color: inherit;
+ background-color: #f5f6f7;
+}
+.w2ui-layout > div .w2ui-resizer {
+ display: none;
+ position: absolute;
+ z-index: 121;
+ background-color: transparent;
+}
+.w2ui-layout > div .w2ui-resizer:hover,
+.w2ui-layout > div .w2ui-resizer.active {
+ background-color: #d7e4f2;
+}
+/*************************************************
+* ---- Grid ----
+*/
+.w2ui-grid {
+ position: relative;
+ border: 1px solid #c0c0c0;
+ border-radius: 2px;
+ overflow: hidden !important;
+}
+.w2ui-grid > div {
+ position: absolute;
+ overflow: hidden;
+}
+.w2ui-grid .w2ui-grid-header {
+ position: absolute;
+ border-bottom: 1px solid #99bbe8 !important;
+ height: 28px;
+ overflow: hidden;
+ color: #444444;
+ font-size: 13px;
+ text-align: center;
+ padding: 7px;
+ background-image: -webkit-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -moz-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -ms-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: -o-linear-gradient(#dae6f3, #c2d5ed);
+ background-image: linear-gradient(#dae6f3, #c2d5ed);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffdae6f3', endColorstr='#ffc2d5ed', GradientType=0);
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+}
+.w2ui-grid .w2ui-grid-toolbar {
+ position: absolute;
+ border-bottom: 1px solid #c0c0c0;
+ background-color: #eaeaea;
+ height: 38px;
+ padding: 7px 3px 4px 3px;
+ margin: 0px;
+ box-shadow: 0px 1px 2px #dddddd;
+}
+.w2ui-grid .w2ui-toolbar-search {
+ width: 160px;
+ margin-right: 3px;
+}
+.w2ui-grid .w2ui-toolbar-search .w2ui-search-all {
+ outline: none !important;
+ width: 160px;
+ border-radius: 10px;
+ line-height: normal;
+ height: 22px;
+ border: 1px solid #b9b9b9;
+ color: #000000;
+ background-color: #ffffff;
+ padding: 3px 18px 3px 23px;
+ margin: 0px;
+}
+.w2ui-grid .w2ui-toolbar-search .w2ui-search-down {
+ position: absolute;
+ margin-top: -7px;
+ margin-left: 6px;
+}
+.w2ui-grid .w2ui-toolbar-search .w2ui-search-clear {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ margin-top: -8px;
+ margin-left: -20px;
+ border-radius: 15px;
+ cursor: default;
+}
+.w2ui-grid .w2ui-toolbar-search .w2ui-search-clear:hover {
+ background-color: #D77F7F;
+ color: white;
+}
+.w2ui-grid .w2ui-toolbar-search .w2ui-search-clear:before {
+ position: relative;
+ top: 1px;
+ left: 5px;
+ opacity: 0.6;
+ color: inherit;
+ text-shadow: inherit;
+ content: 'x';
+ cursor: default;
+}
+.w2ui-grid .w2ui-grid-body {
+ position: absolute;
+ overflow: hidden;
+ padding: 0px;
+ background-color: #ffffff;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+.w2ui-grid .w2ui-grid-body input,
+.w2ui-grid .w2ui-grid-body select,
+.w2ui-grid .w2ui-grid-body textarea {
+ user-select: text;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ -o-user-select: text;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-columns {
+ overflow: hidden;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ right: 0px;
+ box-shadow: 0px 1px 4px #dddddd;
+ height: auto;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-columns table {
+ height: auto;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-columns .w2ui-resizer {
+ position: absolute;
+ z-index: 1000;
+ display: block;
+ background-image: none;
+ background-color: rgba(0, 0, 0, 0);
+ /* needed for IE */
+ padding: 0px;
+ margin: 0px;
+ width: 6px;
+ height: 12px;
+ cursor: col-resize;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+ bottom: 0px;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd {
+ color: inherit;
+ background-color: #ffffff;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd:hover {
+ color: inherit;
+ background-color: #e6f0ff;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd.w2ui-empty-record:hover {
+ background-color: #ffffff;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even {
+ color: inherit;
+ background-color: #f3f6fa;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even:hover {
+ color: inherit;
+ background-color: #e6f0ff;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even.w2ui-empty-record:hover {
+ background-color: #f3f6fa;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-selected,
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-selected {
+ color: #000000 !important;
+ background-color: #b6d5ff !important;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded {
+ background-color: #CCDCF0 !important;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded1 {
+ height: 0px;
+ border-bottom: 1px solid #b2bac0;
+ background-color: #CCDCF0;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded1 > div {
+ height: 100%;
+ margin: 0px;
+ padding: 0px;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded2 {
+ height: 0px;
+ border-radius: 0px;
+ border-bottom: 1px solid #b2bac0;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded2 > div {
+ height: 0px;
+ border: 0px;
+ transition: height .3s, opacity .3s;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more {
+ border-top: 1px solid #d6d5d7;
+ cursor: pointer;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more > div {
+ text-align: center;
+ color: #777777;
+ background-color: rgba(233, 237, 243, 0.5);
+ padding: 10px 0px 15px 0px;
+ border-top: 1px solid #ffffff;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more > div:hover {
+ color: inherit;
+ background-color: #e6f0ff;
+}
+.w2ui-grid .w2ui-grid-body table {
+ border-spacing: 0px;
+ border-collapse: collapse;
+ table-layout: fixed;
+ width: 1px;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head {
+ margin: 0px;
+ padding: 0px;
+ border-right: 1px solid #c5c5c5;
+ border-bottom: 1px solid #c5c5c5;
+ color: #000000;
+ background-image: -webkit-linear-gradient(#f9f9f9, #e4e4e4);
+ background-image: -moz-linear-gradient(#f9f9f9, #e4e4e4);
+ background-image: -ms-linear-gradient(#f9f9f9, #e4e4e4);
+ background-image: -o-linear-gradient(#f9f9f9, #e4e4e4);
+ background-image: linear-gradient(#f9f9f9, #e4e4e4);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#ffe4e4e4', GradientType=0);
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head > div {
+ padding: 7px 3px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ position: relative;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head.w2ui-col-intersection {
+ border-right-color: #72b2ff;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head.w2ui-reorder-cols-head:hover {
+ cursor: move;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker {
+ padding: 0;
+ position: absolute;
+ height: 100%;
+ top: 0;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker.left {
+ left: 0;
+ margin-left: -5px;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker.right {
+ right: 0;
+ margin-right: -5px;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker .top-marker {
+ position: absolute;
+ top: 0;
+ height: 0;
+ width: 0;
+ border-top: 5px solid #72b2ff;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker .bottom-marker {
+ position: absolute;
+ bottom: 0;
+ height: 0;
+ width: 0;
+ border-bottom: 5px solid #72b2ff;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+}
+.w2ui-grid .w2ui-grid-body table td {
+ border-right: 1px solid #d6d5d7;
+ border-bottom: 0px solid #d6d5d7;
+ cursor: default;
+ overflow: hidden;
+}
+.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data {
+ margin: 0px;
+ padding: 0px;
+}
+.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data > div {
+ padding: 3px 3px 3px 3px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data > div.flexible-record {
+ height: auto;
+ overflow: visible;
+ white-space: normal;
+}
+.w2ui-grid .w2ui-grid-body table td:last-child {
+ border-right: 0px;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-col-number {
+ width: 34px;
+ color: #777777;
+ background-color: rgba(233, 237, 243, 0.5);
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-col-number div {
+ padding: 0px 7px 0px 3px;
+ text-align: right;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-col-select {
+ width: 26px;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-col-select div {
+ padding: 0px 0px;
+ text-align: center;
+ overflow: hidden;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-col-select div input[type=checkbox] {
+ margin-top: 2px;
+ position: relative;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-col-expand {
+ width: 26px;
+}
+.w2ui-grid .w2ui-grid-body table .w2ui-col-expand div {
+ padding: 0px 0px;
+ text-align: center;
+ font-weight: bold;
+}
+.w2ui-grid .w2ui-grid-body div.w2ui-col-header {
+ height: auto !important;
+ width: 100%;
+ overflow: hidden;
+ padding-right: 10px !important;
+}
+.w2ui-grid .w2ui-grid-body div.w2ui-col-header > div.w2ui-sort-up {
+ border: 4px solid transparent;
+ border-bottom: 5px solid #8D99A7;
+ margin-top: -2px;
+ margin-right: -7px;
+ float: right;
+}
+.w2ui-grid .w2ui-grid-body div.w2ui-col-header > div.w2ui-sort-down {
+ border: 4px solid transparent;
+ border-top: 5px solid #8D99A7;
+ margin-top: 2px;
+ margin-right: -7px;
+ float: right;
+}
+.w2ui-grid .w2ui-grid-body .w2ui-col-group {
+ text-align: center;
+}
+.w2ui-grid .w2ui-changed {
+ background: url(data:image/gif;base64,R0lGODlhCgAKAJEAALAABf///wAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAIPlI8Hy8mbxIsSUnup3rQAADs=) no-repeat top right;
+}
+.w2ui-grid .w2ui-editable {
+ overflow: hidden;
+ height: 100% !important;
+ margin: 0px !important;
+ padding: 0px !important;
+}
+.w2ui-grid .w2ui-editable input {
+ border: 0px;
+ border-radius: 0px;
+ margin: 0px;
+ padding: 4px 3px;
+ width: 100%;
+ height: 100%;
+}
+.w2ui-grid .w2ui-editable input.w2ui-select {
+ outline: none !important;
+ background: #fff;
+}
+.w2ui-grid .w2ui-grid-summary {
+ position: absolute;
+ box-shadow: 0px -1px 4px #aaaaaa;
+}
+.w2ui-grid .w2ui-grid-summary table {
+ color: inherit;
+}
+.w2ui-grid .w2ui-grid-summary table .w2ui-odd {
+ background-color: #eef5eb;
+}
+.w2ui-grid .w2ui-grid-summary table .w2ui-even {
+ background-color: #f8fff5;
+}
+.w2ui-grid .w2ui-grid-footer {
+ position: absolute;
+ margin: 0px;
+ padding: 0px;
+ text-align: center;
+ height: 24px;
+ overflow: hidden;
+ user-select: text;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ -o-user-select: text;
+ box-shadow: 0px -1px 4px #eeeeee;
+ color: #444444;
+ background-color: #f8f8f8;
+ border-top: 1px solid #dddddd;
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
+}
+.w2ui-grid .w2ui-grid-footer .w2ui-footer-left {
+ float: left;
+ padding-top: 5px;
+ padding-left: 5px;
+}
+.w2ui-grid .w2ui-grid-footer .w2ui-footer-right {
+ float: right;
+ padding-top: 5px;
+ padding-right: 5px;
+}
+.w2ui-grid .w2ui-grid-footer .w2ui-footer-center {
+ padding: 2px;
+ text-align: center;
+}
+.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav {
+ width: 110px;
+ margin: 0 auto;
+ padding: 0px;
+ text-align: center;
+}
+.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav input[type=text] {
+ padding: 1px 2px 2px 2px;
+ border-radius: 3px;
+ width: 40px;
+ text-align: center;
+}
+.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav a.w2ui-footer-btn {
+ display: inline-block;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 11px;
+ line-height: 16px;
+ padding: 1px 5px;
+ width: 30px;
+ height: 18px;
+ margin-top: -1px;
+ color: #000000;
+ background-color: transparent;
+}
+.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav a.w2ui-footer-btn:hover {
+ color: #000000;
+ background-color: #aec8ff;
+}
+/* SpeadSheet */
+.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd,
+.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even,
+.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd:hover,
+.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even:hover {
+ background-color: inherit;
+}
+.w2ui-ss .w2ui-grid-records table td {
+ border-right-width: 1px;
+ border-bottom: 1px solid #efefef;
+}
+.w2ui-ss .w2ui-grid-records table tr:first-child td {
+ border-bottom: 0px;
+}
+.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-selected,
+.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-selected {
+ background-color: #EEF4FE !important;
+}
+.w2ui-ss .w2ui-changed {
+ background: inherit;
+}
+.w2ui-ss .w2ui-grid-body .w2ui-selection {
+ position: absolute;
+ border: 2px solid #6299DA;
+ /* #457FC2; */
+ pointer-events: none;
+}
+.w2ui-ss .w2ui-grid-body .w2ui-selection .w2ui-selection-resizer {
+ cursor: crosshair;
+ position: absolute;
+ bottom: 0px;
+ right: 0px;
+ width: 6px;
+ height: 6px;
+ margin-right: -3px;
+ margin-bottom: -3px;
+ background-color: #457FC2;
+ border: 0.5px solid #fff;
+ outline: 1px solid white;
+ pointer-events: auto;
+}
+.w2ui-overlay .w2ui-select-field {
+ padding: 8px 5px;
+ cursor: default;
+}
+.w2ui-overlay .w2ui-select-field table {
+ font-size: 11px;
+ font-family: Verdana, Arial, sans-serif;
+ border-spacing: 0px;
+ border-collapse: border-collapse;
+}
+.w2ui-overlay .w2ui-select-field table tr:hover {
+ background-color: #b6d5ff;
+}
+.w2ui-overlay .w2ui-select-field table td:nth-child(1) {
+ padding: 3px 3px 3px 6px;
+}
+.w2ui-overlay .w2ui-select-field table td:nth-child(1) input {
+ margin: 3px 2px 2px 2px;
+}
+.w2ui-overlay .w2ui-select-field table td:nth-child(2) {
+ padding: 3px 15px 3px 3px;
+}
+.w2ui-overlay .w2ui-col-on-off {
+ padding: 4px 0px;
+}
+.w2ui-overlay .w2ui-col-on-off table {
+ border-spacing: 0px;
+ border-collapse: border-collapse;
+}
+.w2ui-overlay .w2ui-col-on-off table tr:hover {
+ background-color: #b6d5ff;
+}
+.w2ui-overlay .w2ui-col-on-off table td input[type=checkbox] {
+ margin: 3px 2px 2px 2px;
+}
+.w2ui-overlay .w2ui-col-on-off table td label {
+ display: block;
+ padding: 3px 0px;
+ padding-right: 10px;
+}
+.w2ui-overlay .w2ui-col-on-off table td:first-child {
+ padding: 4px 0px 4px 6px;
+}
+.w2ui-overlay .w2ui-col-on-off table td:last-child {
+ padding: 4px 6px 4px 0px;
+}
+.w2ui-overlay .w2ui-grid-searches {
+ text-align: left;
+ padding: 0px;
+ border-top: 0px;
+ background-color: #f7f6f0;
+}
+.w2ui-overlay .w2ui-grid-searches table {
+ padding: 4px;
+ padding-top: 12px;
+ border-collapse: border-collapse;
+}
+.w2ui-overlay .w2ui-grid-searches table td {
+ padding: 4px;
+ /* for IE */
+}
+.w2ui-overlay .w2ui-grid-searches table td.close-btn {
+ width: 20px;
+ padding-right: 20px;
+}
+.w2ui-overlay .w2ui-grid-searches table td.close-btn button {
+ min-width: 24px;
+ height: 24px;
+ padding-top: 6px !important;
+}
+.w2ui-overlay .w2ui-grid-searches table td.caption {
+ text-align: right;
+ padding-right: 5px;
+ border-right: 1px solid #e8e8e3;
+}
+.w2ui-overlay .w2ui-grid-searches table td.operator {
+ text-align: left;
+ padding: 0px 10px;
+ padding-right: 5px;
+ border-right: 1px solid #e8e8e3;
+}
+.w2ui-overlay .w2ui-grid-searches table td.operator select {
+ width: 100%;
+ color: black;
+ padding: 0px 15px 0px 5px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ -o-appearance: none;
+ background-image: -webkit-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: -moz-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: -ms-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: -o-linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+ background-image: linear-gradient(top,#ffffff 20%,#f6f6f6 50%,#eeeeee 52%,#f4f4f4 100%);
+}
+.w2ui-overlay .w2ui-grid-searches table td.operator select::-ms-expand {
+ display: none;
+}
+.w2ui-overlay .w2ui-grid-searches table td.value {
+ padding-right: 5px;
+ padding-left: 5px;
+}
+.w2ui-overlay .w2ui-grid-searches table td.value input[type=text] {
+ border-radius: 3px;
+ padding: 3px;
+ margin-right: 3px;
+ height: 23px;
+}
+.w2ui-overlay .w2ui-grid-searches table td.value select {
+ padding: 3px;
+ margin-right: 3px;
+ height: 23px;
+}
+.w2ui-overlay .w2ui-grid-searches table td.actions {
+ border-right: 0px;
+}
+.w2ui-overlay .w2ui-grid-searches table td.actions > div {
+ margin: -7px;
+ margin-top: 15px;
+ padding: 13px 0px;
+ text-align: center;
+ background-color: #efefe9;
+ border-top: 1px solid #e8e8e3;
+}
+/*************************************************
+* ---- Popup ----
+*/
+.w2ui-popup {
+ position: fixed;
+ z-index: 1600;
+ overflow: hidden;
+ font-family: Verdana, Arial, sans-serif;
+ border-radius: 6px;
+ padding: 0px;
+ margin: 0px;
+ border: 1px solid #777777;
+ background-color: #eeeeee;
+ box-shadow: 0px 0px 25px #555555;
+}
+.w2ui-popup,
+.w2ui-popup * {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-popup .w2ui-msg-title {
+ padding: 6px;
+ border-radius: 6px 6px 0px 0px;
+ background-image: -webkit-linear-gradient(#ececec, #dfdfdf);
+ background-image: -moz-linear-gradient(#ececec, #dfdfdf);
+ background-image: -ms-linear-gradient(#ececec, #dfdfdf);
+ background-image: -o-linear-gradient(#ececec, #dfdfdf);
+ background-image: linear-gradient(#ececec, #dfdfdf);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffececec', endColorstr='#ffdfdfdf', GradientType=0);
+ border-bottom: 2px solid #bfbfbf;
+ position: absolute;
+ overflow: hidden;
+ height: 32px;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+ text-overflow: ellipsis;
+ text-align: center;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+ cursor: move;
+ font-size: 15px;
+ color: #555555;
+ z-index: 300;
+}
+.w2ui-popup .w2ui-msg-button {
+ float: right;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ overflow: hidden;
+ padding: 0px;
+ margin: 0px 3px 0px 0px;
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAj1JREFUeNrslr9rFFEQxz/zZi/qxSgW2lsqkiYoBku5Ro1o4UFKEYkgSaxSCLYqdv5IEVPYCMJJwERWrK0CKhoQ8hdobQTjXW7njcXlYnLunQQu0YDTLOy+Nzvfme98Z8Td2ckW2OGWdMvRvYfT/RGfBPoBBVpLK0AEPgVkdGL06vt/CoB5nBaRE8AXYKXNsQIwaB4fAwOtH+88mn4m7ifN4vUYebWBKkFKqjIV3N9NjI2Uuw5ARI45fBanH+F77iFnN8JHETmS68P9NHBQNTwHL8foaSN4SqoyA/SZyL4tqQAQBVYCLOFYlNxmq0WorVLpN9Oe5LKt1CsgRVWpAOfB66phBuhTkepSdfnKVjaxNJMSWn/iawmTtpeDp6pWBpaBoqrMqoYU6AOqIbFhxGa3R4V8nfNNKLUESzXJhoCvQC+wF/gW1C5IiC+2XUbD5jA3rd4C26NR3945IA2iRzqRJgdElJJlSQocAKrAD2A/6Ev3cLajjN59MDWHyKl2voOI1zKbv3Xj2lCHJFoz+LXuBoIAjnUklEvJrDDT5LwmdhG8blkyBxRjXSu4loE0X4VEznXKV3SnoOFMB7YUolBcbcKNdxuPXUBPu8pbLXsK0ghebVjEXgNoYmXLtGLuxd6ePU+AQ20AaIrb4DpFycmSv81/7YsiMgAstB1kQgE47O4LuQmCNwGOB7VxCb/URsRSTbhkmU4ifGiZHd1Z5m7fnxoIQSaBo39YJRZj9LGb4yPzXWm1/9voX7afAwAC5tacDTA2XgAAAABJRU5ErkJggg==) no-repeat center left;
+ background-position: 0px 0px;
+ color: transparent !important;
+ border-radius: 3px;
+ border: 1px solid transparent;
+}
+.w2ui-popup .w2ui-msg-close {
+ margin-top: 0px;
+ background-position: -32px 0px;
+}
+.w2ui-popup .w2ui-msg-close:hover {
+ background-color: #cccccc;
+ border: 1px solid #aaaaaa;
+}
+.w2ui-popup .w2ui-msg-max {
+ background-position: -16px 0px;
+}
+.w2ui-popup .w2ui-msg-max:hover {
+ background-color: #cccccc;
+ border: 1px solid #aaaaaa;
+}
+.w2ui-popup .w2ui-box1,
+.w2ui-popup .w2ui-box2 {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ top: 32px;
+ bottom: 55px;
+ z-index: 100;
+}
+.w2ui-popup .w2ui-msg-body {
+ font-size: 13px;
+ line-height: 130%;
+ padding: 0px 7px 7px 7px;
+ color: #000000;
+ background-color: #eeeeee;
+ position: absolute;
+ overflow: auto;
+ width: 100%;
+ height: 100%;
+}
+.w2ui-popup .w2ui-popup-message {
+ position: absolute;
+ z-index: 250;
+ background-color: #f9f9f9;
+ border: 1px solid #999999;
+ box-shadow: 0px 0px 15px #aaaaaa;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+ border-top: 0px;
+ border-radius: 0px 0px 6px 6px;
+ overflow: auto;
+}
+.w2ui-popup .w2ui-msg-buttons {
+ padding: 12px;
+ border-radius: 0px 0px 6px 6px;
+ border-top: 1px solid #d5d8d8;
+ background-color: #f1f1f1;
+ text-align: center;
+ position: absolute;
+ overflow: hidden;
+ height: 52px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ z-index: 200;
+}
+.w2ui-popup .w2ui-msg-no-title {
+ border-top-left-radius: 6px;
+ border-top-right-radius: 6px;
+ top: 0px !important;
+}
+.w2ui-popup .w2ui-msg-no-buttons {
+ border-bottom-left-radius: 6px;
+ border-bottom-right-radius: 6px;
+ bottom: 0px !important;
+}
+/*************************************************
+* ---- Sidebar ----
+*/
+.w2ui-sidebar {
+ cursor: default;
+ overflow: hidden !important;
+ background-color: #edf1f6 !important;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-sidebar * {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-sidebar > div {
+ position: relative;
+ overflow: hidden;
+}
+.w2ui-sidebar .w2ui-sidebar-top {
+ position: absolute;
+ z-index: 2;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+}
+.w2ui-sidebar .w2ui-sidebar-bottom {
+ position: absolute;
+ z-index: 2;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
+}
+.w2ui-sidebar .w2ui-sidebar-div {
+ position: absolute;
+ z-index: 1;
+ overflow: auto;
+ top: 0px;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
+ padding: 2px 0px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+.w2ui-sidebar .w2ui-sidebar-div table {
+ width: 100%;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node {
+ background-color: #edf1f6;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ margin: 0px;
+ padding: 1px 0px;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node table {
+ pointer-events: none;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-caption,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image > span,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node td.w2ui-node-dots {
+ color: #000000;
+ text-shadow: 0px 0px 0px #ffffff;
+ pointer-events: none;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-caption:hover,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image:hover,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image > span:hover,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node td.w2ui-node-dots:hover {
+ color: inherit;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node:hover {
+ border-top: 1px solid #f9f9f9;
+ border-bottom: 1px solid #f9f9f9;
+ background-color: #d7e1ef;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image {
+ width: 22px;
+ text-align: center;
+ pointer-events: none;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image > span {
+ color: #516173 !important;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover {
+ background-image: -webkit-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -moz-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -ms-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -o-linear-gradient(#69b1e0, #4a96d3);
+ background-image: linear-gradient(#69b1e0, #4a96d3);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff69b1e0', endColorstr='#ff4a96d3', GradientType=0);
+ border-top: 1px solid #5295cd;
+ border-bottom: 1px solid #2661a6;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected .w2ui-node-caption,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover .w2ui-node-caption,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected .w2ui-node-image,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover .w2ui-node-image,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected .w2ui-node-image > span,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover .w2ui-node-image > span,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected td.w2ui-node-dots,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover td.w2ui-node-dots {
+ color: #ffffff !important;
+ text-shadow: 1px 1px 2px #666666 !important;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover {
+ background: transparent !important;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled .w2ui-node-caption,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover .w2ui-node-caption,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled .w2ui-node-image,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover .w2ui-node-image,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled .w2ui-node-image > span,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover .w2ui-node-image > span,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled td.w2ui-node-dots,
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover td.w2ui-node-dots {
+ opacity: 0.4;
+ filter: alpha(opacity=40);
+ color: #000000 !important;
+ text-shadow: 0px 0px 0px #ffffff !important;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-caption {
+ white-space: nowrap;
+ padding: 5px 0px 5px 3px;
+ margin: 1px 0px 1px 22px;
+ position: relative;
+ z-index: 1;
+ font-size: 12px;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-group {
+ white-space: nowrap;
+ overflow: hidden;
+ padding: 10px 0px 10px 10px;
+ margin: 0px;
+ cursor: default;
+ color: #868b92;
+ background-color: transparent;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-group :nth-child(1) {
+ /* show / hide link */
+ margin-right: 10px;
+ float: right;
+ color: transparent;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-group :nth-child(2) {
+ /* title text */
+ font-weight: normal;
+ text-transform: uppercase;
+}
+.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-sub {
+ overflow: hidden;
+}
+.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-dots {
+ width: 18px;
+ padding: 0px 0px 1px 7px;
+ text-align: center;
+}
+.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-dots .w2ui-expand {
+ width: 16px;
+ margin-top: -3px;
+ pointer-events: auto;
+}
+.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data {
+ padding: 1px 1px 3px 1px;
+}
+.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-image {
+ padding: 3px 0px 0px 0px;
+ float: left;
+}
+.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-image > span {
+ font-size: 16px;
+ color: #000000;
+ text-shadow: 0px 0px 0px #ffffff;
+}
+.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-image.w2ui-icon {
+ margin-top: 3px;
+}
+.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-count {
+ float: right;
+ border: 1px solid #9da4af;
+ border-radius: 20px;
+ width: auto;
+ height: 18px;
+ padding: 2px 7px;
+ margin: 3px 4px -2px 0;
+ background-color: #e7f0fc;
+ color: #667274;
+ box-shadow: 0 0 2px #ffffff;
+ text-shadow: 1px 1px 1px #e6e6e6;
+ position: relative;
+ z-index: 2;
+}
+/*************************************************
+* ---- Tabs ----
+*/
+.w2ui-tabs {
+ cursor: default;
+ overflow: hidden !important;
+ background-color: #fafafa;
+ padding: 3px 0px;
+ padding-bottom: 0px !important;
+}
+.w2ui-tabs table {
+ border-bottom: 1px solid silver;
+ padding: 0px 7px;
+}
+.w2ui-tabs .w2ui-tab {
+ padding: 6px 20px;
+ text-align: center;
+ color: #000000;
+ background-color: transparent;
+ border: 1px solid #c0c0c0;
+ border-bottom: 1px solid silver;
+ white-space: nowrap;
+ margin: 1px 1px -1px 0px;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ cursor: default;
+}
+.w2ui-tabs .w2ui-tab.active {
+ color: #000000;
+ background-color: #ffffff;
+ border: 1px solid #c0c0c0;
+ border-bottom: 1px solid transparent;
+}
+.w2ui-tabs .w2ui-tab.closable {
+ padding: 6px 28px 6px 20px;
+}
+.w2ui-tabs .w2ui-tab-close {
+ color: #555;
+ text-shadow: 1px 1px 1px #bbb;
+ float: right;
+ margin: 6px 4px 0px 0px;
+ padding: 0px 0px 0px 5px;
+ width: 16px;
+ height: 16px;
+ opacity: 0.9;
+ border: 0px;
+ border-top: 3px solid transparent;
+ border-radius: 9px;
+}
+.w2ui-tabs .w2ui-tab-close:hover {
+ background-color: #D77F7F;
+ color: white;
+}
+.w2ui-tabs .w2ui-tab-close:before {
+ position: relative;
+ top: -2px;
+ left: 0px;
+ opacity: 0.6;
+ color: inherit;
+ text-shadow: inherit;
+ content: 'x';
+}
+/*************************************************
+* ---- Toolbar ----
+*/
+.w2ui-toolbar {
+ margin: 0px;
+ padding: 2px;
+ outline: 0px;
+ background-color: #efefef;
+ overflow: hidden !important;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+.w2ui-toolbar .disabled {
+ opacity: 0.3;
+ filter: alpha(opacity=30);
+}
+.w2ui-toolbar table {
+ table-layout: auto !important;
+}
+.w2ui-toolbar table td {
+ border: 0px !important;
+}
+.w2ui-toolbar table.w2ui-button {
+ margin: 0px 1px;
+ border-radius: 4px;
+ height: 24px;
+ border: 1px solid transparent;
+ background-color: transparent;
+}
+.w2ui-toolbar table.w2ui-button .w2ui-tb-image {
+ width: 16px;
+ height: 16px;
+ padding: 0px;
+ margin: 2px 4px 3px 3px !important;
+ border: 0px !important;
+ text-align: center;
+}
+.w2ui-toolbar table.w2ui-button .w2ui-tb-image > span {
+ font-size: 15px;
+ margin-top: 3px;
+ display: block;
+ color: #8d99a7;
+}
+.w2ui-toolbar table.w2ui-button .w2ui-tb-caption {
+ color: #000000;
+ padding: 0px 4px 0px 2px;
+}
+.w2ui-toolbar table.w2ui-button .w2ui-tb-count {
+ padding: 0px 4px 0px 0px;
+}
+.w2ui-toolbar table.w2ui-button .w2ui-tb-count > span {
+ border: 1px solid #9da4af;
+ border-radius: 20px;
+ width: auto;
+ height: 18px;
+ padding: 2px 7px;
+ background-color: #e7f0fc;
+ color: #667274;
+ box-shadow: 0 0 2px #ffffff;
+ text-shadow: 1px 1px 1px #e6e6e6;
+}
+.w2ui-toolbar table.w2ui-button .w2ui-tb-down {
+ padding: 3px;
+}
+.w2ui-toolbar table.w2ui-button .w2ui-tb-down > div {
+ border: 4px solid transparent;
+ border-top: 5px solid #8D99A7;
+ margin-top: 5px;
+}
+.w2ui-toolbar table.w2ui-button.over {
+ border: 1px solid #cccccc;
+ background-color: #eeeeee;
+}
+.w2ui-toolbar table.w2ui-button.over .w2ui-tb-caption {
+ color: #000000;
+}
+.w2ui-toolbar table.w2ui-button.down {
+ border: 1px solid #aaaaaa;
+ background-color: #dddddd;
+}
+.w2ui-toolbar table.w2ui-button.down .w2ui-tb-caption {
+ color: #666666;
+}
+.w2ui-toolbar table.w2ui-button.checked {
+ border: 1px solid #aaaaaa;
+ background-color: #ffffff;
+}
+.w2ui-toolbar table.w2ui-button.checked .w2ui-tb-caption {
+ color: #000000;
+}
+.w2ui-toolbar table.w2ui-button table {
+ height: 17px;
+ border-radius: 4px;
+ cursor: default;
+}
+.w2ui-toolbar .w2ui-break {
+ background-image: -webkit-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, #999999 40%, #999999 60%, rgba(153, 153, 153, 0.1) 100%);
+ background-image: -moz-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, #999999 40%, #999999 60%, rgba(153, 153, 153, 0.1) 100%);
+ background-image: -ms-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, #999999 40%, #999999 60%, rgba(153, 153, 153, 0.1) 100%);
+ background-image: -o-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, #999999 40%, #999999 60%, rgba(153, 153, 153, 0.1) 100%);
+ background-image: linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, #999999 40%, #999999 60%, rgba(153, 153, 153, 0.1) 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff999999', endColorstr='#ff999999', GradientType=0);
+ width: 1px !important;
+ height: 22px;
+ padding: 0px;
+ margin: 0px 6px;
+}
+.w2ui-listview {
+ overflow: auto !important;
+ background-color: #ffffff !important;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-listview * {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.w2ui-listview > ul {
+ list-style-type: none;
+ margin: 0;
+ cursor: default;
+}
+.w2ui-listview > ul > li {
+ display: inline-block;
+ vertical-align: top;
+ overflow: hidden;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+ border: 1px solid transparent;
+ border-radius: 4px;
+}
+.w2ui-listview > ul > li.w2ui-focused {
+ border: 1px solid #2661a6;
+}
+.w2ui-listview > ul > li.w2ui-selected {
+ border: 1px solid #2661a6;
+}
+.w2ui-listview > ul > li.w2ui-selected,
+.w2ui-listview > ul > li.w2ui-selected.hover {
+ background-image: -webkit-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -moz-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -ms-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -o-linear-gradient(#69b1e0, #4a96d3);
+ background-image: linear-gradient(#69b1e0, #4a96d3);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff69b1e0', endColorstr='#ff4a96d3', GradientType=0);
+}
+.w2ui-listview > ul > li.w2ui-selected > div > div.caption,
+.w2ui-listview > ul > li.w2ui-selected.hover > div > div.caption {
+ color: #ffffff;
+}
+.w2ui-listview > ul > li.w2ui-selected > div > div.description,
+.w2ui-listview > ul > li.w2ui-selected.hover > div > div.description {
+ color: #dddddd;
+}
+.w2ui-listview > ul > li.w2ui-selected > div > div.extra > div > div,
+.w2ui-listview > ul > li.w2ui-selected.hover > div > div.extra > div > div {
+ color: #dddddd;
+}
+.w2ui-listview > ul > li.hover {
+ background-color: #d7e1ef;
+ border: 1px solid #2661a6;
+}
+.w2ui-listview > ul > li div {
+ vertical-align: middle;
+}
+.w2ui-listview > ul > li > div > div.caption {
+ display: block;
+ text-align: center;
+ word-wrap: break-word;
+ max-height: 50px;
+ color: #000000;
+ font-size: 12px;
+}
+.w2ui-listview > ul > li > div > div.description {
+ display: none;
+ text-align: left;
+ color: #777777;
+ font-size: 12px;
+}
+.w2ui-listview > ul > li > div > div.extra {
+ display: none;
+}
+.w2ui-listview > ul > li > div > div.extra > div > div {
+ color: #777777;
+}
+.w2ui-icon-small > ul {
+ padding: 1px 0px 0px 1px;
+}
+.w2ui-icon-small > ul > li {
+ margin: 0px 1px 1px 0px;
+ padding: 2px;
+ width: 250px;
+ white-space: nowrap;
+}
+.w2ui-icon-small > ul > li > div > div.w2ui-listview-img {
+ display: inline-block;
+ width: 26px;
+ height: 22px;
+ font-size: 21px;
+ margin-right: 2px;
+}
+.w2ui-icon-small > ul > li > div > div.caption {
+ display: inline-block;
+}
+.w2ui-icon-medium > ul {
+ padding: 4px 0px 0px 4px;
+}
+.w2ui-icon-medium > ul > li {
+ margin: 0px 4px 4px 0px;
+ padding: 4px;
+ width: 100px;
+}
+.w2ui-icon-medium > ul > li > div > div.w2ui-listview-img {
+ display: block;
+ width: 92px;
+ height: 60px;
+ font-size: 57px;
+ margin-left: auto;
+ margin-right: auto;
+ background-position: center;
+}
+.w2ui-icon-large > ul {
+ padding: 4px 0px 0px 4px;
+}
+.w2ui-icon-large > ul > li {
+ margin: 0px 4px 4px 0px;
+ padding: 4px;
+ width: 160px;
+}
+.w2ui-icon-large > ul > li > div > div.w2ui-listview-img {
+ display: block;
+ width: 152px;
+ height: 120px;
+ font-size: 114px;
+ margin-left: auto;
+ margin-right: auto;
+ background-position: center;
+}
+.w2ui-icon-tile > ul {
+ padding: 1px 0px 0px 1px;
+}
+.w2ui-icon-tile > ul > li {
+ margin: 0px 1px 1px 0px;
+ padding: 4px;
+ width: 250px;
+ white-space: nowrap;
+}
+.w2ui-icon-tile > ul > li > div > div.w2ui-listview-img {
+ display: inline-block;
+ width: 72px;
+ height: 60px;
+ font-size: 57px;
+ float: left;
+ margin-right: 4px;
+}
+.w2ui-icon-tile > ul > li > div > div.caption {
+ text-align: left;
+}
+.w2ui-icon-tile > ul > li > div > div.description {
+ display: block;
+}
+.w2ui-table > ul {
+ padding: 0;
+}
+.w2ui-table > ul > li {
+ width: 100%;
+ padding: 2px;
+ border-radius: 0px;
+ border-bottom: 1px dotted lightgray;
+}
+.w2ui-table > ul > li > div {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+.w2ui-table > ul > li > div > div.w2ui-listview-img {
+ display: inline-block;
+ width: 38px;
+ height: 32px;
+ font-size: 31px;
+ margin-right: 2px;
+}
+.w2ui-table > ul > li > div > div.caption {
+ display: inline-block;
+}
+.w2ui-table > ul > li > div > div.extra {
+ display: inline-block;
+ position: absolute;
+ right: 0;
+ height: 100%;
+ background-color: #ffffff;
+}
+.w2ui-table > ul > li > div > div.extra > div:before {
+ display: inline-block;
+ height: 100%;
+ width: 0;
+ content: '';
+ vertical-align: middle;
+}
+.w2ui-table > ul > li > div > div.extra > div {
+ display: inline;
+}
+.w2ui-table > ul > li > div > div.extra > div > div {
+ display: inline-block;
+ font-size: 12px;
+}
+.w2ui-table > ul > li.w2ui-selected div.extra,
+.w2ui-table > ul > li.w2ui-selected.hover div.extra {
+ background-image: -webkit-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -moz-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -ms-linear-gradient(#69b1e0, #4a96d3);
+ background-image: -o-linear-gradient(#69b1e0, #4a96d3);
+ background-image: linear-gradient(#69b1e0, #4a96d3);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff69b1e0', endColorstr='#ff4a96d3', GradientType=0);
+}
+.w2ui-table > ul > li.hover div.extra {
+ background-color: #d7e1ef;
+}
+.w2ui-listview > ul > li div.icon-none {
+ border: 1px solid rgba(102, 102, 102, 0.35);
+}
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.js b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.js
new file mode 100644
index 00000000..afdc6a00
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.js
@@ -0,0 +1,13617 @@
+/* w2ui 1.4 (c) http://w2ui.com, vitmalina@gmail.com */
+var w2ui = w2ui || {};
+var w2obj = w2obj || {}; // expose object to be able to overwrite default functions
+
+/************************************************
+* Library: Web 2.0 UI for jQuery
+* - Following objects are defines
+* - w2ui - object that will contain all widgets
+* - w2obj - object with widget prototypes
+* - w2utils - basic utilities
+* - $().w2render - common render
+* - $().w2destroy - common destroy
+* - $().w2marker - marker plugin
+* - $().w2tag - tag plugin
+* - $().w2overlay - overlay plugin
+* - $().w2menu - menu plugin
+* - w2utils.event - generic event object
+* - w2utils.keyboard - object for keyboard navigation
+* - Dependencies: jQuery
+*
+* == NICE TO HAVE ==
+* - date has problems in FF new Date('yyyy-mm-dd') breaks
+* - bug: w2utils.formatDate('2011-31-01', 'yyyy-dd-mm'); - wrong foratter
+* - overlay should be displayed where more space (on top or on bottom)
+* - write and article how to replace certain framework functions
+* - format date and time is buggy
+* - onComplete should pass widget as context (this)
+* - add maxHeight for the w2menu
+* - user localization from another lib (make it generic), https://github.com/jquery/globalize#readme
+* - hidden and disabled in menus
+* - isTime should support seconds
+* - TEST On IOS
+*
+************************************************/
+
+var w2utils = (function () {
+ var tmp = {}; // for some temp variables
+ var obj = {
+ version : '1.4.0',
+ settings : {
+ "locale" : "en-us",
+ "date_format" : "m/d/yyyy",
+ "date_display" : "Mon d, yyyy",
+ "time_format" : "h12",
+ "currencyPrefix" : "$",
+ "currencySuffix" : "",
+ "currencyPrecision" : 2,
+ "groupSymbol" : ",",
+ "shortmonths" : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+ "fullmonths" : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+ "shortdays" : ["M", "T", "W", "T", "F", "S", "S"],
+ "fulldays" : ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
+ "dataType" : 'HTTP', // can be HTTP, RESTFULL, JSON (case sensative)
+ "phrases" : {} // empty object for english phrases
+ },
+ isInt : isInt,
+ isFloat : isFloat,
+ isMoney : isMoney,
+ isHex : isHex,
+ isAlphaNumeric : isAlphaNumeric,
+ isEmail : isEmail,
+ isDate : isDate,
+ isTime : isTime,
+ age : age,
+ date : date,
+ size : size,
+ formatNumber : formatNumber,
+ formatDate : formatDate,
+ formatTime : formatTime,
+ formatDateTime : formatDateTime,
+ stripTags : stripTags,
+ encodeTags : encodeTags,
+ escapeId : escapeId,
+ base64encode : base64encode,
+ base64decode : base64decode,
+ transition : transition,
+ lock : lock,
+ unlock : unlock,
+ lang : lang,
+ locale : locale,
+ getSize : getSize,
+ scrollBarSize : scrollBarSize,
+ checkName : checkName,
+ checkUniqueId : checkUniqueId,
+ parseRoute : parseRoute,
+ // some internal variables
+ isIOS : ((navigator.userAgent.toLowerCase().indexOf('iphone') != -1 ||
+ navigator.userAgent.toLowerCase().indexOf('ipod') != -1 ||
+ navigator.userAgent.toLowerCase().indexOf('ipad') != -1)
+ ? true : false),
+ isIE : ((navigator.userAgent.toLowerCase().indexOf('msie') != -1 ||
+ navigator.userAgent.toLowerCase().indexOf('trident') != -1 )
+ ? true : false)
+ };
+ return obj;
+
+ function isInt (val) {
+ var re = /^[-+]?[0-9]+$/;
+ return re.test(val);
+ }
+
+ function isFloat (val) {
+ return (typeof val === 'number' || (typeof val === 'string' && val !== '')) && !isNaN(Number(val));
+ }
+
+ function isMoney (val) {
+ var se = w2utils.settings;
+ var re = new RegExp('^'+ (se.currencyPrefix ? '\\' + se.currencyPrefix + '?' : '') +'[-+]?[0-9]*[\.]?[0-9]+'+ (se.currencySuffix ? '\\' + se.currencySuffix + '?' : '') +'$', 'i');
+ if (typeof val === 'string') {
+ val = val.replace(new RegExp(se.groupSymbol, 'g'), '');
+ }
+ if (typeof val === 'object' || val === '') return false;
+ return re.test(val);
+ }
+
+ function isHex (val) {
+ var re = /^[a-fA-F0-9]+$/;
+ return re.test(val);
+ }
+
+ function isAlphaNumeric (val) {
+ var re = /^[a-zA-Z0-9_-]+$/;
+ return re.test(val);
+ }
+
+ function isEmail (val) {
+ var email = /^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
+ return email.test(val);
+ }
+
+ function isDate (val, format, retDate) {
+ if (!val) return false;
+
+ var dt = 'Invalid Date';
+ var month, day, year;
+
+ if (format == null) format = w2utils.settings.date_format;
+
+ if (typeof val.getUTCFullYear === 'function' && typeof val.getUTCMonth === 'function' && typeof val.getUTCDate === 'function') {
+ year = val.getUTCFullYear();
+ month = val.getUTCMonth();
+ day = val.getUTCDate();
+ } else if (typeof val.getFullYear === 'function' && typeof val.getMonth === 'function' && typeof val.getDate === 'function') {
+ year = val.getFullYear();
+ month = val.getMonth();
+ day = val.getDate();
+ } else {
+ val = String(val);
+ // convert month formats
+ if (RegExp('mon', 'ig').test(format)) {
+ format = format.replace(/month/ig, 'm').replace(/mon/ig, 'm').replace(/dd/ig, 'd').replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase();
+ val = val.replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase();
+ for (var m = 0, len = w2utils.settings.fullmonths.length; m < len; m++) {
+ var t = w2utils.settings.fullmonths[m];
+ val = val.replace(RegExp(t, 'ig'), (parseInt(m) + 1)).replace(RegExp(t.substr(0, 3), 'ig'), (parseInt(m) + 1));
+ }
+ }
+ // format date
+ var tmp = val.replace(/-/g, '/').replace(/\./g, '/').toLowerCase().split('/');
+ var tmp2 = format.replace(/-/g, '/').replace(/\./g, '/').toLowerCase();
+ if (tmp2 === 'mm/dd/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2]; }
+ if (tmp2 === 'm/d/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2]; }
+ if (tmp2 === 'dd/mm/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2]; }
+ if (tmp2 === 'd/m/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2]; }
+ if (tmp2 === 'yyyy/dd/mm') { month = tmp[2]; day = tmp[1]; year = tmp[0]; }
+ if (tmp2 === 'yyyy/d/m') { month = tmp[2]; day = tmp[1]; year = tmp[0]; }
+ if (tmp2 === 'yyyy/mm/dd') { month = tmp[1]; day = tmp[2]; year = tmp[0]; }
+ if (tmp2 === 'yyyy/m/d') { month = tmp[1]; day = tmp[2]; year = tmp[0]; }
+ if (tmp2 === 'mm/dd/yy') { month = tmp[0]; day = tmp[1]; year = tmp[2]; }
+ if (tmp2 === 'm/d/yy') { month = tmp[0]; day = tmp[1]; year = parseInt(tmp[2]) + 1900; }
+ if (tmp2 === 'dd/mm/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900; }
+ if (tmp2 === 'd/m/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900; }
+ if (tmp2 === 'yy/dd/mm') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900; }
+ if (tmp2 === 'yy/d/m') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900; }
+ if (tmp2 === 'yy/mm/dd') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900; }
+ if (tmp2 === 'yy/m/d') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900; }
+ }
+ if (!isInt(year)) return false;
+ if (!isInt(month)) return false;
+ if (!isInt(day)) return false;
+ year = +year;
+ month = +month;
+ day = +day;
+ dt = new Date(year, month - 1, day);
+ // do checks
+ if (month == null) return false;
+ if (dt === 'Invalid Date') return false;
+ if ((dt.getMonth() + 1 !== month) || (dt.getDate() !== day) || (dt.getFullYear() !== year)) return false;
+ if (retDate === true) return dt; else return true;
+ }
+
+ function isTime (val, retTime) {
+ // Both formats 10:20pm and 22:20
+ if (val == null) return false;
+ var max, pm;
+ // -- process american format
+ val = String(val);
+ val = val.toUpperCase();
+ pm = val.indexOf('PM') >= 0;
+ var ampm = (pm || val.indexOf('AM') >= 0);
+ if (ampm) max = 12; else max = 24;
+ val = val.replace('AM', '').replace('PM', '');
+ val = $.trim(val);
+ // ---
+ var tmp = val.split(':');
+ var h = parseInt(tmp[0] || 0), m = parseInt(tmp[1] || 0);
+ // accept edge case: 3PM is a good timestamp, but 3 (without AM or PM) is NOT:
+ if ((!ampm || tmp.length !== 1) && tmp.length !== 2) { return false; }
+ if (tmp[0] === '' || h < 0 || h > max || !this.isInt(tmp[0]) || tmp[0].length > 2) { return false; }
+ if (tmp.length === 2 && (tmp[1] === '' || m < 0 || m > 59 || !this.isInt(tmp[1]) || tmp[1].length !== 2)) { return false; }
+ // check the edge cases: 12:01AM is ok, as is 12:01PM, but 24:01 is NOT ok while 24:00 is (midnight; equivalent to 00:00).
+ // meanwhile, there is 00:00 which is ok, but 0AM nor 0PM are okay, while 0:01AM and 0:00AM are.
+ if (!ampm && max === h && m !== 0) { return false; }
+ if (ampm && tmp.length === 1 && h === 0) { return false; }
+
+ if (retTime === true) {
+ if (pm) h += 12;
+ return {
+ hours: h,
+ minutes: m
+ };
+ }
+ return true;
+ }
+
+ function age (dateStr) {
+ if (dateStr === '' || dateStr == null) return '';
+ var d1 = new Date(dateStr);
+ if (w2utils.isInt(dateStr)) d1 = new Date(Number(dateStr)); // for unix timestamps
+ if (d1 === 'Invalid Date') return '';
+
+ var d2 = new Date();
+ var sec = (d2.getTime() - d1.getTime()) / 1000;
+ var amount = '';
+ var type = '';
+ if (sec < 0) {
+ amount = '<span style="color: #aaa">future</span>';
+ type = '';
+ } else if (sec < 60) {
+ amount = Math.floor(sec);
+ type = 'sec';
+ if (sec < 0) { amount = 0; type = 'sec'; }
+ } else if (sec < 60*60) {
+ amount = Math.floor(sec/60);
+ type = 'min';
+ } else if (sec < 24*60*60) {
+ amount = Math.floor(sec/60/60);
+ type = 'hour';
+ } else if (sec < 30*24*60*60) {
+ amount = Math.floor(sec/24/60/60);
+ type = 'day';
+ } else if (sec < 365.25*24*60*60) {
+ amount = Math.floor(sec/365.25/24/60/60*10)/10;
+ type = 'month';
+ } else if (sec >= 365.25*24*60*60) {
+ amount = Math.floor(sec/365.25/24/60/60*10)/10;
+ type = 'year';
+ }
+ return amount + ' ' + type + (amount > 1 ? 's' : '');
+ }
+
+ function date (dateStr) {
+ if (dateStr === '' || dateStr == null) return '';
+ var d1 = new Date(dateStr);
+ if (w2utils.isInt(dateStr)) d1 = new Date(Number(dateStr)); // for unix timestamps
+ if (d1 === 'Invalid Date') return '';
+
+ var months = w2utils.settings.shortmonths;
+ var d2 = new Date(); // today
+ var d3 = new Date();
+ d3.setTime(d3.getTime() - 86400000); // yesterday
+
+ var dd1 = months[d1.getMonth()] + ' ' + d1.getDate() + ', ' + d1.getFullYear();
+ var dd2 = months[d2.getMonth()] + ' ' + d2.getDate() + ', ' + d2.getFullYear();
+ var dd3 = months[d3.getMonth()] + ' ' + d3.getDate() + ', ' + d3.getFullYear();
+
+ var time = (d1.getHours() - (d1.getHours() > 12 ? 12 :0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am');
+ var time2= (d1.getHours() - (d1.getHours() > 12 ? 12 :0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ':' + (d1.getSeconds() < 10 ? '0' : '') + d1.getSeconds() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am');
+ var dsp = dd1;
+ if (dd1 === dd2) dsp = time;
+ if (dd1 === dd3) dsp = w2utils.lang('Yesterday');
+
+ return '<span title="'+ dd1 +' ' + time2 +'">'+ dsp +'</span>';
+ }
+
+ function size (sizeStr) {
+ if (!w2utils.isFloat(sizeStr) || sizeStr === '') return '';
+ sizeStr = parseFloat(sizeStr);
+ if (sizeStr === 0) return 0;
+ var sizes = ['Bt', 'KB', 'MB', 'GB', 'TB'];
+ var i = parseInt( Math.floor( Math.log(sizeStr) / Math.log(1024) ) );
+ return (Math.floor(sizeStr / Math.pow(1024, i) * 10) / 10).toFixed(i === 0 ? 0 : 1) + ' ' + sizes[i];
+ }
+
+ function formatNumber (val, groupSymbol) {
+ var ret = '';
+ if (groupSymbol == null) groupSymbol = w2utils.settings.groupSymbol || ',';
+ // check if this is a number
+ if (w2utils.isFloat(val) || w2utils.isInt(val) || w2utils.isMoney(val)) {
+ tmp = String(val).split('.');
+ ret = String(tmp[0]).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1" + groupSymbol);
+ if (tmp[1] != null) ret += '.' + tmp[1];
+ }
+ return ret;
+ }
+
+ function formatDate (dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String
+ var months = w2utils.settings.shortmonths;
+ var fullMonths = w2utils.settings.fullmonths;
+ if (!format) format = this.settings.date_format;
+ if (dateStr === '' || dateStr == null) return '';
+
+ var dt = new Date(dateStr);
+ if (w2utils.isInt(dateStr)) dt = new Date(Number(dateStr)); // for unix timestamps
+ if (dt === 'Invalid Date') return '';
+
+ var year = dt.getFullYear();
+ var month = dt.getMonth();
+ var date = dt.getDate();
+ return format.toLowerCase()
+ .replace('month', w2utils.settings.fullmonths[month])
+ .replace('mon', w2utils.settings.shortmonths[month])
+ .replace(/yyyy/g, year)
+ .replace(/yyy/g, year)
+ .replace(/yy/g, year > 2000 ? 100 + parseInt(String(year).substr(2)) : String(year).substr(2))
+ .replace(/(^|[^a-z$])y/g, '$1' + year) // only y's that are not preceeded by a letter
+ .replace(/mm/g, (month + 1 < 10 ? '0' : '') + (month + 1))
+ .replace(/dd/g, (date < 10 ? '0' : '') + date)
+ .replace(/(^|[^a-z$])m/g, '$1' + (month + 1)) // only y's that are not preceeded by a letter
+ .replace(/(^|[^a-z$])d/g, '$1' + date); // only y's that are not preceeded by a letter
+ }
+
+ function formatTime (dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String
+ var months = w2utils.settings.shortmonths;
+ var fullMonths = w2utils.settings.fullmonths;
+ if (!format) format = (this.settings.time_format === 'h12' ? 'hh:mi pm' : 'h24:mi');
+ if (dateStr === '' || dateStr == null) return '';
+
+ var dt = new Date(dateStr);
+ if (w2utils.isInt(dateStr)) dt = new Date(Number(dateStr)); // for unix timestamps
+ if (w2utils.isTime(dateStr)) {
+ var tmp = w2utils.isTime(dateStr, true);
+ dt = new Date();
+ dt.setHours(tmp.hours);
+ dt.setMinutes(tmp.minutes);
+ }
+ if (dt === 'Invalid Date') return '';
+
+ var type = 'am';
+ var hour = dt.getHours();
+ var h24 = dt.getHours();
+ var min = dt.getMinutes();
+ var sec = dt.getSeconds();
+ if (min < 10) min = '0' + min;
+ if (sec < 10) sec = '0' + sec;
+ if (format.indexOf('am') !== -1 || format.indexOf('pm') !== -1) {
+ if (hour >= 12) type = 'pm';
+ if (hour > 12) hour = hour - 12;
+ }
+ return format.toLowerCase()
+ .replace('am', type)
+ .replace('pm', type)
+ .replace('hh', hour)
+ .replace('h24', h24)
+ .replace('mm', min)
+ .replace('mi', min)
+ .replace('ss', sec)
+ .replace(/(^|[^a-z$])h/g, '$1' + hour) // only y's that are not preceeded by a letter
+ .replace(/(^|[^a-z$])m/g, '$1' + min) // only y's that are not preceeded by a letter
+ .replace(/(^|[^a-z$])s/g, '$1' + sec); // only y's that are not preceeded by a letter
+ }
+
+ function formatDateTime(dateStr, format) {
+ var fmt;
+ if (typeof format !== 'string') {
+ fmt = [this.settings.date_format, this.settings.time_format];
+ } else {
+ fmt = format.split('|');
+ }
+ return this.formatDate(dateStr, fmt[0]) + ' ' + this.formatTime(dateStr, fmt[1]);
+ }
+
+ function stripTags (html) {
+ if (html === null) return html;
+ switch (typeof html) {
+ case 'number':
+ break;
+ case 'string':
+ html = $.trim(String(html).replace(/(<([^>]+)>)/ig, ""));
+ break;
+ case 'object':
+ for (var a in html) html[a] = this.stripTags(html[a]);
+ break;
+ }
+ return html;
+ }
+
+ function encodeTags (html) {
+ if (html === null) return html;
+ switch (typeof html) {
+ case 'number':
+ break;
+ case 'string':
+ html = String(html).replace(/&/g, "&amp;").replace(/>/g, "&gt;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
+ break;
+ case 'object':
+ for (var a in html) html[a] = this.encodeTags(html[a]);
+ break;
+ }
+ return html;
+ }
+
+ function escapeId (id) {
+ if (id === '' || id == null) return '';
+ return String(id).replace(/([;&,\.\+\*\~'`:"\!\^#$%@\[\]\(\)=<>\|\/? {}\\])/g, '\\$1');
+ }
+
+ function base64encode (input) {
+ var output = "";
+ var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
+ var i = 0;
+ var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+ input = utf8_encode(input);
+
+ while (i < input.length) {
+ chr1 = input.charCodeAt(i++);
+ chr2 = input.charCodeAt(i++);
+ chr3 = input.charCodeAt(i++);
+ enc1 = chr1 >> 2;
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+ enc4 = chr3 & 63;
+ if (isNaN(chr2)) {
+ enc3 = enc4 = 64;
+ } else if (isNaN(chr3)) {
+ enc4 = 64;
+ }
+ output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
+ }
+
+ function utf8_encode (string) {
+ var string = String(string).replace(/\r\n/g,"\n");
+ var utftext = "";
+
+ for (var n = 0; n < string.length; n++) {
+ var c = string.charCodeAt(n);
+ if (c < 128) {
+ utftext += String.fromCharCode(c);
+ }
+ else if((c > 127) && (c < 2048)) {
+ utftext += String.fromCharCode((c >> 6) | 192);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+ else {
+ utftext += String.fromCharCode((c >> 12) | 224);
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+ }
+ return utftext;
+ }
+
+ return output;
+ }
+
+ function base64decode (input) {
+ var output = "";
+ var chr1, chr2, chr3;
+ var enc1, enc2, enc3, enc4;
+ var i = 0;
+ var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+ input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
+
+ while (i < input.length) {
+ enc1 = keyStr.indexOf(input.charAt(i++));
+ enc2 = keyStr.indexOf(input.charAt(i++));
+ enc3 = keyStr.indexOf(input.charAt(i++));
+ enc4 = keyStr.indexOf(input.charAt(i++));
+ chr1 = (enc1 << 2) | (enc2 >> 4);
+ chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+ chr3 = ((enc3 & 3) << 6) | enc4;
+ output = output + String.fromCharCode(chr1);
+ if (enc3 !== 64) {
+ output = output + String.fromCharCode(chr2);
+ }
+ if (enc4 !== 64) {
+ output = output + String.fromCharCode(chr3);
+ }
+ }
+ output = utf8_decode(output);
+
+ function utf8_decode (utftext) {
+ var string = "";
+ var i = 0;
+ var c = 0, c2, c3;
+
+ while ( i < utftext.length ) {
+ c = utftext.charCodeAt(i);
+ if (c < 128) {
+ string += String.fromCharCode(c);
+ i++;
+ }
+ else if((c > 191) && (c < 224)) {
+ c2 = utftext.charCodeAt(i+1);
+ string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
+ i += 2;
+ }
+ else {
+ c2 = utftext.charCodeAt(i+1);
+ c3 = utftext.charCodeAt(i+2);
+ string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+ i += 3;
+ }
+ }
+
+ return string;
+ }
+
+ return output;
+ }
+
+ function transition (div_old, div_new, type, callBack) {
+ var width = $(div_old).width();
+ var height = $(div_old).height();
+ var time = 0.5;
+
+ if (!div_old || !div_new) {
+ console.log('ERROR: Cannot do transition when one of the divs is null');
+ return;
+ }
+
+ div_old.parentNode.style.cssText += cross('perspective', '700px') +'; overflow: hidden;';
+ div_old.style.cssText += '; position: absolute; z-index: 1019; '+ cross('backface-visibility', 'hidden');
+ div_new.style.cssText += '; position: absolute; z-index: 1020; '+ cross('backface-visibility', 'hidden');
+
+ switch (type) {
+ case 'slide-left':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d('+ width + 'px, 0, 0)', 'translate('+ width +'px, 0)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +';'+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_old.style.cssText += cross('transition', time+'s') +';'+ cross('transform', 'translate3d(-'+ width +'px, 0, 0)', 'translate(-'+ width +'px, 0)');
+ }, 1);
+ break;
+
+ case 'slide-right':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(-'+ width +'px, 0, 0)', 'translate(-'+ width +'px, 0)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'translate3d(0px, 0, 0)', 'translate(0px, 0)');
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'translate3d('+ width +'px, 0, 0)', 'translate('+ width +'px, 0)');
+ }, 1);
+ break;
+
+ case 'slide-down':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; z-index: 1; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_new.style.cssText += 'overflow: hidden; z-index: 0; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'translate3d(0, '+ height +'px, 0)', 'translate(0, '+ height +'px)');
+ }, 1);
+ break;
+
+ case 'slide-up':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, '+ height +'px, 0)', 'translate(0, '+ height +'px)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ }, 1);
+ break;
+
+ case 'flip-left':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateY(0deg)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateY(-180deg)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateY(0deg)');
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateY(180deg)');
+ }, 1);
+ break;
+
+ case 'flip-right':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateY(0deg)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateY(180deg)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateY(0deg)');
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateY(-180deg)');
+ }, 1);
+ break;
+
+ case 'flip-down':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateX(0deg)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateX(180deg)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateX(0deg)');
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateX(-180deg)');
+ }, 1);
+ break;
+
+ case 'flip-up':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateX(0deg)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'rotateX(-180deg)');
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateX(0deg)');
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'rotateX(180deg)');
+ }, 1);
+ break;
+
+ case 'pop-in':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)') + '; '+ cross('transform', 'scale(.8)') + '; opacity: 0;';
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'scale(1)') +'; opacity: 1;';
+ div_old.style.cssText += cross('transition', time+'s') +';';
+ }, 1);
+ break;
+
+ case 'pop-out':
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)') +'; '+ cross('transform', 'scale(1)') +'; opacity: 1;';
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)') +'; opacity: 0;';
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time+'s') +'; opacity: 1;';
+ div_old.style.cssText += cross('transition', time+'s') +'; '+ cross('transform', 'scale(1.7)') +'; opacity: 0;';
+ }, 1);
+ break;
+
+ default:
+ // init divs
+ div_old.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)');
+ div_new.style.cssText += 'overflow: hidden; '+ cross('transform', 'translate3d(0, 0, 0)', 'translate(0, 0)') +'; opacity: 0;';
+ $(div_new).show();
+ // -- need a timing function because otherwise not working
+ window.setTimeout(function() {
+ div_new.style.cssText += cross('transition', time +'s') +'; opacity: 1;';
+ div_old.style.cssText += cross('transition', time +'s');
+ }, 1);
+ break;
+ }
+
+ setTimeout(function () {
+ if (type === 'slide-down') {
+ $(div_old).css('z-index', '1019');
+ $(div_new).css('z-index', '1020');
+ }
+ if (div_new) {
+ $(div_new).css({
+ 'opacity': '1',
+ '-webkit-transition': '',
+ '-moz-transition': '',
+ '-ms-transition': '',
+ '-o-transition': '',
+ '-webkit-transform': '',
+ '-moz-transform': '',
+ '-ms-transform': '',
+ '-o-transform': '',
+ '-webkit-backface-visibility': '',
+ '-moz-backface-visibility': '',
+ '-ms-backface-visibility': '',
+ '-o-backface-visibility': ''
+ });
+ }
+ if (div_old) {
+ $(div_old).css({
+ 'opacity': '1',
+ '-webkit-transition': '',
+ '-moz-transition': '',
+ '-ms-transition': '',
+ '-o-transition': '',
+ '-webkit-transform': '',
+ '-moz-transform': '',
+ '-ms-transform': '',
+ '-o-transform': '',
+ '-webkit-backface-visibility': '',
+ '-moz-backface-visibility': '',
+ '-ms-backface-visibility': '',
+ '-o-backface-visibility': ''
+ });
+ if (div_old.parentNode) $(div_old.parentNode).css({
+ '-webkit-perspective': '',
+ '-moz-perspective': '',
+ '-ms-perspective': '',
+ '-o-perspective': ''
+ });
+ }
+ if (typeof callBack === 'function') callBack();
+ }, time * 1000);
+
+ function cross(property, value, none_webkit_value) {
+ var isWebkit=!!window.webkitURL; // jQuery no longer supports $.browser - RR
+ if (!isWebkit && typeof none_webkit_value !== 'undefined') value = none_webkit_value;
+ return ';'+ property +': '+ value +'; -webkit-'+ property +': '+ value +'; -moz-'+ property +': '+ value +'; '+
+ '-ms-'+ property +': '+ value +'; -o-'+ property +': '+ value +';';
+ }
+ }
+
+ function lock (box, msg, spinner) {
+ var options = {};
+ if (typeof msg === 'object') {
+ options = msg;
+ } else {
+ options.msg = msg;
+ options.spinner = spinner;
+ }
+ if (!options.msg && options.msg !== 0) options.msg = '';
+ w2utils.unlock(box);
+ $(box).prepend(
+ '<div class="w2ui-lock"></div>'+
+ '<div class="w2ui-lock-msg"></div>'
+ );
+ var $lock = $(box).find('.w2ui-lock');
+ var mess = $(box).find('.w2ui-lock-msg');
+ if (!options.msg) mess.css({ 'background-color': 'transparent', 'border': '0px' });
+ if (options.spinner === true) options.msg = '<div class="w2ui-spinner" '+ (!options.msg ? 'style="width: 35px; height: 35px"' : '') +'></div>' + options.msg;
+ if (options.opacity != null) $lock.css('opacity', options.opacity);
+ if (typeof $lock.fadeIn == 'function') {
+ $lock.fadeIn(200);
+ mess.html(options.msg).fadeIn(200);
+ } else {
+ $lock.show();
+ mess.html(options.msg).show(0);
+ }
+ // hide all tags (do not hide overlays as the form can be in overlay)
+ $().w2tag();
+ }
+
+ function unlock (box) {
+ $(box).find('.w2ui-lock').remove();
+ $(box).find('.w2ui-lock-msg').remove();
+ }
+
+ function getSize (el, type) {
+ var $el = $(el);
+ var bwidth = {
+ left : parseInt($el.css('border-left-width')) || 0,
+ right : parseInt($el.css('border-right-width')) || 0,
+ top : parseInt($el.css('border-top-width')) || 0,
+ bottom : parseInt($el.css('border-bottom-width')) || 0
+ };
+ var mwidth = {
+ left : parseInt($el.css('margin-left')) || 0,
+ right : parseInt($el.css('margin-right')) || 0,
+ top : parseInt($el.css('margin-top')) || 0,
+ bottom : parseInt($el.css('margin-bottom')) || 0
+ };
+ var pwidth = {
+ left : parseInt($el.css('padding-left')) || 0,
+ right : parseInt($el.css('padding-right')) || 0,
+ top : parseInt($el.css('padding-top')) || 0,
+ bottom : parseInt($el.css('padding-bottom')) || 0
+ };
+ switch (type) {
+ case 'top' : return bwidth.top + mwidth.top + pwidth.top;
+ case 'bottom' : return bwidth.bottom + mwidth.bottom + pwidth.bottom;
+ case 'left' : return bwidth.left + mwidth.left + pwidth.left;
+ case 'right' : return bwidth.right + mwidth.right + pwidth.right;
+ case 'width' : return bwidth.left + bwidth.right + mwidth.left + mwidth.right + pwidth.left + pwidth.right + parseInt($el.width());
+ case 'height' : return bwidth.top + bwidth.bottom + mwidth.top + mwidth.bottom + pwidth.top + pwidth.bottom + parseInt($el.height());
+ case '+width' : return bwidth.left + bwidth.right + mwidth.left + mwidth.right + pwidth.left + pwidth.right;
+ case '+height' : return bwidth.top + bwidth.bottom + mwidth.top + mwidth.bottom + pwidth.top + pwidth.bottom;
+ }
+ return 0;
+ }
+
+ function lang (phrase) {
+ var translation = this.settings.phrases[phrase];
+ if (translation == null) return phrase; else return translation;
+ }
+
+ function locale (locale) {
+ if (!locale) locale = 'en-us';
+ if (locale.length === 5) locale = 'locale/'+ locale +'.json';
+ // load from the file
+ $.ajax({
+ url : locale,
+ type : "GET",
+ dataType : "JSON",
+ async : false,
+ cache : false,
+ success : function (data, status, xhr) {
+ w2utils.settings = $.extend(true, w2utils.settings, data);
+ // apply translation to some prototype functions
+ var p = w2obj.grid.prototype;
+ for (var b in p.buttons) {
+ p.buttons[b].caption = w2utils.lang(p.buttons[b].caption);
+ p.buttons[b].hint = w2utils.lang(p.buttons[b].hint);
+ }
+ p.msgDelete = w2utils.lang(p.msgDelete);
+ p.msgNotJSON = w2utils.lang(p.msgNotJSON);
+ p.msgRefresh = w2utils.lang(p.msgRefresh);
+ },
+ error : function (xhr, status, msg) {
+ console.log('ERROR: Cannot load locale '+ locale);
+ }
+ });
+ }
+
+ function scrollBarSize () {
+ if (tmp.scrollBarSize) return tmp.scrollBarSize;
+ var html =
+ '<div id="_scrollbar_width" style="position: absolute; top: -300px; width: 100px; height: 100px; overflow-y: scroll;">'+
+ ' <div style="height: 120px">1</div>'+
+ '</div>';
+ $('body').append(html);
+ tmp.scrollBarSize = 100 - $('#_scrollbar_width > div').width();
+ $('#_scrollbar_width').remove();
+ if (String(navigator.userAgent).indexOf('MSIE') >= 0) tmp.scrollBarSize = tmp.scrollBarSize / 2; // need this for IE9+
+ return tmp.scrollBarSize;
+ }
+
+
+ function checkName (params, component) { // was w2checkNameParam
+ if (!params || typeof params.name === 'undefined') {
+ console.log('ERROR: The parameter "name" is required but not supplied in $().'+ component +'().');
+ return false;
+ }
+ if (typeof w2ui[params.name] !== 'undefined') {
+ console.log('ERROR: The parameter "name" is not unique. There are other objects already created with the same name (obj: '+ params.name +').');
+ return false;
+ }
+ if (!w2utils.isAlphaNumeric(params.name)) {
+ console.log('ERROR: The parameter "name" has to be alpha-numeric (a-z, 0-9, dash and underscore). ');
+ return false;
+ }
+ return true;
+ }
+
+ function checkUniqueId (id, items, itemsDecription, objName) { // was w2checkUniqueId
+ if (!$.isArray(items)) items = [items];
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].id === id) {
+ console.log('ERROR: The parameter "id='+ id +'" is not unique within the current '+ itemsDecription +'. (obj: '+ objName +')');
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function parseRoute(route) {
+ var keys = [];
+ var path = route
+ .replace(/\/\(/g, '(?:/')
+ .replace(/\+/g, '__plus__')
+ .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) {
+ keys.push({ name: key, optional: !! optional });
+ slash = slash || '';
+ return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '');
+ })
+ .replace(/([\/.])/g, '\\$1')
+ .replace(/__plus__/g, '(.+)')
+ .replace(/\*/g, '(.*)');
+ return {
+ path : new RegExp('^' + path + '$', 'i'),
+ keys : keys
+ };
+ }
+})();
+
+/***********************************************************
+* Generic Event Object
+* --- This object is reused across all other
+* --- widgets in w2ui.
+*
+*********************************************************/
+
+w2utils.event = {
+
+ on: function (eventData, handler) {
+ if (!$.isPlainObject(eventData)) eventData = { type: eventData };
+ eventData = $.extend({ type: null, execute: 'before', target: null, onComplete: null }, eventData);
+
+ if (!eventData.type) { console.log('ERROR: You must specify event type when calling .on() method of '+ this.name); return; }
+ if (!handler) { console.log('ERROR: You must specify event handler function when calling .on() method of '+ this.name); return; }
+ if (!$.isArray(this.handlers)) this.handlers = [];
+ this.handlers.push({ event: eventData, handler: handler });
+ },
+
+ off: function (eventData, handler) {
+ if (!$.isPlainObject(eventData)) eventData = { type: eventData };
+ eventData = $.extend({}, { type: null, execute: 'before', target: null, onComplete: null }, eventData);
+
+ if (!eventData.type) { console.log('ERROR: You must specify event type when calling .off() method of '+ this.name); return; }
+ if (!handler) { handler = null; }
+ // remove handlers
+ var newHandlers = [];
+ for (var h = 0, len = this.handlers.length; h < len; h++) {
+ var t = this.handlers[h];
+ if ((t.event.type === eventData.type || eventData.type === '*') &&
+ (t.event.target === eventData.target || eventData.target === null) &&
+ (t.handler === handler || handler === null))
+ {
+ // match
+ } else {
+ newHandlers.push(t);
+ }
+ }
+ this.handlers = newHandlers;
+ },
+
+ trigger: function (eventData) {
+ var eventData = $.extend({ type: null, phase: 'before', target: null }, eventData, {
+ isStopped: false, isCancelled: false,
+ preventDefault : function () { this.isCancelled = true; },
+ stopPropagation : function () { this.isStopped = true; }
+ });
+ if (eventData.phase === 'before') eventData.onComplete = null;
+ var args, fun, tmp;
+ if (eventData.target == null) eventData.target = null;
+ if (!$.isArray(this.handlers)) this.handlers = [];
+ // process events in REVERSE order
+ for (var h = this.handlers.length-1; h >= 0; h--) {
+ var item = this.handlers[h];
+ if ((item.event.type === eventData.type || item.event.type === '*') &&
+ (item.event.target === eventData.target || item.event.target === null) &&
+ (item.event.execute === eventData.phase || item.event.execute === '*' || item.event.phase === '*'))
+ {
+ eventData = $.extend({}, item.event, eventData);
+ // check handler arguments
+ args = [];
+ tmp = RegExp(/\((.*?)\)/).exec(item.handler);
+ if (tmp) args = tmp[1].split(/\s*,\s*/);
+ if (args.length === 2) {
+ item.handler.call(this, eventData.target, eventData); // old way for back compatibility
+ } else {
+ item.handler.call(this, eventData); // new way
+ }
+ if (eventData.isStopped === true || eventData.stop === true) return eventData; // back compatibility eventData.stop === true
+ }
+ }
+ // main object events
+ var funName = 'on' + eventData.type.substr(0,1).toUpperCase() + eventData.type.substr(1);
+ if (eventData.phase === 'before' && typeof this[funName] === 'function') {
+ fun = this[funName];
+ // check handler arguments
+ args = [];
+ tmp = RegExp(/\((.*?)\)/).exec(fun);
+ if (tmp) args = tmp[1].split(/\s*,\s*/);
+ if (args.length === 2) {
+ fun.call(this, eventData.target, eventData); // old way for back compatibility
+ } else {
+ fun.call(this, eventData); // new way
+ }
+ if (eventData.isStopped === true || eventData.stop === true) return eventData; // back compatibility eventData.stop === true
+ }
+ // item object events
+ if (eventData.object != null && eventData.phase === 'before' &&
+ typeof eventData.object[funName] === 'function')
+ {
+ fun = eventData.object[funName];
+ // check handler arguments
+ args = [];
+ tmp = RegExp(/\((.*?)\)/).exec(fun);
+ if (tmp) args = tmp[1].split(/\s*,\s*/);
+ if (args.length === 2) {
+ fun.call(this, eventData.target, eventData); // old way for back compatibility
+ } else {
+ fun.call(this, eventData); // new way
+ }
+ if (eventData.isStopped === true || eventData.stop === true) return eventData;
+ }
+ // execute onComplete
+ if (eventData.phase === 'after' && typeof eventData.onComplete === 'function') eventData.onComplete.call(this, eventData);
+
+ return eventData;
+ }
+};
+
+/***********************************************************
+* Common Keyboard Handler. Supported in
+* - grid
+* - sidebar
+* - popup
+*
+*********************************************************/
+
+w2utils.keyboard = (function (obj) {
+ // private scope
+ var w2ui_name = null;
+
+ obj.active = active;
+ obj.clear = clear;
+
+ init();
+ return obj;
+
+ function init() {
+ $(document).on('keydown', keydown);
+ $(document).on('mousedown', mousedown);
+ }
+
+ function keydown (event) {
+ var tag = event.target.tagName;
+ if ($.inArray(tag, ['INPUT', 'SELECT', 'TEXTAREA']) !== -1) return;
+ if ($(event.target).prop('contenteditable') === 'true') return;
+ if (!w2ui_name) return;
+ // pass to appropriate widget
+ if (w2ui[w2ui_name] && typeof w2ui[w2ui_name].keydown === 'function') {
+ w2ui[w2ui_name].keydown.call(w2ui[w2ui_name], event);
+ }
+ }
+
+ function mousedown (event) {
+ var tag = event.target.tagName;
+ var obj = $(event.target).parents('.w2ui-reset');
+ if (obj.length > 0) {
+ var name = obj.attr('name');
+ if (w2ui[name] && w2ui[name].keyboard) w2ui_name = name;
+ }
+ }
+
+ function active (new_w2ui_name) {
+ if (typeof new_w2ui_name !== 'undefined') w2ui_name = new_w2ui_name;
+ return w2ui_name;
+ }
+
+ function clear () {
+ w2ui_name = null;
+ }
+
+})({});
+
+/***********************************************************
+* Commonly used plugins
+* --- used primarily in grid and form
+*
+*********************************************************/
+
+(function () {
+
+ $.fn.w2render = function (name) {
+ if ($(this).length > 0) {
+ if (typeof name === 'string' && w2ui[name]) w2ui[name].render($(this)[0]);
+ if (typeof name === 'object') name.render($(this)[0]);
+ }
+ };
+
+ $.fn.w2destroy = function (name) {
+ if (!name && this.length > 0) name = this.attr('name');
+ if (typeof name === 'string' && w2ui[name]) w2ui[name].destroy();
+ if (typeof name === 'object') name.destroy();
+ };
+
+ $.fn.w2marker = function (str) {
+ if (str === '' || str == null) { // remove marker
+ return $(this).each(function (index, el) {
+ el.innerHTML = el.innerHTML.replace(/\<span class=\"w2ui\-marker\"\>(.*)\<\/span\>/ig, '$1'); // unmark
+ });
+ } else { // add marker
+ return $(this).each(function (index, el) {
+ if (typeof str === 'string') str = [str];
+ el.innerHTML = el.innerHTML.replace(/\<span class=\"w2ui\-marker\"\>(.*)\<\/span\>/ig, '$1'); // unmark
+ for (var s in str) {
+ var tmp = str[s];
+ if (typeof tmp !== 'string') tmp = String(tmp);
+ // escape regex special chars
+ tmp = tmp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&").replace(/&/g, '&amp;').replace(/</g, '&gt;').replace(/>/g, '&lt;');
+ var regex = new RegExp(tmp + '(?!([^<]+)?>)', "gi"); // only outside tags
+ el.innerHTML = el.innerHTML.replace(regex, replaceValue);
+ }
+ function replaceValue(matched) { // mark new
+ return '<span class="w2ui-marker">' + matched + '</span>';
+ }
+ });
+ }
+ };
+
+ // -- w2tag - appears on the right side from element, there can be multiple on screen at a time
+
+ $.fn.w2tag = function (text, options) {
+ if (!$.isPlainObject(options)) options = {};
+ if (!$.isPlainObject(options.css)) options.css = {};
+ if (typeof options['class'] === 'undefined') options['class'] = '';
+ // remove all tags
+ if ($(this).length === 0) {
+ $('.w2ui-tag').each(function (index, elem) {
+ var opt = $(elem).data('options');
+ if (opt == null) opt = {};
+ $($(elem).data('taged-el')).removeClass( opt['class'] );
+ clearInterval($(elem).data('timer'));
+ $(elem).remove();
+ });
+ return;
+ }
+ return $(this).each(function (index, el) {
+ // show or hide tag
+ var tagOrigID = el.id;
+ var tagID = w2utils.escapeId(el.id);
+ if (text === '' || text == null) {
+ $('#w2ui-tag-'+tagID).css('opacity', 0);
+ setTimeout(function () {
+ // remmove element
+ clearInterval($('#w2ui-tag-'+tagID).data('timer'));
+ $('#w2ui-tag-'+tagID).remove();
+ }, 300);
+ } else {
+ // remove elements
+ clearInterval($('#w2ui-tag-'+tagID).data('timer'));
+ $('#w2ui-tag-'+tagID).remove();
+ // insert
+ $('body').append(
+ '<div id="w2ui-tag-'+ tagOrigID +'" class="w2ui-tag '+ ($(el).parents('.w2ui-popup').length > 0 ? 'w2ui-tag-popup' : '') +
+ '" style=""></div>');
+
+ var timer = setInterval(function () {
+ // monitor if destroyed
+ if ($(el).length === 0 || ($(el).offset().left === 0 && $(el).offset().top === 0)) {
+ clearInterval($('#w2ui-tag-'+tagID).data('timer'));
+ tmp_hide();
+ return;
+ }
+ // monitor if moved
+ if ($('#w2ui-tag-'+tagID).data('position') !== ($(el).offset().left + el.offsetWidth) + 'x' + $(el).offset().top) {
+ $('#w2ui-tag-'+tagID).css({
+ '-webkit-transition' : '.2s',
+ '-moz-transition' : '.2s',
+ '-ms-transition' : '.2s',
+ '-o-transition' : '.2s',
+ left: ($(el).offset().left + el.offsetWidth) + 'px',
+ top: $(el).offset().top + 'px'
+ }).data('position', ($(el).offset().left + el.offsetWidth) + 'x' + $(el).offset().top);
+ }
+ }, 100);
+ setTimeout(function () {
+ if (!$(el).offset()) return;
+ $('#w2ui-tag-'+tagID).css({
+ opacity: '1',
+ left: ($(el).offset().left + el.offsetWidth) + 'px',
+ top: $(el).offset().top + 'px'
+ }).html('<div style="margin-top: -2px 0px 0px -2px; white-space: nowrap;"> <div class="w2ui-tag-body">'+ text +'</div> </div>')
+ .data('text', text)
+ .data('taged-el', el)
+ .data('options', options)
+ .data('position', ($(el).offset().left + el.offsetWidth) + 'x' + $(el).offset().top)
+ .data('timer', timer);
+ $(el).off('keypress', tmp_hide).on('keypress', tmp_hide).off('change', tmp_hide).on('change', tmp_hide)
+ .css(options.css).addClass(options['class']);
+ if (typeof options.onShow === 'function') options.onShow();
+ }, 1);
+ var originalCSS = '';
+ if ($(el).length > 0) originalCSS = $(el)[0].style.cssText;
+ // bind event to hide it
+ function tmp_hide() {
+ $tag = $('#w2ui-tag-'+tagID);
+ if ($tag.length <= 0) return;
+ clearInterval($tag.data('timer'));
+ $tag.remove();
+ $(el).off('keypress', tmp_hide).removeClass(options['class']);
+ if ($(el).length > 0) $(el)[0].style.cssText = originalCSS;
+ if (typeof options.onHide === 'function') options.onHide();
+ }
+ }
+ });
+ };
+
+ // w2overlay - appears under the element, there can be only one at a time
+
+ $.fn.w2overlay = function (html, options) {
+ var obj = this;
+ var name = '';
+ var defaults = {
+ name : null, // it not null, then allows multiple concurent overlays
+ html : '', // html text to display
+ align : 'none', // can be none, left, right, both
+ left : 0, // offset left
+ top : 0, // offset top
+ tipLeft : 30, // tip offset left
+ width : 0, // fixed width
+ height : 0, // fixed height
+ maxWidth : null, // max width if any
+ maxHeight : null, // max height if any
+ style : '', // additional style for main div
+ 'class' : '', // additional class name for main div
+ onShow : null, // event on show
+ onHide : null, // event on hide
+ openAbove : false, // show abover control
+ tmp : {}
+ };
+ if (arguments.length == 1) {
+ if (typeof html == 'object') {
+ options = html;
+ } else {
+ options = { html: html };
+ }
+ }
+ if (arguments.length == 2) options.html = html;
+ if (!$.isPlainObject(options)) options = {};
+ options = $.extend({}, defaults, options);
+ if (options.name) name = '-' + options.name;
+ // if empty then hide
+ var tmp_hide;
+ if (this.length === 0 || options.html === '' || options.html == null) {
+ if ($('#w2ui-overlay'+ name).length > 0) {
+ tmp_hide = $('#w2ui-overlay'+ name)[0].hide;
+ if (typeof tmp_hide === 'function') tmp_hide();
+ } else {
+ $('#w2ui-overlay'+ name).remove();
+ }
+ return $(this);
+ }
+ if ($('#w2ui-overlay'+ name).length > 0) {
+ tmp_hide = $('#w2ui-overlay'+ name)[0].hide;
+ $(document).off('click', tmp_hide);
+ if (typeof tmp_hide === 'function') tmp_hide();
+ }
+ $('body').append(
+ '<div id="w2ui-overlay'+ name +'" style="display: none"'+
+ ' class="w2ui-reset w2ui-overlay '+ ($(this).parents('.w2ui-popup, .w2ui-overlay-popup').length > 0 ? 'w2ui-overlay-popup' : '') +'">'+
+ ' <style></style>'+
+ ' <div style="'+ options.style +'" class="'+ options['class'] +'"></div>'+
+ '</div>'
+ );
+ // init
+ var div1 = $('#w2ui-overlay'+ name);
+ var div2 = div1.find(' > div');
+ div2.html(options.html);
+ // pick bg color of first div
+ var bc = div2.css('background-color');
+ if (bc != null && bc !== 'rgba(0, 0, 0, 0)' && bc !== 'transparent') div1.css('background-color', bc);
+
+ div1.data('element', obj.length > 0 ? obj[0] : null)
+ .data('options', options)
+ .data('position', $(obj).offset().left + 'x' + $(obj).offset().top)
+ .fadeIn('fast').on('mousedown', function (event) {
+ $('#w2ui-overlay'+ name).data('keepOpen', true);
+ if (['INPUT', 'TEXTAREA', 'SELECT'].indexOf(event.target.tagName) === -1) event.preventDefault();
+ });
+ div1[0].hide = hide;
+ div1[0].resize = resize;
+
+ // need time to display
+ resize();
+ setTimeout(function () {
+ resize();
+ $(document).off('click', hide).on('click', hide);
+ if (typeof options.onShow === 'function') options.onShow();
+ }, 10);
+
+ monitor();
+ return $(this);
+
+ // monitor position
+ function monitor() {
+ var tmp = $('#w2ui-overlay'+ name);
+ if (tmp.data('element') !== obj[0]) return; // it if it different overlay
+ if (tmp.length === 0) return;
+ var pos = $(obj).offset().left + 'x' + $(obj).offset().top;
+ if (tmp.data('position') !== pos) {
+ hide();
+ } else {
+ setTimeout(monitor, 250);
+ }
+ }
+
+ // click anywhere else hides the drop down
+ function hide () {
+ var div1 = $('#w2ui-overlay'+ name);
+ if (div1.data('keepOpen') === true) {
+ div1.removeData('keepOpen');
+ return;
+ }
+ var result;
+ if (typeof options.onHide === 'function') result = options.onHide();
+ if (result === false) return;
+ div1.remove();
+ $(document).off('click', hide);
+ clearInterval(div1.data('timer'));
+ }
+
+ function resize () {
+ var div1 = $('#w2ui-overlay'+ name);
+ var div2 = div1.find(' > div');
+ // if goes over the screen, limit height and width
+ if (div1.length > 0) {
+ div2.height('auto').width('auto');
+ // width/height
+ var overflowX = false;
+ var overflowY = false;
+ var h = div2.height();
+ var w = div2.width();
+ if (options.width && options.width < w) w = options.width;
+ if (w < 30) w = 30;
+ // if content of specific height
+ if (options.tmp.contentHeight) {
+ h = options.tmp.contentHeight;
+ div2.height(h);
+ setTimeout(function () {
+ if (div2.height() > div2.find('div.menu > table').height()) {
+ div2.find('div.menu').css('overflow-y', 'hidden');
+ }
+ }, 1);
+ setTimeout(function () { div2.find('div.menu').css('overflow-y', 'auto'); }, 10);
+ }
+ if (options.tmp.contentWidth) {
+ w = options.tmp.contentWidth;
+ div2.width(w);
+ setTimeout(function () {
+ if (div2.width() > div2.find('div.menu > table').width()) {
+ div2.find('div.menu').css('overflow-x', 'hidden');
+ }
+ }, 1);
+ setTimeout(function () { div2.find('div.menu').css('overflow-y', 'auto'); }, 10);
+ }
+ // alignment
+ switch (options.align) {
+ case 'both':
+ options.left = 17;
+ if (options.width === 0) options.width = w2utils.getSize($(obj), 'width');
+ break;
+ case 'left':
+ options.left = 17;
+ break;
+ case 'right':
+ options.tipLeft = w - 45;
+ options.left = w2utils.getSize($(obj), 'width') - w + 10;
+ break;
+ }
+ // adjust position
+ var tmp = (w - 17) / 2;
+ var boxLeft = options.left;
+ var boxWidth = options.width;
+ var tipLeft = options.tipLeft;
+ if (w === 30 && !boxWidth) boxWidth = 30; else boxWidth = (options.width ? options.width : 'auto');
+ if (tmp < 25) {
+ boxLeft = 25 - tmp;
+ tipLeft = Math.floor(tmp);
+ }
+ // Y coord
+ div1.css({
+ top : (obj.offset().top + w2utils.getSize(obj, 'height') + options.top + 7) + 'px',
+ left : ((obj.offset().left > 25 ? obj.offset().left : 25) + boxLeft) + 'px',
+ 'min-width' : boxWidth,
+ 'min-height': (options.height ? options.height : 'auto')
+ });
+ // $(window).height() - has a problem in FF20
+ var maxHeight = window.innerHeight + $(document).scrollTop() - div2.offset().top - 7;
+ var maxWidth = window.innerWidth + $(document).scrollLeft() - div2.offset().left - 7;
+ if ((maxHeight > -50 && maxHeight < 210) || options.openAbove === true) {
+ // show on top
+ maxHeight = div2.offset().top - $(document).scrollTop() - 7;
+ if (options.maxHeight && maxHeight > options.maxHeight) maxHeight = options.maxHeight;
+ if (h > maxHeight) {
+ overflowY = true;
+ div2.height(maxHeight).width(w).css({ 'overflow-y': 'auto' });
+ h = maxHeight;
+ }
+ div1.css('top', ($(obj).offset().top - h - 24 + options.top) + 'px');
+ div1.find('>style').html(
+ '#w2ui-overlay'+ name +':before { display: none; margin-left: '+ parseInt(tipLeft) +'px; }'+
+ '#w2ui-overlay'+ name +':after { display: block; margin-left: '+ parseInt(tipLeft) +'px; }'
+ );
+ } else {
+ // show under
+ if (options.maxHeight && maxHeight > options.maxHeight) maxHeight = options.maxHeight;
+ if (h > maxHeight) {
+ overflowY = true;
+ div2.height(maxHeight).width(w).css({ 'overflow-y': 'auto' });
+ }
+ div1.find('>style').html(
+ '#w2ui-overlay'+ name +':before { display: block; margin-left: '+ parseInt(tipLeft) +'px; }'+
+ '#w2ui-overlay'+ name +':after { display: none; margin-left: '+ parseInt(tipLeft) +'px; }'
+ );
+ }
+ // check width
+ w = div2.width();
+ maxWidth = window.innerWidth + $(document).scrollLeft() - div2.offset().left - 7;
+ if (options.maxWidth && maxWidth > options.maxWidth) maxWidth = options.maxWidth;
+ if (w > maxWidth && options.align !== 'both') {
+ options.align = 'right';
+ setTimeout(function () { resize(); }, 1);
+ }
+ // check scroll bar
+ if (overflowY && overflowX) div2.width(w + w2utils.scrollBarSize() + 2);
+ }
+ }
+ };
+
+ $.fn.w2menu = function (menu, options) {
+ /*
+ ITEM STRUCTURE
+ item : {
+ id : null,
+ text : '',
+ style : '',
+ img : '',
+ icon : '',
+ count : '',
+ hidden : false,
+ disabled : false
+ ...
+ }
+ */
+ var defaults = {
+ index : null, // current selected
+ items : [],
+ render : null,
+ msgNoItems : 'No items',
+ onSelect : null,
+ tmp : {}
+ };
+ var obj = this;
+ var name = '';
+ if (menu === 'refresh') {
+ // if not show - call blur
+ if ($('#w2ui-overlay'+ name).length > 0) {
+ options = $.extend($.fn.w2menuOptions, options);
+ var scrTop = $('#w2ui-overlay'+ name +' div.menu').scrollTop();
+ $('#w2ui-overlay'+ name +' div.menu').html(getMenuHTML());
+ $('#w2ui-overlay'+ name +' div.menu').scrollTop(scrTop);
+ mresize();
+ } else {
+ $(this).w2menu(options);
+ }
+ } else {
+ if (arguments.length === 1) options = menu; else options.items = menu;
+ if (typeof options !== 'object') options = {};
+ options = $.extend({}, defaults, options);
+ $.fn.w2menuOptions = options;
+ if (options.name) name = '-' + options.name;
+ if (typeof options.select === 'function' && typeof options.onSelect !== 'function') options.onSelect = options.select;
+ if (typeof options.onRender === 'function' && typeof options.render !== 'function') options.render = options.onRender;
+ // since only one overlay can exist at a time
+ $.fn.w2menuHandler = function (event, index) {
+ if (typeof options.onSelect === 'function') {
+ // need time so that menu first hides
+ setTimeout(function () {
+ options.onSelect({
+ index : index,
+ item : options.items[index],
+ originalEvent: event
+ });
+ }, 10);
+ }
+ setTimeout(function () { $(document).click(); }, 50);
+ };
+ var html = '';
+ if (options.search) {
+ html +=
+ '<div style="position: absolute; top: 0px; height: 40px; left: 0px; right: 0px; border-bottom: 1px solid silver; background-color: #ECECEC; padding: 8px 5px;">'+
+ ' <div class="w2ui-icon icon-search" style="position: absolute; margin-top: 4px; margin-left: 6px; width: 11px; background-position: left !important;"></div>'+
+ ' <input id="menu-search" type="text" style="width: 100%; outline: none; padding-left: 20px;" onclick="event.stopPropagation();">'+
+ '</div>';
+ options.style += ';background-color: #ECECEC';
+ options.index = 0;
+ for (var i in options.items) options.items[i].hidden = false;
+ }
+ html += '<div class="menu" style="position: absolute; top: '+ (options.search ? 40 : 0) + 'px; bottom: 0px; width: 100%; overflow: auto;">' +
+ getMenuHTML() +
+ '</div>';
+ var ret = $(this).w2overlay(html, options);
+ setTimeout(function () {
+ $('#w2ui-overlay'+ name +' #menu-search')
+ .on('keyup', change)
+ .on('keydown', function (event) {
+ // cancel tab key
+ if (event.keyCode === 9) { event.stopPropagation(); event.preventDefault(); }
+ });
+ if (options.search) {
+ if (['text', 'password'].indexOf($(obj)[0].type) != -1 || $(obj)[0].tagName == 'texarea') return;
+ $('#w2ui-overlay'+ name +' #menu-search').focus();
+ }
+ }, 200);
+ mresize();
+ return ret;
+ }
+
+ function mresize() {
+ setTimeout(function () {
+ // show selected
+ $('#w2ui-overlay'+ name +' tr.w2ui-selected').removeClass('w2ui-selected');
+ var cur = $('#w2ui-overlay'+ name +' tr[index='+ options.index +']');
+ var scrTop = $('#w2ui-overlay'+ name +' div.menu').scrollTop();
+ cur.addClass('w2ui-selected');
+ if (options.tmp) options.tmp.contentHeight = $('#w2ui-overlay'+ name +' table').height() + (options.search ? 50 : 10);
+ if (options.tmp) options.tmp.contentWidth = $('#w2ui-overlay'+ name +' table').width();
+ if ($('#w2ui-overlay'+ name).length > 0) $('#w2ui-overlay'+ name)[0].resize();
+ // scroll into view
+ if (cur.length > 0) {
+ var top = cur[0].offsetTop - 5; // 5 is margin top
+ var el = $('#w2ui-overlay'+ name +' div.menu');
+ var height = el.height();
+ $('#w2ui-overlay'+ name +' div.menu').scrollTop(scrTop);
+ if (top < scrTop || top + cur.height() > scrTop + height) {
+ $('#w2ui-overlay'+ name +' div.menu').animate({ 'scrollTop': top - (height - cur.height() * 2) / 2 }, 200, 'linear');
+ }
+ }
+ }, 1);
+ }
+
+ function change(event) {
+ var search = this.value;
+ var key = event.keyCode;
+ var cancel = false;
+ switch (key) {
+ case 13: // enter
+ $('#w2ui-overlay'+ name).remove();
+ $.fn.w2menuHandler(event, options.index);
+ break;
+ case 9: // tab
+ case 27: // escape
+ $('#w2ui-overlay'+ name).remove();
+ $.fn.w2menuHandler(event, -1);
+ break;
+ case 38: // up
+ options.index = w2utils.isInt(options.index) ? parseInt(options.index) : 0;
+ options.index--;
+ while (options.index > 0 && options.items[options.index].hidden) options.index--;
+ if (options.index === 0 && options.items[options.index].hidden) {
+ while (options.items[options.index] && options.items[options.index].hidden) options.index++;
+ }
+ if (options.index < 0) options.index = 0;
+ cancel = true;
+ break;
+ case 40: // down
+ options.index = w2utils.isInt(options.index) ? parseInt(options.index) : 0;
+ options.index++;
+ while (options.index < options.items.length-1 && options.items[options.index].hidden) options.index++;
+ if (options.index === options.items.length-1 && options.items[options.index].hidden) {
+ while (options.items[options.index] && options.items[options.index].hidden) options.index--;
+ }
+ if (options.index >= options.items.length) options.index = options.items.length - 1;
+ cancel = true;
+ break;
+ }
+ // filter
+ if (!cancel) {
+ var shown = 0;
+ for (var i in options.items) {
+ var item = options.items[i];
+ var prefix = '';
+ var suffix = '';
+ if (['is', 'begins with'].indexOf(options.match) !== -1) prefix = '^';
+ if (['is', 'ends with'].indexOf(options.match) !== -1) suffix = '$';
+ try {
+ var re = new RegExp(prefix + search + suffix, 'i');
+ if (re.test(item.text) || item.text === '...') item.hidden = false; else item.hidden = true;
+ } catch (e) {}
+ // do not show selected items
+ if (obj.type === 'enum' && $.inArray(item.id, ids) !== -1) item.hidden = true;
+ if (item.hidden !== true) shown++;
+ }
+ options.index = 0;
+ while (options.index < options.items.length-1 && options.items[options.index].hidden) options.index++;
+ if (shown <= 0) options.index = -1;
+ }
+ $(obj).w2menu('refresh', options);
+ mresize();
+ }
+
+ function getMenuHTML () {
+ if (options.spinner) {
+ return '<table class="w2ui-drop-menu"><tr><td style="padding: 5px 10px 10px 10px; text-align: center">'+
+ ' <div class="w2ui-spinner" style="width: 18px; height: 18px; position: relative; top: 5px;"></div> '+
+ ' <div style="display: inline-block; padding: 3px; color: #999;"> Loading...</div>'+
+ '</td></tr></table>';
+ }
+ var count = 0;
+ var menu_html = '<table cellspacing="0" cellpadding="0" class="w2ui-drop-menu">';
+ var img = null, icon = null;
+ for (var f = 0; f < options.items.length; f++) {
+ var mitem = options.items[f];
+ if (typeof mitem === 'string') {
+ mitem = { id: mitem, text: mitem };
+ } else {
+ if (mitem.text != null && mitem.id == null) mitem.id = mitem.text;
+ if (mitem.text == null && mitem.id != null) mitem.text = mitem.id;
+ if (mitem.caption != null) mitem.text = mitem.caption;
+ img = mitem.img;
+ icon = mitem.icon;
+ if (img == null) img = null;
+ if (icon == null) icon = null;
+ }
+ if (mitem.hidden !== true) {
+ var imgd = '';
+ var txt = mitem.text;
+ if (typeof options.render === 'function') txt = options.render(mitem, options);
+ if (img) imgd = '<td class="menu-icon"><div class="w2ui-tb-image w2ui-icon '+ img +'"></div></td>';
+ if (icon) imgd = '<td class="menu-icon" align="center"><span class="w2ui-icon '+ icon +'"></span></td>';
+ // render only if non-empty
+ if (typeof txt !== 'undefined' && txt !== '' && !(/^-+$/.test(txt))) {
+ var bg = (count % 2 === 0 ? 'w2ui-item-even' : 'w2ui-item-odd');
+ if (options.altRows !== true) bg = '';
+ var colspan = 1;
+ if (imgd == '') colspan++;
+ if (mitem.count == null) colspan++;
+ menu_html +=
+ '<tr index="'+ f + '" style="'+ (mitem.style ? mitem.style : '') +'" '+
+ ' class="'+ bg +' '+ (options.index === f ? 'w2ui-selected' : '') + ' ' + (mitem.disabled === true ? 'w2ui-disabled' : '') +'"'+
+ ' onmousedown="$(this).parent().find(\'tr\').removeClass(\'w2ui-selected\'); $(this).addClass(\'w2ui-selected\');"'+
+ ' onclick="event.stopPropagation(); '+
+ ' if ('+ (mitem.disabled === true ? 'true' : 'false') + ') return;'+
+ ' $(\'#w2ui-overlay'+ name +'\').remove(); '+
+ ' $.fn.w2menuHandler(event, \''+ f +'\');">'+
+ imgd +
+ ' <td class="menu-text" colspan="'+ colspan +'">'+ txt +'</td>'+
+ ' <td class="menu-count">'+ (mitem.count != null ? '<span>' + mitem.count + '</span>' : '') + '</td>' +
+ '</tr>';
+ count++;
+ } else {
+ // horizontal line
+ menu_html += '<tr><td colspan="2" style="padding: 6px; pointer-events: none"><div style="border-top: 1px solid silver;"></div></td></tr>';
+ }
+ }
+ options.items[f] = mitem;
+ }
+ if (count === 0) {
+ menu_html += '<tr><td style="padding: 13px; color: #999; text-align: center">'+ options.msgNoItems +'</div></td></tr>';
+ }
+ menu_html += "</table>";
+ return menu_html;
+ }
+ };
+})();
+
+
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2grid - grid widget
+* - $().w2grid - jQuery wrapper
+* - Dependencies: jQuery, w2utils, w2toolbar, w2fields, w2alert, w2confirm
+*
+* == NICE TO HAVE ==
+* - frozen columns
+* - add colspans
+* - allow this.total to be unknown (-1)
+* - column autosize based on largest content
+* - easy bubbles in the grid
+* - More than 2 layers of header groups
+* - reorder columns/records
+* - hidden searches could not be clearned by the user
+* - problem with .set() and arrays, array get extended too, but should be replaced
+* - move events into prototype
+* - add grid.focus()
+* - add showExtra, KickIn Infinite scroll when so many records
+* - after edit stay on the same record option
+* - allow render: function to be filters
+*
+************************************************************************/
+
+(function () {
+ var w2grid = function(options) {
+
+ // public properties
+ this.name = null;
+ this.box = null; // HTML element that hold this element
+ this.header = '';
+ this.url = '';
+ this.routeData = {}; // data for dynamic routes
+ this.columns = []; // { field, caption, size, attr, render, hidden, gridMinWidth, editable }
+ this.columnGroups = []; // { span: int, caption: 'string', master: true/false }
+ this.records = []; // { recid: int(requied), field1: 'value1', ... fieldN: 'valueN', style: 'string', editable: true/false, summary: true/false, changes: object }
+ this.summary = []; // arry of summary records, same structure as records array
+ this.searches = []; // { type, caption, field, inTag, outTag, hidden }
+ this.searchData = [];
+ this.sortData = [];
+ this.postData = {};
+ this.toolbar = {}; // if not empty object; then it is toolbar object
+
+ this.show = {
+ header : false,
+ toolbar : false,
+ footer : false,
+ columnHeaders : true,
+ lineNumbers : false,
+ expandColumn : false,
+ selectColumn : false,
+ emptyRecords : true,
+ toolbarReload : true,
+ toolbarColumns : true,
+ toolbarSearch : true,
+ toolbarAdd : false,
+ toolbarEdit : false,
+ toolbarDelete : false,
+ toolbarSave : false,
+ selectionBorder : true,
+ recordTitles : true,
+ skipRecords : true
+ };
+
+ this.autoLoad = true; // for infinite scroll
+ this.fixedBody = true; // if false; then grid grows with data
+ this.recordHeight = 24;
+ this.keyboard = true;
+ this.selectType = 'row'; // can be row|cell
+ this.multiSearch = true;
+ this.multiSelect = true;
+ this.multiSort = true;
+ this.reorderColumns = false;
+ this.reorderRows = false;
+ this.markSearch = true;
+
+ this.total = 0; // server total
+ this.limit = 100;
+ this.offset = 0; // how many records to skip (for infinite scroll) when pulling from server
+ this.style = '';
+ this.ranges = [];
+ this.menu = [];
+ this.method = null; // if defined, then overwrited ajax method
+ this.recid = null;
+ this.parser = null;
+
+ // events
+ this.onAdd = null;
+ this.onEdit = null;
+ this.onRequest = null; // called on any server event
+ this.onLoad = null;
+ this.onDelete = null;
+ this.onDeleted = null;
+ this.onSubmit = null;
+ this.onSave = null;
+ this.onSelect = null;
+ this.onUnselect = null;
+ this.onClick = null;
+ this.onDblClick = null;
+ this.onContextMenu = null;
+ this.onMenuClick = null; // when context menu item selected
+ this.onColumnClick = null;
+ this.onColumnResize = null;
+ this.onSort = null;
+ this.onSearch = null;
+ this.onChange = null; // called when editable record is changed
+ this.onRestore = null; // called when editable record is restored
+ this.onExpand = null;
+ this.onCollapse = null;
+ this.onError = null;
+ this.onKeydown = null;
+ this.onToolbar = null; // all events from toolbar
+ this.onColumnOnOff = null;
+ this.onCopy = null;
+ this.onPaste = null;
+ this.onSelectionExtend = null;
+ this.onEditField = null;
+ this.onRender = null;
+ this.onRefresh = null;
+ this.onReload = null;
+ this.onResize = null;
+ this.onDestroy = null;
+ this.onStateSave = null;
+ this.onStateRestore = null;
+
+ // internal
+ this.last = {
+ field : 'all',
+ caption : w2utils.lang('All Fields'),
+ logic : 'OR',
+ search : '',
+ searchIds : [],
+ selection : {
+ indexes : [],
+ columns : {}
+ },
+ multi : false,
+ scrollTop : 0,
+ scrollLeft : 0,
+ sortData : null,
+ sortCount : 0,
+ xhr : null,
+ range_start : null,
+ range_end : null,
+ sel_ind : null,
+ sel_col : null,
+ sel_type : null,
+ edit_col : null
+ };
+
+ $.extend(true, this, w2obj.grid, options);
+ };
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2grid = function(method) {
+ if (typeof method === 'object' || !method ) {
+ // check name parameter
+ if (!w2utils.checkName(method, 'w2grid')) return;
+ // remember items
+ var columns = method.columns;
+ var columnGroups = method.columnGroups;
+ var records = method.records;
+ var searches = method.searches;
+ var searchData = method.searchData;
+ var sortData = method.sortData;
+ var postData = method.postData;
+ var toolbar = method.toolbar;
+ // extend items
+ var object = new w2grid(method);
+ $.extend(object, { postData: {}, records: [], columns: [], searches: [], toolbar: {}, sortData: [], searchData: [], handlers: [] });
+ if (object.onExpand != null) object.show.expandColumn = true;
+ $.extend(true, object.toolbar, toolbar);
+ // reassign variables
+ for (var p in columns) object.columns[p] = $.extend(true, {}, columns[p]);
+ for (var p in columnGroups) object.columnGroups[p] = $.extend(true, {}, columnGroups[p]);
+ for (var p in searches) object.searches[p] = $.extend(true, {}, searches[p]);
+ for (var p in searchData) object.searchData[p] = $.extend(true, {}, searchData[p]);
+ for (var p in sortData) object.sortData[p] = $.extend(true, {}, sortData[p]);
+ object.postData = $.extend(true, {}, postData);
+
+ // check if there are records without recid
+ for (var r in records) {
+ if (records[r].recid == null || typeof records[r].recid == 'undefined') {
+ console.log('ERROR: Cannot add records without recid. (obj: '+ object.name +')');
+ return;
+ }
+ object.records[r] = $.extend(true, {}, records[r]);
+ }
+ // add searches
+ for (var c in object.columns) {
+ var col = object.columns[c];
+ if (typeof col.searchable == 'undefined' || object.getSearch(col.field) != null) continue;
+ var stype = col.searchable;
+ var attr = '';
+ if (col.searchable === true) { stype = 'text'; attr = 'size="20"'; }
+ object.addSearch({ field: col.field, caption: col.caption, type: stype, attr: attr });
+ }
+ // init toolbar
+ object.initToolbar();
+ // render if necessary
+ if ($(this).length !== 0) {
+ object.render($(this)[0]);
+ }
+ // register new object
+ w2ui[object.name] = object;
+ return object;
+
+ } else if (w2ui[$(this).attr('name')]) {
+ var obj = w2ui[$(this).attr('name')];
+ obj[method].apply(obj, Array.prototype.slice.call(arguments, 1));
+ return this;
+ } else {
+ console.log('ERROR: Method ' + method + ' does not exist on jQuery.w2grid');
+ }
+ }
+
+ // ====================================================
+ // -- Implementation of core functionality
+
+ w2grid.prototype = {
+ // ----
+ // properties that need to be in prototype
+
+ msgDelete : 'Are you sure you want to delete selected records?',
+ msgNotJSON : 'Returned data is not in valid JSON format.',
+ msgAJAXerror : 'AJAX error. See console for more details.',
+ msgRefresh : 'Refreshing...',
+
+ // for easy button overwrite
+ buttons: {
+ 'reload' : { type: 'button', id: 'w2ui-reload', icon: 'w2ui-icon-reload', hint: 'Reload data in the list' },
+ 'columns' : { type: 'drop', id: 'w2ui-column-on-off', icon: 'w2ui-icon-columns', hint: 'Show/hide columns', arrow: false, html: '' },
+ 'search' : { type: 'html', id: 'w2ui-search',
+ html: '<div class="w2ui-icon icon-search-down w2ui-search-down" title="'+ 'Select Search Field' +'" '+
+ 'onclick="var obj = w2ui[$(this).parents(\'div.w2ui-grid\').attr(\'name\')]; obj.searchShowFields();"></div>'
+ },
+ 'search-go': { type: 'check', id: 'w2ui-search-advanced', caption: 'Search...', hint: 'Open Search Fields' },
+ 'add' : { type: 'button', id: 'w2ui-add', caption: 'Add New', hint: 'Add new record', icon: 'w2ui-icon-plus' },
+ 'edit' : { type: 'button', id: 'w2ui-edit', caption: 'Edit', hint: 'Edit selected record', icon: 'w2ui-icon-pencil', disabled: true },
+ 'delete' : { type: 'button', id: 'w2ui-delete', caption: 'Delete', hint: 'Delete selected records', icon: 'w2ui-icon-cross', disabled: true },
+ 'save' : { type: 'button', id: 'w2ui-save', caption: 'Save', hint: 'Save changed records', icon: 'w2ui-icon-check' }
+ },
+
+ add: function (record) {
+ if (!$.isArray(record)) record = [record];
+ var added = 0;
+ for (var o in record) {
+ if (!this.recid && typeof record[o].recid == 'undefined') record[o].recid = record[o][this.recid];
+ if (record[o].recid == null || typeof record[o].recid == 'undefined') {
+ console.log('ERROR: Cannot add record without recid. (obj: '+ this.name +')');
+ continue;
+ }
+ this.records.push(record[o]);
+ added++;
+ }
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (!url) {
+ this.total = this.records.length;
+ this.localSort();
+ this.localSearch();
+ }
+ this.refresh(); // ?? should it be reload?
+ return added;
+ },
+
+ find: function (obj, returnIndex) {
+ if (typeof obj == 'undefined' || obj == null) obj = {};
+ var recs = [];
+ var hasDots = false;
+ // check if property is nested - needed for speed
+ for (var o in obj) if (String(o).indexOf('.') != -1) hasDots = true;
+ // look for an item
+ for (var i = 0; i < this.records.length; i++) {
+ var match = true;
+ for (var o in obj) {
+ var val = this.records[i][o];
+ if (hasDots && String(o).indexOf('.') != -1) val = this.parseField(this.records[i], o);
+ if (obj[o] != val) match = false;
+ }
+ if (match && returnIndex !== true) recs.push(this.records[i].recid);
+ if (match && returnIndex === true) recs.push(i);
+ }
+ return recs;
+ },
+
+ set: function (recid, record, noRefresh) { // does not delete existing, but overrides on top of it
+ if (typeof recid == 'object') {
+ noRefresh = record;
+ record = recid;
+ recid = null;
+ }
+ // update all records
+ if (recid == null) {
+ for (var r in this.records) {
+ $.extend(true, this.records[r], record); // recid is the whole record
+ }
+ if (noRefresh !== true) this.refresh();
+ } else { // find record to update
+ var ind = this.get(recid, true);
+ if (ind == null) return false;
+ $.extend(true, this.records[ind], record);
+ if (noRefresh !== true) this.refreshRow(recid); // refresh only that record
+ }
+ return true;
+ },
+
+ get: function (recid, returnIndex) {
+ for (var i = 0; i < this.records.length; i++) {
+ if (this.records[i].recid == recid) {
+ if (returnIndex === true) return i; else return this.records[i];
+ }
+ }
+ return null;
+ },
+
+ remove: function () {
+ var removed = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.records.length-1; r >= 0; r--) {
+ if (this.records[r].recid == arguments[a]) { this.records.splice(r, 1); removed++; }
+ }
+ }
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (!url) {
+ this.localSort();
+ this.localSearch();
+ }
+ this.refresh();
+ return removed;
+ },
+
+ addColumn: function (before, columns) {
+ var added = 0;
+ if (arguments.length == 1) {
+ columns = before;
+ before = this.columns.length;
+ } else {
+ if (typeof before == 'string') before = this.getColumn(before, true);
+ if (before === null) before = this.columns.length;
+ }
+ if (!$.isArray(columns)) columns = [columns];
+ for (var o in columns) {
+ this.columns.splice(before, 0, columns[o]);
+ before++;
+ added++;
+ }
+ this.refresh();
+ return added;
+ },
+
+ removeColumn: function () {
+ var removed = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.columns.length-1; r >= 0; r--) {
+ if (this.columns[r].field == arguments[a]) { this.columns.splice(r, 1); removed++; }
+ }
+ }
+ this.refresh();
+ return removed;
+ },
+
+ getColumn: function (field, returnIndex) {
+ for (var i = 0; i < this.columns.length; i++) {
+ if (this.columns[i].field == field) {
+ if (returnIndex === true) return i; else return this.columns[i];
+ }
+ }
+ return null;
+ },
+
+ toggleColumn: function () {
+ var effected = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.columns.length-1; r >= 0; r--) {
+ var col = this.columns[r];
+ if (col.field == arguments[a]) {
+ col.hidden = !col.hidden;
+ effected++;
+ }
+ }
+ }
+ this.refresh();
+ return effected;
+ },
+
+ showColumn: function () {
+ var shown = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.columns.length-1; r >= 0; r--) {
+ var col = this.columns[r];
+ if (col.gridMinWidth) delete col.gridMinWidth;
+ if (col.field == arguments[a] && col.hidden !== false) {
+ col.hidden = false;
+ shown++;
+ }
+ }
+ }
+ this.refresh();
+ return shown;
+ },
+
+ hideColumn: function () {
+ var hidden = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.columns.length-1; r >= 0; r--) {
+ var col = this.columns[r];
+ if (col.field == arguments[a] && col.hidden !== true) {
+ col.hidden = true;
+ hidden++;
+ }
+ }
+ }
+ this.refresh();
+ return hidden;
+ },
+
+ addSearch: function (before, search) {
+ var added = 0;
+ if (arguments.length == 1) {
+ search = before;
+ before = this.searches.length;
+ } else {
+ if (typeof before == 'string') before = this.getSearch(before, true);
+ if (before === null) before = this.searches.length;
+ }
+ if (!$.isArray(search)) search = [search];
+ for (var o in search) {
+ this.searches.splice(before, 0, search[o]);
+ before++;
+ added++;
+ }
+ this.searchClose();
+ return added;
+ },
+
+ removeSearch: function () {
+ var removed = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.searches.length-1; r >= 0; r--) {
+ if (this.searches[r].field == arguments[a]) { this.searches.splice(r, 1); removed++; }
+ }
+ }
+ this.searchClose();
+ return removed;
+ },
+
+ getSearch: function (field, returnIndex) {
+ for (var i = 0; i < this.searches.length; i++) {
+ if (this.searches[i].field == field) {
+ if (returnIndex === true) return i; else return this.searches[i];
+ }
+ }
+ return null;
+ },
+
+ toggleSearch: function () {
+ var effected = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.searches.length-1; r >= 0; r--) {
+ if (this.searches[r].field == arguments[a]) {
+ this.searches[r].hidden = !this.searches[r].hidden;
+ effected++;
+ }
+ }
+ }
+ this.searchClose();
+ return effected;
+ },
+
+ showSearch: function () {
+ var shown = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.searches.length-1; r >= 0; r--) {
+ if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== false) {
+ this.searches[r].hidden = false;
+ shown++;
+ }
+ }
+ }
+ this.searchClose();
+ return shown;
+ },
+
+ hideSearch: function () {
+ var hidden = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ for (var r = this.searches.length-1; r >= 0; r--) {
+ if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== true) {
+ this.searches[r].hidden = true;
+ hidden++;
+ }
+ }
+ }
+ this.searchClose();
+ return hidden;
+ },
+
+ getSearchData: function (field) {
+ for (var s in this.searchData) {
+ if (this.searchData[s].field == field) return this.searchData[s];
+ }
+ return null;
+ },
+
+ localSort: function (silent) {
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (url) {
+ console.log('ERROR: grid.localSort can only be used on local data source, grid.url should be empty.');
+ return;
+ }
+ if ($.isEmptyObject(this.sortData)) return;
+ var time = (new Date()).getTime();
+ var obj = this;
+ // process date fields
+ obj.prepareData();
+ // process sortData
+ for (var s in this.sortData) {
+ var column = this.getColumn(this.sortData[s].field);
+ if (!column) return;
+ if (column.render && ['date', 'age'].indexOf(column.render.split(':')[0]) != -1) {
+ this.sortData[s]['field_'] = column.field + '_';
+ }
+ if (column.render && ['time'].indexOf(column.render.split(':')[0]) != -1) {
+ this.sortData[s]['field_'] = column.field + '_';
+ }
+ }
+ // process sort
+ this.records.sort(function (a, b) {
+ var ret = 0;
+ for (var s in obj.sortData) {
+ var fld = obj.sortData[s].field;
+ if (obj.sortData[s].field_) fld = obj.sortData[s].field_;
+ var aa = a[fld];
+ var bb = b[fld];
+ if (String(fld).indexOf('.') != -1) {
+ aa = obj.parseField(a, fld);
+ bb = obj.parseField(b, fld);
+ }
+ if (typeof aa == 'string') aa = $.trim(aa.toLowerCase());
+ if (typeof bb == 'string') bb = $.trim(bb.toLowerCase());
+ if (aa > bb) ret = (obj.sortData[s].direction == 'asc' ? 1 : -1);
+ if (aa < bb) ret = (obj.sortData[s].direction == 'asc' ? -1 : 1);
+ if (typeof aa != 'object' && typeof bb == 'object') ret = -1;
+ if (typeof bb != 'object' && typeof aa == 'object') ret = 1;
+ if (aa == null && bb != null) ret = 1; // all nuls and undefined on bottom
+ if (aa != null && bb == null) ret = -1;
+ if (ret != 0) break;
+ }
+ return ret;
+ });
+ time = (new Date()).getTime() - time;
+ if (silent !== true) setTimeout(function () { obj.status('Sorting took ' + time/1000 + ' sec'); }, 10);
+ return time;
+ },
+
+ localSearch: function (silent) {
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (url) {
+ console.log('ERROR: grid.localSearch can only be used on local data source, grid.url should be empty.');
+ return;
+ }
+ var time = (new Date()).getTime();
+ var obj = this;
+ this.total = this.records.length;
+ // mark all records as shown
+ this.last.searchIds = [];
+ // prepare date/time fields
+ this.prepareData();
+ // hide records that did not match
+ if (this.searchData.length > 0 && !url) {
+ this.total = 0;
+ for (var r in this.records) {
+ var rec = this.records[r];
+ var fl = 0;
+ for (var s in this.searchData) {
+ var sdata = this.searchData[s];
+ var search = this.getSearch(sdata.field);
+ if (sdata == null) continue;
+ if (search == null) search = { field: sdata.field, type: sdata.type };
+ var val1 = String(obj.parseField(rec, search.field)).toLowerCase();
+ if (typeof sdata.value != 'undefined') {
+ if (!$.isArray(sdata.value)) {
+ var val2 = String(sdata.value).toLowerCase();
+ } else {
+ var val2 = sdata.value[0];
+ var val3 = sdata.value[1];
+ }
+ }
+ switch (sdata.operator) {
+ case 'is':
+ if (rec[search.field] == sdata.value) fl++; // do not hide record
+ if (search.type == 'date') {
+ var val1 = w2utils.formatDate(rec[search.field + '_'], 'yyyy-mm-dd');
+ var val2 = w2utils.formatDate(val2, 'yyyy-mm-dd');
+ if (val1 == val2) fl++;
+ }
+ if (search.type == 'time') {
+ var val1 = w2utils.formatTime(rec[search.field + '_'], 'h24:mi');
+ var val2 = w2utils.formatTime(val2, 'h24:mi');
+ if (val1 == val2) fl++;
+ }
+ break;
+ case 'between':
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) {
+ if (parseFloat(rec[search.field]) >= parseFloat(val2) && parseFloat(rec[search.field]) <= parseFloat(val3)) fl++;
+ }
+ if (search.type == 'date') {
+ var val1 = rec[search.field + '_'];
+ var val2 = w2utils.isDate(val2, w2utils.settings.date_format, true);
+ var val3 = w2utils.isDate(val3, w2utils.settings.date_format, true);
+ if (val3 != null) val3 = new Date(val3.getTime() + 86400000); // 1 day
+ if (val1 >= val2 && val1 < val3) fl++;
+ }
+ if (search.type == 'time') {
+ var val1 = rec[search.field + '_'];
+ var val2 = w2utils.isTime(val2, true);
+ var val3 = w2utils.isTime(val3, true);
+ val2 = (new Date()).setHours(val2.hours, val2.minutes, val2.seconds ? val2.seconds : 0, 0);
+ val3 = (new Date()).setHours(val3.hours, val3.minutes, val3.seconds ? val3.seconds : 0, 0);
+ if (val1 >= val2 && val1 < val3) fl++;
+ }
+ break;
+ case 'in':
+ var tmp = sdata.value;
+ if (sdata.svalue) tmp = sdata.svalue;
+ if (tmp.indexOf(val1) !== -1) fl++;
+ break;
+ case 'not in':
+ var tmp = sdata.value;
+ if (sdata.svalue) tmp = sdata.svalue;
+ if (tmp.indexOf(val1) == -1) fl++;
+ break;
+ case 'begins':
+ case 'begins with': // need for back compatib.
+ if (val1.indexOf(val2) == 0) fl++; // do not hide record
+ break;
+ case 'contains':
+ if (val1.indexOf(val2) >= 0) fl++; // do not hide record
+ break;
+ case 'ends':
+ case 'ends with': // need for back compatib.
+ if (val1.indexOf(val2) == val1.length - val2.length) fl++; // do not hide record
+ break;
+ }
+ }
+ if ((this.last.logic == 'OR' && fl != 0) || (this.last.logic == 'AND' && fl == this.searchData.length)) this.last.searchIds.push(parseInt(r));
+ }
+ this.total = this.last.searchIds.length;
+ }
+ time = (new Date()).getTime() - time;
+ if (silent !== true) setTimeout(function () { obj.status('Search took ' + time/1000 + ' sec'); }, 10);
+ return time;
+ },
+
+ getRangeData: function (range, extra) {
+ var rec1 = this.get(range[0].recid, true);
+ var rec2 = this.get(range[1].recid, true);
+ var col1 = range[0].column;
+ var col2 = range[1].column;
+
+ var res = [];
+ if (col1 == col2) { // one row
+ for (var r = rec1; r <= rec2; r++) {
+ var record = this.records[r];
+ var dt = record[this.columns[col1].field] || null;
+ if (extra !== true) {
+ res.push(dt);
+ } else {
+ res.push({ data: dt, column: col1, index: r, record: record });
+ }
+ }
+ } else if (rec1 == rec2) { // one line
+ var record = this.records[rec1];
+ for (var i = col1; i <= col2; i++) {
+ var dt = record[this.columns[i].field] || null;
+ if (extra !== true) {
+ res.push(dt);
+ } else {
+ res.push({ data: dt, column: i, index: rec1, record: record });
+ }
+ }
+ } else {
+ for (var r = rec1; r <= rec2; r++) {
+ var record = this.records[r];
+ res.push([]);
+ for (var i = col1; i <= col2; i++) {
+ var dt = record[this.columns[i].field];
+ if (extra !== true) {
+ res[res.length-1].push(dt);
+ } else {
+ res[res.length-1].push({ data: dt, column: i, index: r, record: record });
+ }
+ }
+ }
+ }
+ return res;
+ },
+
+ addRange: function (ranges) {
+ var added = 0;
+ if (this.selectType == 'row') return added;
+ if (!$.isArray(ranges)) ranges = [ranges];
+ // if it is selection
+ for (var r in ranges) {
+ if (typeof ranges[r] != 'object') ranges[r] = { name: 'selection' };
+ if (ranges[r].name == 'selection') {
+ if (this.show.selectionBorder === false) continue;
+ var sel = this.getSelection();
+ if (sel.length == 0) {
+ this.removeRange(ranges[r].name);
+ continue;
+ } else {
+ var first = sel[0];
+ var last = sel[sel.length-1];
+ var td1 = $('#grid_'+ this.name +'_rec_'+ first.recid + ' td[col='+ first.column +']');
+ var td2 = $('#grid_'+ this.name +'_rec_'+ last.recid + ' td[col='+ last.column +']');
+ }
+ } else { // other range
+ var first = ranges[r].range[0];
+ var last = ranges[r].range[1];
+ var td1 = $('#grid_'+ this.name +'_rec_'+ first.recid + ' td[col='+ first.column +']');
+ var td2 = $('#grid_'+ this.name +'_rec_'+ last.recid + ' td[col='+ last.column +']');
+ }
+ if (first) {
+ var rg = {
+ name: ranges[r].name,
+ range: [{ recid: first.recid, column: first.column }, { recid: last.recid, column: last.column }],
+ style: ranges[r].style || ''
+ };
+ // add range
+ var ind = false;
+ for (var t in this.ranges) if (this.ranges[t].name == ranges[r].name) { ind = r; break; }
+ if (ind !== false) {
+ this.ranges[ind] = rg;
+ } else {
+ this.ranges.push(rg);
+ }
+ added++
+ }
+ }
+ this.refreshRanges();
+ return added;
+ },
+
+ removeRange: function () {
+ var removed = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ var name = arguments[a];
+ $('#grid_'+ this.name +'_'+ name).remove();
+ for (var r = this.ranges.length-1; r >= 0; r--) {
+ if (this.ranges[r].name == name) {
+ this.ranges.splice(r, 1);
+ removed++;
+ }
+ }
+ }
+ return removed;
+ },
+
+ refreshRanges: function () {
+ var obj = this;
+ var time = (new Date()).getTime();
+ var rec = $('#grid_'+ this.name +'_records');
+ for (var r in this.ranges) {
+ var rg = this.ranges[r];
+ var first = rg.range[0];
+ var last = rg.range[1];
+ var td1 = $('#grid_'+ this.name +'_rec_'+ first.recid + ' td[col='+ first.column +']');
+ var td2 = $('#grid_'+ this.name +'_rec_'+ last.recid + ' td[col='+ last.column +']');
+ if ($('#grid_'+ this.name +'_'+ rg.name).length == 0) {
+ rec.append('<div id="grid_'+ this.name +'_' + rg.name +'" class="w2ui-selection" style="'+ rg.style +'">'+
+ (rg.name == 'selection' ? '<div id="grid_'+ this.name +'_resizer" class="w2ui-selection-resizer"></div>' : '')+
+ '</div>');
+ } else {
+ $('#grid_'+ this.name +'_'+ rg.name).attr('style', rg.style);
+ }
+ if (td1.length > 0 && td2.length > 0) {
+ $('#grid_'+ this.name +'_'+ rg.name).css({
+ left : (td1.position().left - 1 + rec.scrollLeft()) + 'px',
+ top : (td1.position().top - 1 + rec.scrollTop()) + 'px',
+ width : (td2.position().left - td1.position().left + td2.width() + 3) + 'px',
+ height : (td2.position().top - td1.position().top + td2.height() + 3) + 'px'
+ });
+ }
+ }
+
+ // add resizer events
+ $(this.box).find('#grid_'+ this.name +'_resizer').off('mousedown').on('mousedown', mouseStart);
+ //$(this.box).find('#grid_'+ this.name +'_resizer').off('selectstart').on('selectstart', function () { return false; }); // fixes chrome cursror bug
+
+ var eventData = { phase: 'before', type: 'selectionExtend', target: obj.name, originalRange: null, newRange: null };
+
+ function mouseStart (event) {
+ var sel = obj.getSelection();
+ obj.last.move = {
+ type : 'expand',
+ x : event.screenX,
+ y : event.screenY,
+ divX : 0,
+ divY : 0,
+ recid : sel[0].recid,
+ column : sel[0].column,
+ originalRange : [{ recid: sel[0].recid, column: sel[0].column }, { recid: sel[sel.length-1].recid, column: sel[sel.length-1].column }],
+ newRange : [{ recid: sel[0].recid, column: sel[0].column }, { recid: sel[sel.length-1].recid, column: sel[sel.length-1].column }]
+ };
+ $(document).off('mousemove', mouseMove).on('mousemove', mouseMove);
+ $(document).off('mouseup', mouseStop).on('mouseup', mouseStop);
+ }
+
+ function mouseMove (event) {
+ var mv = obj.last.move;
+ if (!mv || mv.type != 'expand') return;
+ mv.divX = (event.screenX - mv.x);
+ mv.divY = (event.screenY - mv.y);
+ // find new cell
+ var recid, column;
+ var tmp = event.originalEvent.target;
+ if (tmp.tagName != 'TD') tmp = $(tmp).parents('td')[0];
+ if (typeof $(tmp).attr('col') != 'undefined') column = parseInt($(tmp).attr('col'));
+ tmp = $(tmp).parents('tr')[0];
+ recid = $(tmp).attr('recid');
+ // new range
+ if (mv.newRange[1].recid == recid && mv.newRange[1].column == column) return;
+ var prevNewRange = $.extend({}, mv.newRange);
+ mv.newRange = [{ recid: mv.recid, column: mv.column }, { recid: recid, column: column }];
+ // event before
+ eventData = obj.trigger($.extend(eventData, { originalRange: mv.originalRange, newRange : mv.newRange }));
+ if (eventData.isCancelled === true) {
+ mv.newRange = prevNewRange;
+ eventData.newRange = prevNewRange;
+ return;
+ } else {
+ // default behavior
+ obj.removeRange('grid-selection-expand');
+ obj.addRange({
+ name : 'grid-selection-expand',
+ range : eventData.newRange,
+ style : 'background-color: rgba(100,100,100,0.1); border: 2px dotted rgba(100,100,100,0.5);'
+ });
+ }
+ }
+
+ function mouseStop (event) {
+ // default behavior
+ obj.removeRange('grid-selection-expand');
+ delete obj.last.move;
+ $(document).off('mousemove', mouseMove);
+ $(document).off('mouseup', mouseStop);
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+
+ return (new Date()).getTime() - time;
+ },
+
+ select: function () {
+ var selected = 0;
+ var sel = this.last.selection;
+ if (!this.multiSelect) this.selectNone();
+ for (var a = 0; a < arguments.length; a++) {
+ var recid = typeof arguments[a] == 'object' ? arguments[a].recid : arguments[a];
+ var record = this.get(recid);
+ if (record == null) continue;
+ var index = this.get(recid, true);
+ var recEl = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid));
+ if (this.selectType == 'row') {
+ if (sel.indexes.indexOf(index) >= 0) continue;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'select', target: this.name, recid: recid, index: index });
+ if (eventData.isCancelled === true) continue;
+ // default action
+ sel.indexes.push(index);
+ sel.indexes.sort(function(a, b) { return a-b });
+ recEl.addClass('w2ui-selected').data('selected', 'yes');
+ recEl.find('.w2ui-grid-select-check').prop("checked", true);
+ selected++;
+ } else {
+ var col = arguments[a].column;
+ if (!w2utils.isInt(col)) { // select all columns
+ var cols = [];
+ for (var c in this.columns) { if (this.columns[c].hidden) continue; cols.push({ recid: recid, column: parseInt(c) }); }
+ if (!this.multiSelect) cols = cols.splice(0, 1);
+ return this.select.apply(this, cols);
+ }
+ var s = sel.columns[index] || [];
+ if ($.isArray(s) && s.indexOf(col) != -1) continue;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'select', target: this.name, recid: recid, index: index, column: col });
+ if (eventData.isCancelled === true) continue;
+ // default action
+ if (sel.indexes.indexOf(index) == -1) {
+ sel.indexes.push(index);
+ sel.indexes.sort(function(a, b) { return a-b });
+ }
+ s.push(col);
+ s.sort(function(a, b) { return a-b }); // sort function must be for numerical sort
+ recEl.find(' > td[col='+ col +']').addClass('w2ui-selected');
+ selected++;
+ recEl.data('selected', 'yes');
+ recEl.find('.w2ui-grid-select-check').prop("checked", true);
+ // save back to selection object
+ sel.columns[index] = s;
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ // all selected?
+ if (sel.indexes.length == this.records.length || (this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)) {
+ $('#grid_'+ this.name +'_check_all').prop('checked', true);
+ } else {
+ $('#grid_'+ this.name +'_check_all').prop('checked', false);
+ }
+ this.status();
+ this.addRange('selection');
+ return selected;
+ },
+
+ unselect: function () {
+ var unselected = 0;
+ var sel = this.last.selection;
+ for (var a = 0; a < arguments.length; a++) {
+ var recid = typeof arguments[a] == 'object' ? arguments[a].recid : arguments[a];
+ var record = this.get(recid);
+ if (record == null) continue;
+ var index = this.get(record.recid, true);
+ var recEl = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid));
+ if (this.selectType == 'row') {
+ if (sel.indexes.indexOf(index) == -1) continue;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'unselect', target: this.name, recid: recid, index: index });
+ if (eventData.isCancelled === true) continue;
+ // default action
+ sel.indexes.splice(sel.indexes.indexOf(index), 1);
+ recEl.removeClass('w2ui-selected').removeData('selected');
+ if (recEl.length != 0) recEl[0].style.cssText = 'height: '+ this.recordHeight +'px; ' + recEl.attr('custom_style');
+ recEl.find('.w2ui-grid-select-check').prop("checked", false);
+ unselected++;
+ } else {
+ var col = arguments[a].column;
+ if (!w2utils.isInt(col)) { // unselect all columns
+ var cols = [];
+ for (var c in this.columns) { if (this.columns[c].hidden) continue; cols.push({ recid: recid, column: parseInt(c) }); }
+ return this.unselect.apply(this, cols);
+ }
+ var s = sel.columns[index];
+ if (!$.isArray(s) || s.indexOf(col) == -1) continue;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'unselect', target: this.name, recid: recid, column: col });
+ if (eventData.isCancelled === true) continue;
+ // default action
+ s.splice(s.indexOf(col), 1);
+ $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid) + ' > td[col='+ col +']').removeClass('w2ui-selected');
+ unselected++;
+ if (s.length == 0) {
+ delete sel.columns[index];
+ sel.indexes.splice(sel.indexes.indexOf(index), 1);
+ recEl.removeData('selected');
+ recEl.find('.w2ui-grid-select-check').prop("checked", false);
+ }
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ // all selected?
+ if (sel.indexes.length == this.records.length || (this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)) {
+ $('#grid_'+ this.name +'_check_all').prop('checked', true);
+ } else {
+ $('#grid_'+ this.name +'_check_all').prop('checked', false);
+ }
+ // show number of selected
+ this.status();
+ this.addRange('selection');
+ return unselected;
+ },
+
+ selectAll: function () {
+ if (this.multiSelect === false) return;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'select', target: this.name, all: true });
+ if (eventData.isCancelled === true) return;
+ // default action
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ var sel = this.last.selection;
+ var cols = [];
+ for (var c in this.columns) cols.push(parseInt(c));
+ // if local data source and searched
+ sel.indexes = [];
+ if (!url && this.searchData.length !== 0) {
+ // local search applied
+ for (var i = 0; i < this.last.searchIds.length; i++) {
+ sel.indexes.push(this.last.searchIds[i]);
+ if (this.selectType != 'row') sel.columns[this.last.searchIds[i]] = cols.slice(); // .slice makes copy of the array
+ }
+ } else {
+ var buffered = this.records.length;
+ if (this.searchData.length != 0 && !this.url) buffered = this.last.searchIds.length;
+ for (var i = 0; i < buffered; i++) {
+ sel.indexes.push(i);
+ if (this.selectType != 'row') sel.columns[i] = cols.slice(); // .slice makes copy of the array
+ }
+ }
+ this.refresh();
+ // enable/disable toolbar buttons
+ var sel = this.getSelection();
+ if (sel.length == 1) this.toolbar.enable('w2ui-edit'); else this.toolbar.disable('w2ui-edit');
+ if (sel.length >= 1) this.toolbar.enable('w2ui-delete'); else this.toolbar.disable('w2ui-delete');
+ this.addRange('selection');
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ selectNone: function () {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'unselect', target: this.name, all: true });
+ if (eventData.isCancelled === true) return;
+ // default action
+ var sel = this.last.selection;
+ for (var s in sel.indexes) {
+ var index = sel.indexes[s];
+ var rec = this.records[index];
+ var recid = rec ? rec.recid : null;
+ var recEl = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid));
+ recEl.removeClass('w2ui-selected').removeData('selected');
+ recEl.find('.w2ui-grid-select-check').prop("checked", false);
+ // for not rows
+ if (this.selectType != 'row') {
+ var cols = sel.columns[index];
+ for (var c in cols) recEl.find(' > td[col='+ cols[c] +']').removeClass('w2ui-selected');
+ }
+ }
+ sel.indexes = [];
+ sel.columns = {};
+ this.toolbar.disable('w2ui-edit', 'w2ui-delete');
+ this.removeRange('selection');
+ $('#grid_'+ this.name +'_check_all').prop('checked', false);
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ getSelection: function (returnIndex) {
+ var ret = [];
+ var sel = this.last.selection;
+ if (this.selectType == 'row') {
+ for (var s in sel.indexes) {
+ if (!this.records[sel.indexes[s]]) continue;
+ if (returnIndex === true) ret.push(sel.indexes[s]); else ret.push(this.records[sel.indexes[s]].recid);
+ }
+ return ret;
+ } else {
+ for (var s in sel.indexes) {
+ var cols = sel.columns[sel.indexes[s]];
+ if (!this.records[sel.indexes[s]]) continue;
+ for (var c in cols) {
+ ret.push({ recid: this.records[sel.indexes[s]].recid, index: parseInt(sel.indexes[s]), column: cols[c] });
+ }
+ }
+ return ret;
+ }
+ },
+
+ search: function (field, value) {
+ var obj = this;
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ var searchData = [];
+ var last_multi = this.last.multi;
+ var last_logic = this.last.logic;
+ var last_field = this.last.field;
+ var last_search = this.last.search;
+ // 1: search() - advanced search (reads from popup)
+ if (arguments.length == 0) {
+ last_search = '';
+ // advanced search
+ for (var s in this.searches) {
+ var search = this.searches[s];
+ var operator = $('#grid_'+ this.name + '_operator_'+s).val();
+ var field1 = $('#grid_'+ this.name + '_field_'+s);
+ var field2 = $('#grid_'+ this.name + '_field2_'+s);
+ var value1 = field1.val();
+ var value2 = field2.val();
+ var svalue = null;
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) {
+ var fld1 = field1.data('w2field');
+ var fld2 = field2.data('w2field');
+ if (fld1) value1 = fld1.clean(value1);
+ if (fld2) value2 = fld2.clean(value2);
+ }
+ if (['list', 'enum'].indexOf(search.type) != -1) {
+ value1 = field1.data('selected') || {};
+ if ($.isArray(value1)) {
+ svalue = [];
+ for (var v in value1) {
+ svalue.push(w2utils.isFloat(value1[v].id) ? parseFloat(value1[v].id) : String(value1[v].id).toLowerCase());
+ delete value1[v].hidden;
+ }
+ } else {
+ value1 = value1.id || '';
+ }
+ }
+ if ((value1 != '' && value1 != null) || (typeof value2 != 'undefined' && value2 != '')) {
+ var tmp = {
+ field : search.field,
+ type : search.type,
+ operator : operator
+ }
+ if (operator == 'between') {
+ $.extend(tmp, { value: [value1, value2] });
+ } else if (operator == 'in' && typeof value1 == 'string') {
+ $.extend(tmp, { value: value1.split(',') });
+ } else if (operator == 'not in' && typeof value1 == 'string') {
+ $.extend(tmp, { value: value1.split(',') });
+ } else {
+ $.extend(tmp, { value: value1 });
+ }
+ if (svalue) $.extend(tmp, { svalue: svalue });
+ // conver date to unix time
+ try {
+ if (search.type == 'date' && operator == 'between') {
+ tmp.value[0] = value1; // w2utils.isDate(value1, w2utils.settings.date_format, true).getTime();
+ tmp.value[1] = value2; // w2utils.isDate(value2, w2utils.settings.date_format, true).getTime();
+ }
+ if (search.type == 'date' && operator == 'is') {
+ tmp.value = value1; // w2utils.isDate(value1, w2utils.settings.date_format, true).getTime();
+ }
+ } catch (e) {
+
+ }
+ searchData.push(tmp);
+ }
+ }
+ if (searchData.length > 0 && !url) {
+ last_multi = true;
+ last_logic = 'AND';
+ } else {
+ last_multi = true;
+ last_logic = 'AND';
+ }
+ }
+ // 2: search(field, value) - regular search
+ if (typeof field == 'string') {
+ last_field = field;
+ last_search = value;
+ last_multi = false;
+ last_logic = 'OR';
+ // loop through all searches and see if it applies
+ if (typeof value != 'undefined') {
+ if (field.toLowerCase() == 'all') {
+ // if there are search fields loop thru them
+ if (this.searches.length > 0) {
+ for (var s in this.searches) {
+ var search = this.searches[s];
+ if (search.type == 'text' || (search.type == 'alphanumeric' && w2utils.isAlphaNumeric(value))
+ || (search.type == 'int' && w2utils.isInt(value)) || (search.type == 'float' && w2utils.isFloat(value))
+ || (search.type == 'percent' && w2utils.isFloat(value)) || (search.type == 'hex' && w2utils.isHex(value))
+ || (search.type == 'currency' && w2utils.isMoney(value)) || (search.type == 'money' && w2utils.isMoney(value))
+ || (search.type == 'date' && w2utils.isDate(value)) ) {
+ var tmp = {
+ field : search.field,
+ type : search.type,
+ operator : (search.type == 'text' ? 'contains' : 'is'),
+ value : value
+ };
+ searchData.push(tmp);
+ }
+ // range in global search box
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1 && String(value).indexOf('-') != -1) {
+ var t = String(value).split('-');
+ var tmp = {
+ field : search.field,
+ type : search.type,
+ operator : 'between',
+ value : [t[0], t[1]]
+ };
+ searchData.push(tmp);
+ }
+ }
+ } else {
+ // no search fields, loop thru columns
+ for (var c in this.columns) {
+ var tmp = {
+ field : this.columns[c].field,
+ type : 'text',
+ operator : 'contains',
+ value : value
+ };
+ searchData.push(tmp);
+ }
+ }
+ } else {
+ var el = $('#grid_'+ this.name +'_search_all');
+ var search = this.getSearch(field);
+ if (search == null) search = { field: field, type: 'text' };
+ if (search.field == field) this.last.caption = search.caption;
+ if (search.type == 'list') {
+ var tmp = el.data('selected');
+ if (tmp && !$.isEmptyObject(tmp)) value = tmp.id;
+ }
+ if (value != '') {
+ var op = 'contains';
+ var val = value;
+ if (['date', 'time', 'list'].indexOf(search.type) != -1) op = 'is';
+ if (search.type == 'int' && value != '') {
+ op = 'is';
+ if (String(value).indexOf('-') != -1) {
+ var tmp = value.split('-');
+ if (tmp.length == 2) {
+ op = 'between';
+ val = [parseInt(tmp[0]), parseInt(tmp[1])];
+ }
+ }
+ if (String(value).indexOf(',') != -1) {
+ var tmp = value.split(',');
+ op = 'in';
+ val = [];
+ for (var t in tmp) val.push(tmp[t]);
+ }
+ }
+ var tmp = {
+ field : search.field,
+ type : search.type,
+ operator : op,
+ value : val
+ }
+ searchData.push(tmp);
+ }
+ }
+ }
+ }
+ // 3: search([ { field, value, [operator,] [type] }, { field, value, [operator,] [type] } ], logic) - submit whole structure
+ if ($.isArray(field)) {
+ var logic = 'AND';
+ if (typeof value == 'string') {
+ logic = value.toUpperCase();
+ if (logic != 'OR' && logic != 'AND') logic = 'AND';
+ }
+ last_search = '';
+ last_multi = true;
+ last_logic = logic;
+ for (var f in field) {
+ var data = field[f];
+ var search = this.getSearch(data.field);
+ if (search == null) search = { type: 'text', operator: 'contains' };
+ // merge current field and search if any
+ searchData.push($.extend(true, {}, search, data));
+ }
+ }
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'search', target: this.name, searchData: searchData,
+ searchField: (field ? field : 'multi'), searchValue: (value ? value : 'multi') });
+ if (eventData.isCancelled === true) return;
+ // default action
+ this.searchData = eventData.searchData;
+ this.last.field = last_field;
+ this.last.search = last_search;
+ this.last.multi = last_multi;
+ this.last.logic = last_logic;
+ this.last.scrollTop = 0;
+ this.last.scrollLeft = 0;
+ this.last.selection.indexes = [];
+ this.last.selection.columns = {};
+ // -- clear all search field
+ this.searchClose();
+ this.set({ expanded: false }, true);
+ // apply search
+ if (url) {
+ this.last.xhr_offset = 0;
+ this.reload();
+ } else {
+ // local search
+ this.localSearch();
+ this.refresh();
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ searchOpen: function () {
+ if (!this.box) return;
+ if (this.searches.length == 0) return;
+ var obj = this;
+ // show search
+ $('#tb_'+ this.name +'_toolbar_item_w2ui-search-advanced').w2overlay(
+ this.getSearchesHTML(), {
+ name : 'searches-'+ this.name,
+ left : -10,
+ 'class' : 'w2ui-grid-searches',
+ onShow : function () {
+ if (obj.last.logic == 'OR') obj.searchData = [];
+ obj.initSearches();
+ $('#w2ui-overlay-searches-'+ this.name +' .w2ui-grid-searches').data('grid-name', obj.name);
+ var sfields = $('#w2ui-overlay-searches-'+ this.name +' .w2ui-grid-searches *[rel=search]');
+ if (sfields.length > 0) sfields[0].focus();
+ }
+ }
+ );
+ },
+
+ searchClose: function () {
+ if (!this.box) return;
+ if (this.searches.length == 0) return;
+ if (this.toolbar) this.toolbar.uncheck('w2ui-search-advanced')
+ // hide search
+ if ($('#w2ui-overlay-searches-'+ this.name +' .w2ui-grid-searches').length > 0) {
+ $().w2overlay('', { name: 'searches-'+ this.name });
+ }
+ },
+
+ searchShowFields: function () {
+ var el = $('#grid_'+ this.name +'_search_all');
+ var html = '<div class="w2ui-select-field"><table>';
+ for (var s = -1; s < this.searches.length; s++) {
+ var search = this.searches[s];
+ if (s == -1) {
+ if (!this.multiSearch) continue;
+ search = { field: 'all', caption: w2utils.lang('All Fields') };
+ } else {
+ if (this.searches[s].hidden === true) continue;
+ }
+ html += '<tr '+ (w2utils.isIOS ? 'onTouchStart' : 'onClick') +'="w2ui[\''+ this.name +'\'].initAllField(\''+ search.field +'\')">'+
+ ' <td><input type="radio" tabIndex="-1" '+ (search.field == this.last.field ? 'checked' : '') +'></td>'+
+ ' <td>'+ search.caption +'</td>'+
+ '</tr>';
+ }
+ html += "</table></div>";
+ // need timer otherwise does nto show with list type
+ setTimeout(function () {
+ $(el).w2overlay(html, { left: -10 });
+ }, 1);
+ },
+
+ initAllField: function (field, value) {
+ var el = $('#grid_'+ this.name +'_search_all');
+ var search = this.getSearch(field);
+ if (field == 'all') {
+ search = { field: 'all', caption: w2utils.lang('All Fields') };
+ el.w2field('clear');
+ el.change().focus();
+ } else {
+ var st = search.type;
+ if (['enum', 'select'].indexOf(st) != -1) st = 'list';
+ el.w2field(st, $.extend({}, search.options, { suffix: '', autoFormat: false, selected: value }));
+ if (['list', 'enum'].indexOf(search.type) != -1) {
+ this.last.search = '';
+ this.last.item = '';
+ el.val('');
+ }
+ // set focus
+ setTimeout(function () {
+ el.focus(); /* do not do el.change() as it will refresh grid and pull from server */
+ }, 1);
+ }
+ // update field
+ if (this.last.search != '') {
+ this.search(search.field, this.last.search);
+ } else {
+ this.last.field = search.field;
+ this.last.caption = search.caption;
+ }
+ el.attr('placeholder', search.caption);
+ $().w2overlay();
+ },
+
+ searchReset: function (noRefresh) {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'search', target: this.name, searchData: [] });
+ if (eventData.isCancelled === true) return;
+ // default action
+ this.searchData = [];
+ this.last.search = '';
+ this.last.logic = 'OR';
+ // --- do not reset to All Fields (I think)
+ // if (this.last.multi) {
+ // if (!this.multiSearch) {
+ // this.last.field = this.searches[0].field;
+ // this.last.caption = this.searches[0].caption;
+ // } else {
+ // this.last.field = 'all';
+ // this.last.caption = w2utils.lang('All Fields');
+ // }
+ // }
+ this.last.multi = false;
+ this.last.xhr_offset = 0;
+ // reset scrolling position
+ this.last.scrollTop = 0;
+ this.last.scrollLeft = 0;
+ this.last.selection.indexes = [];
+ this.last.selection.columns = {};
+ // -- clear all search field
+ this.searchClose();
+ $('#grid_'+ this.name +'_search_all').val('');
+ // apply search
+ if (!noRefresh) this.reload();
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ clear: function (noRefresh) {
+ // this.offset = 0; // clear should not reset offset
+ // this.total = 0; // clear should not reset total
+ this.records = [];
+ this.summary = [];
+ this.last.scrollTop = 0;
+ this.last.scrollLeft = 0;
+ this.last.range_start = null;
+ this.last.range_end = null;
+ // this.last.xhr_offset = 0; // clear should not reset offset
+ if (!noRefresh) this.refresh();
+ },
+
+ reset: function (noRefresh) {
+ // reset last remembered state
+ this.offset = 0;
+ this.total = 0;
+ this.last.scrollTop = 0;
+ this.last.scrollLeft = 0;
+ this.last.selection.indexes = [];
+ this.last.selection.columns = {};
+ this.last.range_start = null;
+ this.last.range_end = null;
+ this.last.xhr_offset = 0;
+ this.searchReset(noRefresh);
+ // initial sort
+ if (this.last.sortData != null ) this.sortData = this.last.sortData;
+ // select none without refresh
+ this.set({ expanded: false }, true);
+ // refresh
+ if (!noRefresh) this.refresh();
+ },
+
+ skip: function (offset) {
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (url) {
+ this.offset = parseInt(offset);
+ if (this.offset > this.total) this.offset = this.total - this.limit;
+ if (this.offset < 0 || !w2utils.isInt(this.offset)) this.offset = 0;
+ this.records = [];
+ this.last.xhr_offset = 0;
+ this.last.pull_more = true;
+ this.last.scrollTop = 0;
+ this.last.scrollLeft = 0;
+ $('#grid_'+ this.name +'_records').prop('scrollTop', 0);
+ this.reload();
+ } else {
+ console.log('ERROR: grid.skip() can only be called when you have remote data source.');
+ }
+ },
+
+ load: function (url, callBack) {
+ if (typeof url == 'undefined') {
+ console.log('ERROR: You need to provide url argument when calling .load() method of "'+ this.name +'" object.');
+ return;
+ }
+ // default action
+ this.request('get-records', {}, url, callBack);
+ },
+
+ reload: function (callBack) {
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (url) {
+ this.clear(true);
+ this.request('get-records', {}, null, callBack);
+ } else {
+ this.last.scrollTop = 0;
+ this.last.scrollLeft = 0;
+ this.last.range_start = null;
+ this.last.range_end = null;
+ this.localSearch();
+ this.refresh();
+ if (typeof callBack == 'function') callBack({ status: 'success' });
+ }
+ },
+
+ request: function (cmd, add_params, url, callBack) {
+ if (typeof add_params == 'undefined') add_params = {};
+ if (typeof url == 'undefined' || url == '' || url == null) url = this.url;
+ if (url == '' || url == null) return;
+ // build parameters list
+ var params = {};
+ if (!w2utils.isInt(this.offset)) this.offset = 0;
+ if (!w2utils.isInt(this.last.xhr_offset)) this.last.xhr_offset = 0;
+ // add list params
+ params['cmd'] = cmd;
+ params['selected'] = this.getSelection();
+ params['limit'] = this.limit;
+ params['offset'] = parseInt(this.offset) + this.last.xhr_offset;
+ params['search'] = this.searchData;
+ params['searchLogic'] = this.last.logic;
+ params['sort'] = this.sortData;
+ if (this.searchData.length == 0) {
+ delete params['search'];
+ delete params['searchLogic'];
+ }
+ if (this.sortData.length == 0) {
+ delete params['sort'];
+ }
+ // append other params
+ $.extend(params, this.postData);
+ $.extend(params, add_params);
+ // event before
+ if (cmd == 'get-records') {
+ var eventData = this.trigger({ phase: 'before', type: 'request', target: this.name, url: url, postData: params });
+ if (eventData.isCancelled === true) { if (typeof callBack == 'function') callBack({ status: 'error', message: 'Request aborted.' }); return; }
+ } else {
+ var eventData = { url: url, postData: params };
+ }
+ // call server to get data
+ var obj = this;
+ if (this.last.xhr_offset == 0) {
+ this.lock(this.msgRefresh, true);
+ } else {
+ var more = $('#grid_'+ this.name +'_rec_more');
+ if (this.autoLoad === true) {
+ more.show().find('td').html('<div><div style="width: 20px; height: 20px;" class="w2ui-spinner"></div></div>');
+ } else {
+ more.find('td').html('<div>'+ w2utils.lang('Load') + ' ' + obj.limit + ' ' + w2utils.lang('More') + '...</div>');
+ }
+ }
+ if (this.last.xhr) try { this.last.xhr.abort(); } catch (e) {};
+ // URL
+ var url = (typeof eventData.url != 'object' ? eventData.url : eventData.url.get);
+ if (params.cmd == 'save-records' && typeof eventData.url == 'object') url = eventData.url.save;
+ if (params.cmd == 'delete-records' && typeof eventData.url == 'object') url = eventData.url.remove;
+ // process url with routeData
+ if (!$.isEmptyObject(obj.routeData)) {
+ var info = w2utils.parseRoute(url);
+ if (info.keys.length > 0) {
+ for (var k = 0; k < info.keys.length; k++) {
+ if (obj.routeData[info.keys[k].name] == null) continue;
+ url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), obj.routeData[info.keys[k].name]);
+ }
+ }
+ }
+ // ajax ptions
+ var ajaxOptions = {
+ type : 'POST',
+ url : url,
+ data : eventData.postData,
+ dataType : 'text' // expected data type from server
+ };
+ if (w2utils.settings.dataType == 'HTTP') {
+ ajaxOptions.data = (typeof ajaxOptions.data == 'object' ? String($.param(ajaxOptions.data, false)).replace(/%5B/g, '[').replace(/%5D/g, ']') : ajaxOptions.data);
+ }
+ if (w2utils.settings.dataType == 'RESTFULL') {
+ ajaxOptions.type = 'GET';
+ if (params.cmd == 'save-records') ajaxOptions.type = 'PUT'; // so far it is always update
+ if (params.cmd == 'delete-records') ajaxOptions.type = 'DELETE';
+ ajaxOptions.data = (typeof ajaxOptions.data == 'object' ? String($.param(ajaxOptions.data, false)).replace(/%5B/g, '[').replace(/%5D/g, ']') : ajaxOptions.data);
+ }
+ if (w2utils.settings.dataType == 'JSON') {
+ ajaxOptions.type = 'POST';
+ ajaxOptions.data = JSON.stringify(ajaxOptions.data);
+ ajaxOptions.contentType = 'application/json';
+ }
+ if (this.method) ajaxOptions.type = this.method;
+
+ this.last.xhr_cmd = params.cmd;
+ this.last.xhr_start = (new Date()).getTime();
+ this.last.xhr = $.ajax(ajaxOptions)
+ .done(function (data, status, xhr) {
+ obj.requestComplete(status, cmd, callBack);
+ })
+ .fail(function (xhr, status, error) {
+ // trigger event
+ var errorObj = { status: status, error: error, rawResponseText: xhr.responseText };
+ var eventData2 = obj.trigger({ phase: 'before', type: 'error', error: errorObj, xhr: xhr });
+ if (eventData2.isCancelled === true) return;
+ // default behavior
+ if (status != 'abort') {
+ var data;
+ try { data = $.parseJSON(xhr.responseText) } catch (e) {}
+ console.log('ERROR: Server communication failed.',
+ '\n EXPECTED:', { status: 'success', total: 5, records: [{ recid: 1, field: 'value' }] },
+ '\n OR:', { status: 'error', message: 'error message' },
+ '\n RECEIVED:', typeof data == 'object' ? data : xhr.responseText);
+ }
+ obj.requestComplete('error', cmd, callBack);
+ // event after
+ obj.trigger($.extend(eventData2, { phase: 'after' }));
+ });
+ if (cmd == 'get-records') {
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ },
+
+ requestComplete: function(status, cmd, callBack) {
+ var obj = this;
+ this.unlock();
+ setTimeout(function () { obj.status(w2utils.lang('Server Response') + ' ' + ((new Date()).getTime() - obj.last.xhr_start)/1000 +' ' + w2utils.lang('sec')); }, 10);
+ this.last.pull_more = false;
+ this.last.pull_refresh = true;
+
+ // event before
+ var event_name = 'load';
+ if (this.last.xhr_cmd == 'save-records') event_name = 'save';
+ if (this.last.xhr_cmd == 'delete-records') event_name = 'deleted';
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: event_name, xhr: this.last.xhr, status: status });
+ if (eventData.isCancelled === true) {
+ if (typeof callBack == 'function') callBack({ status: 'error', message: 'Request aborted.' });
+ return;
+ }
+ // parse server response
+ var data;
+ var responseText = this.last.xhr.responseText;
+ if (status != 'error') {
+ // default action
+ if (typeof responseText != 'undefined' && responseText != '') {
+ // check if the onLoad handler has not already parsed the data
+ if (typeof responseText == "object") {
+ data = responseText;
+ } else {
+ if (typeof obj.parser == 'function') {
+ data = obj.parser(responseText);
+ if (typeof data != 'object') {
+ console.log('ERROR: Your parser did not return proper object');
+ }
+ } else {
+ // $.parseJSON or $.getJSON did not work because those expect perfect JSON data - where everything is in double quotes
+ //
+ // TODO: avoid (potentially malicious) code injection from the response.
+ try { eval('data = '+ responseText); } catch (e) { }
+ }
+ }
+ // convert recids
+ if (obj.recid) {
+ for (var r in data.records) {
+ data.records[r]['recid'] = data.records[r][obj.recid];
+ }
+ }
+ if (typeof data == 'undefined') {
+ data = {
+ status : 'error',
+ message : this.msgNotJSON,
+ responseText : responseText
+ };
+ }
+ if (data['status'] == 'error') {
+ obj.error(data['message']);
+ } else {
+ if (cmd == 'get-records') {
+ if (this.last.xhr_offset == 0) {
+ this.records = [];
+ this.summary = [];
+ //data.xhr_status=data.status;
+ delete data.status;
+ $.extend(true, this, data);
+ } else {
+ var records = data.records;
+ delete data.records;
+ //data.xhr_status=data.status;
+ delete data.status;
+ $.extend(true, this, data);
+ for (var r in records) {
+ this.records.push(records[r]);
+ }
+ }
+ }
+ if (cmd == 'delete-records') {
+ // reset() also triggers reload
+ this.reset(); // unselect old selections
+ return;
+ }
+ }
+ }
+ } else {
+ data = {
+ status : 'error',
+ message : this.msgAJAXerror,
+ responseText : responseText
+ };
+ obj.error(this.msgAJAXerror);
+ }
+ // event after
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (!url) {
+ this.localSort();
+ this.localSearch();
+ }
+ this.total = parseInt(this.total);
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ // do not refresh if loading on infinite scroll
+ if (this.last.xhr_offset == 0) this.refresh(); else this.scroll();
+ // call back
+ if (typeof callBack == 'function') callBack(data);
+ },
+
+ error: function (msg) {
+ var obj = this;
+ // let the management of the error outside of the grid
+ var eventData = this.trigger({ target: this.name, type: 'error', message: msg , xhr: this.last.xhr });
+ if (eventData.isCancelled === true) {
+ if (typeof callBack == 'function') callBack({ status: 'error', message: 'Request aborted.' });
+ return;
+ }
+ w2alert(msg, 'Error');
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ getChanges: function () {
+ var changes = [];
+ for (var r in this.records) {
+ var rec = this.records[r];
+ if (typeof rec['changes'] != 'undefined') {
+ changes.push($.extend(true, { recid: rec.recid }, rec.changes));
+ }
+ }
+ return changes;
+ },
+
+ mergeChanges: function () {
+ var changes = this.getChanges();
+ for (var c in changes) {
+ var record = this.get(changes[c].recid);
+ for (var s in changes[c]) {
+ if (s == 'recid') continue; // do not allow to change recid
+ try { eval('record.' + s + ' = changes[c][s]'); } catch (e) {}
+ delete record.changes;
+ }
+ }
+ this.refresh();
+ },
+
+ // ===================================================
+ // -- Action Handlers
+
+ save: function () {
+ var obj = this;
+ var changes = this.getChanges();
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'submit', changes: changes });
+ if (eventData.isCancelled === true) return;
+ var url = (typeof this.url != 'object' ? this.url : this.url.save);
+ if (url) {
+ this.request('save-records', { 'changes' : eventData.changes }, null,
+ function (data) {
+ if (data.status !== 'error') {
+ // only merge changes, if save was successful
+ obj.mergeChanges();
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ );
+ } else {
+ this.mergeChanges();
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ },
+
+ editField: function (recid, column, value, event) {
+ var obj = this;
+ var index = obj.get(recid, true);
+ var rec = obj.records[index];
+ var col = obj.columns[column];
+ var edit = col ? col.editable : null;
+ if (!rec || !col || !edit || rec.editable === false) return;
+ if (['enum', 'file'].indexOf(edit.type) != -1) {
+ console.log('ERROR: input types "enum" and "file" are not supported in inline editing.');
+ return;
+ }
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'editField', target: obj.name, recid: recid, column: column, value: value,
+ index: index, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ value = eventData.value;
+ // default behaviour
+ this.selectNone();
+ this.select({ recid: recid, column: column });
+ this.last.edit_col = column;
+ if (['checkbox', 'check'].indexOf(edit.type) != -1) return;
+ // create input element
+ var tr = $('#grid_'+ obj.name +'_rec_'+ w2utils.escapeId(recid));
+ var el = tr.find('[col='+ column +'] > div');
+ if (typeof edit.inTag == 'undefined') edit.inTag = '';
+ if (typeof edit.outTag == 'undefined') edit.outTag = '';
+ if (typeof edit.style == 'undefined') edit.style = '';
+ if (typeof edit.items == 'undefined') edit.items = [];
+ var val = (rec.changes && typeof rec.changes[col.field] != 'undefined' ? w2utils.stripTags(rec.changes[col.field]) : w2utils.stripTags(rec[col.field]));
+ if (val == null || typeof val == 'undefined') val = '';
+ if (typeof value != 'undefined' && value != null) val = value;
+ var addStyle = (typeof col.style != 'undefined' ? col.style + ';' : '');
+ if (typeof col.render == 'string' && ['number', 'int', 'float', 'money', 'percent'].indexOf(col.render.split(':')[0]) != -1) {
+ addStyle += 'text-align: right;';
+ }
+ if (edit.type == 'select') {
+ var html = '';
+ for (var i in edit.items) {
+ html += '<option value="'+ edit.items[i].id +'" '+ (edit.items[i].id == val ? 'selected' : '') +'>'+ edit.items[i].text +'</option>';
+ }
+ el.addClass('w2ui-editable')
+ .html('<select id="grid_'+ obj.name +'_edit_'+ recid +'_'+ column +'" column="'+ column +'" '+
+ ' style="width: 100%; '+ addStyle + edit.style +'" field="'+ col.field +'" recid="'+ recid +'" '+
+ ' '+ edit.inTag +
+ '>'+ html +'</select>' + edit.outTag);
+ el.find('select').focus()
+ .on('change', function (event) {
+ delete obj.last.move;
+ })
+ .on('blur', function (event) {
+ obj.editChange.call(obj, this, index, column, event);
+ });
+ } else {
+ el.addClass('w2ui-editable')
+ .html('<input id="grid_'+ obj.name +'_edit_'+ recid +'_'+ column +'" '+
+ ' type="text" style="outline: none; '+ addStyle + edit.style +'" field="'+ col.field +'" recid="'+ recid +'" '+
+ ' column="'+ column +'" '+ edit.inTag +
+ '>' + edit.outTag);
+ if (value == null) el.find('input').val(val != 'object' ? val : '');
+ // init w2field
+ var input = el.find('input').get(0);
+ $(input).w2field(edit.type, $.extend(edit, { selected: val }))
+ // add blur listener
+ setTimeout(function () {
+ var tmp = input;
+ if (edit.type == 'list') {
+ tmp = $($(input).data('w2field').helpers.focus).find('input');
+ if (val != 'object' && val != '') tmp.val(val).css({ opacity: 1 }).prev().css({ opacity: 1 });
+ }
+ $(tmp).on('blur', function (event) {
+ obj.editChange.call(obj, input, index, column, event);
+ });
+ }, 10);
+ if (value != null) $(input).val(val != 'object' ? val : '');
+ }
+ setTimeout(function () {
+ el.find('input, select')
+ .on('click', function (event) {
+ event.stopPropagation();
+ })
+ .on('keydown', function (event) {
+ var cancel = false;
+ switch (event.keyCode) {
+ case 9: // tab
+ cancel = true;
+ var next_rec = recid;
+ var next_col = event.shiftKey ? obj.prevCell(column, true) : obj.nextCell(column, true);
+ // next or prev row
+ if (next_col == null) {
+ var tmp = event.shiftKey ? obj.prevRow(index) : obj.nextRow(index);
+ if (tmp != null && tmp != index) {
+ next_rec = obj.records[tmp].recid;
+ // find first editable row
+ for (var c in obj.columns) {
+ var tmp = obj.columns[c].editable;
+ if (typeof tmp != 'undefined' && ['checkbox', 'check'].indexOf(tmp.type) == -1) {
+ next_col = parseInt(c);
+ if (!event.shiftKey) break;
+ }
+ }
+ }
+
+ }
+ if (next_rec === false) next_rec = recid;
+ if (next_col == null) next_col = column;
+ // init new or same record
+ this.blur();
+ setTimeout(function () {
+ if (obj.selectType != 'row') {
+ obj.selectNone();
+ obj.select({ recid: next_rec, column: next_col });
+ } else {
+ obj.editField(next_rec, next_col, null, event);
+ }
+ }, 1);
+ break;
+
+ case 13: // enter
+ this.blur();
+ var next = event.shiftKey ? obj.prevRow(index) : obj.nextRow(index);
+ if (next != null && next != index) {
+ setTimeout(function () {
+ if (obj.selectType != 'row') {
+ obj.selectNone();
+ obj.select({ recid: obj.records[next].recid, column: column });
+ } else {
+ obj.editField(obj.records[next].recid, column, null, event);
+ }
+ }, 100);
+ }
+ break;
+
+ case 38: // up arrow
+ if (!event.shiftKey) break;
+ cancel = true;
+ var next = obj.prevRow(index);
+ if (next != index) {
+ this.blur();
+ setTimeout(function () {
+ if (obj.selectType != 'row') {
+ obj.selectNone();
+ obj.select({ recid: obj.records[next].recid, column: column });
+ } else {
+ obj.editField(obj.records[next].recid, column, null, event);
+ }
+ }, 1);
+ }
+ break;
+
+ case 40: // down arrow
+ if (!event.shiftKey) break;
+ cancel = true;
+ var next = obj.nextRow(index);
+ if (next != null && next != index) {
+ this.blur();
+ setTimeout(function () {
+ if (obj.selectType != 'row') {
+ obj.selectNone();
+ obj.select({ recid: obj.records[next].recid, column: column });
+ } else {
+ obj.editField(obj.records[next].recid, column, null, event);
+ }
+ }, 1);
+ }
+ break;
+
+ case 27: // escape
+ var old = obj.parseField(rec, col.field);
+ if (rec.changes && typeof rec.changes[col.field] != 'undefined') old = rec.changes[col.field];
+ this.value = typeof old != 'undefined' ? old : '';
+ this.blur();
+ setTimeout(function () { obj.select({ recid: recid, column: column }) }, 1);
+ break;
+ }
+ if (cancel) if (event.preventDefault) event.preventDefault();
+ });
+ // focus and select
+ var tmp = el.find('input').focus();
+ if (value != null) {
+ // set cursor to the end
+ tmp[0].setSelectionRange(tmp.val().length, tmp.val().length);
+ } else {
+ tmp.select();
+ }
+
+ }, 1);
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ editChange: function (el, index, column, event) {
+ // all other fields
+ var summary = index < 0;
+ index = index < 0 ? -index - 1 : index;
+ var records = summary ? this.summary : this.records;
+ var rec = records[index];
+ var tr = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(rec.recid));
+ var col = this.columns[column];
+ var new_val = el.value;
+ var old_val = this.parseField(rec, col.field);
+ var tmp = $(el).data('w2field');
+ if (tmp) {
+ new_val = tmp.clean(new_val);
+ if (tmp.type == 'list' && new_val != '') new_val = $(el).data('selected');
+ }
+ if (el.type == 'checkbox') new_val = el.checked;
+ // change/restore event
+ var eventData = {
+ phase: 'before', type: 'change', target: this.name, input_id: el.id, recid: rec.recid, index: index, column: column,
+ value_new: new_val, value_previous: (rec.changes && rec.changes.hasOwnProperty(col.field) ? rec.changes[col.field]: old_val), value_original: old_val
+ };
+ while (true) {
+ new_val = eventData.value_new;
+ if (( typeof old_val == 'undefined' || old_val === null ? '' : String(old_val)) !== String(new_val)) {
+ // change event
+ eventData = this.trigger($.extend(eventData, { type: 'change', phase: 'before' }));
+ if (eventData.isCancelled !== true) {
+ if (new_val !== eventData.value_new) {
+ // re-evaluate the type of change to be made
+ continue;
+ }
+ // default action
+ rec.changes = rec.changes || {};
+ rec.changes[col.field] = eventData.value_new;
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ } else {
+ // restore event
+ eventData = this.trigger($.extend(eventData, { type: 'restore', phase: 'before' }));
+ if (eventData.isCancelled !== true) {
+ if (new_val !== eventData.value_new) {
+ // re-evaluate the type of change to be made
+ continue;
+ }
+ // default action
+ if (rec.changes) delete rec.changes[col.field];
+ if ($.isEmptyObject(rec.changes)) delete rec.changes;
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ }
+ break;
+ }
+ // refresh cell
+ var cell = this.getCellHTML(index, column, summary);
+ if (!summary) {
+ if (rec.changes && typeof rec.changes[col.field] != 'undefined') {
+ $(tr).find('[col='+ column +']').addClass('w2ui-changed').html(cell);
+ } else {
+ $(tr).find('[col='+ column +']').removeClass('w2ui-changed').html(cell);
+ }
+ }
+ },
+
+ "delete": function (force) {
+ var obj = this;
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'delete', force: force });
+ if (eventData.isCancelled === true) return;
+ force = eventData.force;
+ // default action
+ var recs = this.getSelection();
+ if (recs.length == 0) return;
+ if (this.msgDelete != '' && !force) {
+ w2confirm({
+ title : w2utils.lang('Delete Confirmation'),
+ msg : obj.msgDelete,
+ btn_yes : { "class": 'btn-red' },
+ callBack: function (result) {
+ if (result == 'Yes') w2ui[obj.name].delete(true);
+ }
+ });
+ return;
+ }
+ // call delete script
+ var url = (typeof this.url != 'object' ? this.url : this.url.remove);
+ if (url) {
+ this.request('delete-records');
+ } else {
+ this.selectNone();
+ if (typeof recs[0] != 'object') {
+ this.remove.apply(this, recs);
+ } else {
+ // clear cells
+ for (var r in recs) {
+ var fld = this.columns[recs[r].column].field;
+ var ind = this.get(recs[r].recid, true);
+ if (ind != null && fld != 'recid') {
+ this.records[ind][fld] = '';
+ if (this.records[ind].changes) delete this.records[ind].changes[fld];
+ }
+ }
+ this.refresh();
+ }
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ click: function (recid, event) {
+ var time = (new Date()).getTime();
+ var column = null;
+ if (this.last.cancelClick == true || (event && event.altKey)) return;
+ if (typeof recid == 'object') {
+ column = recid.column;
+ recid = recid.recid;
+ }
+ if (typeof event == 'undefined') event = {};
+ // check for double click
+ if (time - parseInt(this.last.click_time) < 350 && event.type == 'click') {
+ this.dblClick(recid, event);
+ return;
+ }
+ this.last.click_time = time;
+ // column user clicked on
+ if (column == null && event.target) {
+ var tmp = event.target;
+ if (tmp.tagName != 'TD') tmp = $(tmp).parents('td')[0];
+ if (typeof $(tmp).attr('col') != 'undefined') column = parseInt($(tmp).attr('col'));
+ }
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'click', recid: recid, column: column, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // if it is subgrid unselect top grid
+ var parent = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid)).parents('tr');
+ if (parent.length > 0 && String(parent.attr('id')).indexOf('expanded_row') != -1) {
+ var grid = parent.parents('.w2ui-grid').attr('name');
+ w2ui[grid].selectNone();
+ // all subgrids
+ parent.parents('.w2ui-grid').find('.w2ui-expanded-row .w2ui-grid').each(function (index, el) {
+ var grid = $(el).attr('name');
+ if (w2ui[grid]) w2ui[grid].selectNone();
+ });
+ }
+ // unselect all subgrids
+ $(this.box).find('.w2ui-expanded-row .w2ui-grid').each(function (index, el) {
+ var grid = $(el).attr('name');
+ if (w2ui[grid]) w2ui[grid].selectNone();
+ });
+ // default action
+ var obj = this;
+ var sel = this.getSelection();
+ $('#grid_'+ this.name +'_check_all').prop("checked", false);
+ var ind = this.get(recid, true);
+ var record = this.records[ind];
+ var selectColumns = [];
+ obj.last.sel_ind = ind;
+ obj.last.sel_col = column;
+ obj.last.sel_recid = recid;
+ obj.last.sel_type = 'click';
+ // multi select with shif key
+ if (event.shiftKey && sel.length > 0 && obj.multiSelect) {
+ if (sel[0].recid) {
+ var start = this.get(sel[0].recid, true);
+ var end = this.get(recid, true);
+ if (column > sel[0].column) {
+ var t1 = sel[0].column;
+ var t2 = column;
+ } else {
+ var t1 = column;
+ var t2 = sel[0].column;
+ }
+ for (var c = t1; c <= t2; c++) selectColumns.push(c);
+ } else {
+ var start = this.get(sel[0], true);
+ var end = this.get(recid, true);
+ }
+ var sel_add = []
+ if (start > end) { var tmp = start; start = end; end = tmp; }
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ for (var i = start; i <= end; i++) {
+ if (this.searchData.length > 0 && !url && $.inArray(i, this.last.searchIds) == -1) continue;
+ if (this.selectType == 'row') {
+ sel_add.push(this.records[i].recid);
+ } else {
+ for (var sc in selectColumns) sel_add.push({ recid: this.records[i].recid, column: selectColumns[sc] });
+ }
+ //sel.push(this.records[i].recid);
+ }
+ this.select.apply(this, sel_add);
+ } else {
+ var last = this.last.selection;
+ var flag = (last.indexes.indexOf(ind) != -1 ? true : false);
+ // clear other if necessary
+ if (((!event.ctrlKey && !event.shiftKey && !event.metaKey) || !this.multiSelect) && !this.showSelectColumn) {
+ if (this.selectType != 'row' && $.inArray(column, last.columns[ind]) == -1) flag = false;
+ if (sel.length > 300) this.selectNone(); else this.unselect.apply(this, sel);
+ if (flag === true) {
+ this.unselect({ recid: recid, column: column });
+ } else {
+ this.select({ recid: recid, column: column });
+ }
+ } else {
+ if (this.selectType != 'row' && $.inArray(column, last.columns[ind]) == -1) flag = false;
+ if (flag === true) {
+ this.unselect({ recid: recid, column: column });
+ } else {
+ this.select({ recid: recid, column: column });
+ }
+ }
+ }
+ this.status();
+ obj.initResize();
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ columnClick: function (field, event) {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'columnClick', target: this.name, field: field, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default behaviour
+ var column = this.getColumn(field);
+ if (column.sortable) this.sort(field, null, (event && (event.ctrlKey || event.metaKey) ? true : false) );
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ keydown: function (event) {
+ // this method is called from w2utils
+ var obj = this;
+ if (obj.keyboard !== true) return;
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'keydown', target: obj.name, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ var empty = false;
+ var records = $('#grid_'+ obj.name +'_records');
+ var sel = obj.getSelection();
+ if (sel.length == 0) empty = true;
+ var recid = sel[0] || null;
+ var columns = [];
+ var recid2 = sel[sel.length-1];
+ if (typeof recid == 'object' && recid != null) {
+ recid = sel[0].recid;
+ columns = [];
+ var ii = 0;
+ while (true) {
+ if (!sel[ii] || sel[ii].recid != recid) break;
+ columns.push(sel[ii].column);
+ ii++;
+ }
+ recid2 = sel[sel.length-1].recid;
+ }
+ var ind = obj.get(recid, true);
+ var ind2 = obj.get(recid2, true);
+ var rec = obj.get(recid);
+ var recEL = $('#grid_'+ obj.name +'_rec_'+ (ind !== null ? w2utils.escapeId(obj.records[ind].recid) : 'none'));
+ var cancel = false;
+ var key = event.keyCode;
+ var shiftKey= event.shiftKey;
+ if (key == 9) { // tab key
+ if (event.shiftKey) key = 37; else key = 39; // replace with arrows
+ shiftKey = false;
+ cancel = true;
+ }
+ switch (key) {
+ case 8: // backspace
+ case 46: // delete
+ if (this.show.toolbarDelete) obj["delete"]();
+ cancel = true;
+ event.stopPropagation();
+ break;
+
+ case 27: // escape
+ obj.selectNone();
+ if (sel.length > 0 && typeof sel[0] == 'object') {
+ obj.select({ recid: sel[0].recid, column: sel[0].column });
+ }
+ cancel = true;
+ break;
+
+ case 65: // cmd + A
+ if (!event.metaKey && !event.ctrlKey) break;
+ obj.selectAll();
+ cancel = true;
+ break;
+
+ case 70: // cmd + F
+ if (!event.metaKey && !event.ctrlKey) break;
+ $('#grid_'+ obj.name + '_search_all').focus();
+ cancel = true;
+ break;
+
+ case 13: // enter
+ // if expandable columns - expand it
+ if (this.selectType == 'row' && obj.show.expandColumn === true) {
+ if (recEL.length <= 0) break;
+ obj.toggle(recid, event);
+ cancel = true;
+ } else { // or enter edit
+ for (var c in this.columns) {
+ if (this.columns[c].editable) {
+ columns.push(parseInt(c));
+ break;
+ }
+ }
+ // edit last column that was edited
+ if (this.selectType == 'row' && this.last.edit_col) columns = [this.last.edit_col];
+ if (columns.length > 0) {
+ obj.editField(recid, columns[0], null, event);
+ cancel = true;
+ }
+ }
+ break;
+
+ case 37: // left
+ if (empty) break;
+ // check if this is subgrid
+ var parent = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(obj.records[ind].recid)).parents('tr');
+ if (parent.length > 0 && String(parent.attr('id')).indexOf('expanded_row') != -1) {
+ var recid = parent.prev().attr('recid');
+ var grid = parent.parents('.w2ui-grid').attr('name');
+ obj.selectNone();
+ w2utils.keyboard.active(grid);
+ w2ui[grid].set(recid, { expanded: false });
+ w2ui[grid].collapse(recid);
+ w2ui[grid].click(recid);
+ cancel = true;
+ break;
+ }
+ if (this.selectType == 'row') {
+ if (recEL.length <= 0 || rec.expanded !== true ) break;
+ obj.set(recid, { expanded: false }, true);
+ obj.collapse(recid, event);
+ } else {
+ var prev = obj.prevCell(columns[0]);
+ if (prev != null) {
+ if (shiftKey && obj.multiSelect) {
+ if (tmpUnselect()) return;
+ var tmp = [];
+ var newSel = [];
+ var unSel = [];
+ if (columns.indexOf(this.last.sel_col) == 0 && columns.length > 1) {
+ for (var i in sel) {
+ if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid);
+ unSel.push({ recid: sel[i].recid, column: columns[columns.length-1] });
+ }
+ } else {
+ for (var i in sel) {
+ if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid);
+ newSel.push({ recid: sel[i].recid, column: prev });
+ }
+ }
+ obj.unselect.apply(obj, unSel);
+ obj.select.apply(obj, newSel);
+ } else {
+ event.shiftKey = false;
+ obj.click({ recid: recid, column: prev }, event);
+ }
+ } else {
+ // if selected more then one, then select first
+ if (!shiftKey) {
+ for (var s=1; s<sel.length; s++) obj.unselect(sel[s]);
+ }
+ }
+ }
+ cancel = true;
+ break;
+
+ case 39: // right
+ if (empty) break;
+ if (this.selectType == 'row') {
+ if (recEL.length <= 0 || rec.expanded === true || obj.show.expandColumn !== true) break;
+ obj.expand(recid, event);
+ } else {
+ var next = obj.nextCell(columns[columns.length-1]);
+ if (next !== null) {
+ if (shiftKey && key == 39 && obj.multiSelect) {
+ if (tmpUnselect()) return;
+ var tmp = [];
+ var newSel = [];
+ var unSel = [];
+ if (columns.indexOf(this.last.sel_col) == columns.length-1 && columns.length > 1) {
+ for (var i in sel) {
+ if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid);
+ unSel.push({ recid: sel[i].recid, column: columns[0] });
+ }
+ } else {
+ for (var i in sel) {
+ if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid);
+ newSel.push({ recid: sel[i].recid, column: next });
+ }
+ }
+ obj.unselect.apply(obj, unSel);
+ obj.select.apply(obj, newSel);
+ } else {
+ obj.click({ recid: recid, column: next }, event);
+ }
+ } else {
+ // if selected more then one, then select first
+ if (!shiftKey) {
+ for (var s=0; s<sel.length-1; s++) obj.unselect(sel[s]);
+ }
+ }
+ }
+ cancel = true;
+ break;
+
+ case 38: // up
+ if (empty) selectTopRecord();
+ if (recEL.length <= 0) break;
+ // move to the previous record
+ var prev = obj.prevRow(ind);
+ if (prev != null) {
+ // jump into subgrid
+ if (obj.records[prev].expanded) {
+ var subgrid = $('#grid_'+ obj.name +'_rec_'+ w2utils.escapeId(obj.records[prev].recid) +'_expanded_row').find('.w2ui-grid');
+ if (subgrid.length > 0 && w2ui[subgrid.attr('name')]) {
+ obj.selectNone();
+ var grid = subgrid.attr('name');
+ var recs = w2ui[grid].records;
+ w2utils.keyboard.active(grid);
+ w2ui[grid].click(recs[recs.length-1].recid);
+ cancel = true;
+ break;
+ }
+ }
+ if (shiftKey && obj.multiSelect) { // expand selection
+ if (tmpUnselect()) return;
+ if (obj.selectType == 'row') {
+ if (obj.last.sel_ind > prev && obj.last.sel_ind != ind2) {
+ obj.unselect(obj.records[ind2].recid);
+ } else {
+ obj.select(obj.records[prev].recid);
+ }
+ } else {
+ if (obj.last.sel_ind > prev && obj.last.sel_ind != ind2) {
+ prev = ind2;
+ var tmp = [];
+ for (var c in columns) tmp.push({ recid: obj.records[prev].recid, column: columns[c] });
+ obj.unselect.apply(obj, tmp);
+ } else {
+ var tmp = [];
+ for (var c in columns) tmp.push({ recid: obj.records[prev].recid, column: columns[c] });
+ obj.select.apply(obj, tmp);
+ }
+ }
+ } else { // move selected record
+ obj.selectNone();
+ obj.click({ recid: obj.records[prev].recid, column: columns[0] }, event);
+ }
+ obj.scrollIntoView(prev);
+ if (event.preventDefault) event.preventDefault();
+ } else {
+ // if selected more then one, then select first
+ if (!shiftKey) {
+ for (var s=1; s<sel.length; s++) obj.unselect(sel[s]);
+ }
+ // jump out of subgird (if first record)
+ var parent = $('#grid_'+ obj.name +'_rec_'+ w2utils.escapeId(obj.records[ind].recid)).parents('tr');
+ if (parent.length > 0 && String(parent.attr('id')).indexOf('expanded_row') != -1) {
+ var recid = parent.prev().attr('recid');
+ var grid = parent.parents('.w2ui-grid').attr('name');
+ obj.selectNone();
+ w2utils.keyboard.active(grid);
+ w2ui[grid].click(recid);
+ cancel = true;
+ break;
+ }
+ }
+ break;
+
+ case 40: // down
+ if (empty) selectTopRecord();
+ if (recEL.length <= 0) break;
+ // jump into subgrid
+ if (obj.records[ind2].expanded) {
+ var subgrid = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(obj.records[ind2].recid) +'_expanded_row').find('.w2ui-grid');
+ if (subgrid.length > 0 && w2ui[subgrid.attr('name')]) {
+ obj.selectNone();
+ var grid = subgrid.attr('name');
+ var recs = w2ui[grid].records;
+ w2utils.keyboard.active(grid);
+ w2ui[grid].click(recs[0].recid);
+ cancel = true;
+ break;
+ }
+ }
+ // move to the next record
+ var next = obj.nextRow(ind2);
+ if (next != null) {
+ if (shiftKey && obj.multiSelect) { // expand selection
+ if (tmpUnselect()) return;
+ if (obj.selectType == 'row') {
+ if (this.last.sel_ind < next && this.last.sel_ind != ind) {
+ obj.unselect(obj.records[ind].recid);
+ } else {
+ obj.select(obj.records[next].recid);
+ }
+ } else {
+ if (this.last.sel_ind < next && this.last.sel_ind != ind) {
+ next = ind;
+ var tmp = [];
+ for (var c in columns) tmp.push({ recid: obj.records[next].recid, column: columns[c] });
+ obj.unselect.apply(obj, tmp);
+ } else {
+ var tmp = [];
+ for (var c in columns) tmp.push({ recid: obj.records[next].recid, column: columns[c] });
+ obj.select.apply(obj, tmp);
+ }
+ }
+ } else { // move selected record
+ obj.selectNone();
+ obj.click({ recid: obj.records[next].recid, column: columns[0] }, event);
+ }
+ obj.scrollIntoView(next);
+ cancel = true;
+ } else {
+ // if selected more then one, then select first
+ if (!shiftKey) {
+ for (var s=0; s<sel.length-1; s++) obj.unselect(sel[s]);
+ }
+ // jump out of subgrid (if last record in subgrid)
+ var parent = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(obj.records[ind2].recid)).parents('tr');
+ if (parent.length > 0 && String(parent.attr('id')).indexOf('expanded_row') != -1) {
+ var recid = parent.next().attr('recid');
+ var grid = parent.parents('.w2ui-grid').attr('name');
+ obj.selectNone();
+ w2utils.keyboard.active(grid);
+ w2ui[grid].click(recid);
+ cancel = true;
+ break;
+ }
+ }
+ break;
+
+ // copy & paste
+
+ case 17: // ctrl key
+ case 91: // cmd key
+ if (empty) break;
+ var text = obj.copy();
+ $('body').append('<textarea id="_tmp_copy_data" '+
+ ' onpaste="var obj = this; setTimeout(function () { w2ui[\''+ obj.name + '\'].paste(obj.value); }, 1);" '+
+ ' onkeydown="w2ui[\''+ obj.name +'\'].keydown(event)"'+
+ ' style="position: absolute; top: -100px; height: 1px; width: 1px">'+ text +'</textarea>');
+ $('#_tmp_copy_data').focus().select();
+ // remove _tmp_copy_data textarea
+ $(document).on('keyup', tmp_key_down);
+ function tmp_key_down() {
+ $('#_tmp_copy_data').remove();
+ $(document).off('keyup', tmp_key_down);
+ }
+ break;
+
+ case 88: // x - cut
+ if (empty) break;
+ if (event.ctrlKey || event.metaKey) {
+ setTimeout(function () { obj["delete"](true); }, 100);
+ }
+ break;
+ }
+ var tmp = [187, 189, 32]; // =-spacebar
+ for (var i=48; i<=90; i++) tmp.push(i); // 0-9,a-z,A-Z
+ if (tmp.indexOf(key) != -1 && !event.ctrlKey && !event.metaKey && !cancel) {
+ if (columns.length == 0) columns.push(0);
+ var tmp = String.fromCharCode(key);
+ if (key == 187) tmp = '=';
+ if (key == 189) tmp = '-';
+ if (!shiftKey) tmp = tmp.toLowerCase();
+ obj.editField(recid, columns[0], tmp, event);
+ cancel = true;
+ }
+ if (cancel) { // cancel default behaviour
+ if (event.preventDefault) event.preventDefault();
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+
+ function selectTopRecord() {
+ var ind = Math.floor((records[0].scrollTop + (records.height() / 2.1)) / obj.recordHeight);
+ if (!obj.records[ind]) ind = 0;
+ obj.select({ recid: obj.records[ind].recid, column: 0});
+ }
+
+ function tmpUnselect () {
+ if (obj.last.sel_type != 'click') return false;
+ if (obj.selectType != 'row') {
+ obj.last.sel_type = 'key';
+ if (sel.length > 1) {
+ for (var s in sel) {
+ if (sel[s].recid == obj.last.sel_recid && sel[s].column == obj.last.sel_col) {
+ sel.splice(s, 1);
+ break;
+ }
+ }
+ obj.unselect.apply(obj, sel);
+ return true;
+ }
+ return false;
+ } else {
+ obj.last.sel_type = 'key';
+ if (sel.length > 1) {
+ sel.splice(sel.indexOf(obj.records[obj.last.sel_ind].recid), 1);
+ obj.unselect.apply(obj, sel);
+ return true;
+ }
+ return false;
+ }
+ }
+ },
+
+ scrollIntoView: function (ind) {
+ var buffered = this.records.length;
+ if (this.searchData.length != 0 && !this.url) buffered = this.last.searchIds.length;
+ if (typeof ind == 'undefined') {
+ var sel = this.getSelection();
+ if (sel.length == 0) return;
+ ind = this.get(sel[0], true);
+ }
+ var records = $('#grid_'+ this.name +'_records');
+ if (buffered == 0) return;
+ // if all records in view
+ var len = this.last.searchIds.length;
+ if (records.height() > this.recordHeight * (len > 0 ? len : buffered)) return;
+ if (len > 0) ind = this.last.searchIds.indexOf(ind); // if seach is applied
+ // scroll to correct one
+ var t1 = Math.floor(records[0].scrollTop / this.recordHeight);
+ var t2 = t1 + Math.floor(records.height() / this.recordHeight);
+ if (ind == t1) records.animate({ 'scrollTop': records.scrollTop() - records.height() / 1.3 }, 250, 'linear');
+ if (ind == t2) records.animate({ 'scrollTop': records.scrollTop() + records.height() / 1.3 }, 250, 'linear');
+ if (ind < t1 || ind > t2) records.animate({ 'scrollTop': (ind - 1) * this.recordHeight });
+ },
+
+ dblClick: function (recid, event) {
+ //if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ // find columns
+ var column = null;
+ if (typeof recid == 'object') {
+ column = recid.column;
+ recid = recid.recid;
+ }
+ if (typeof event == 'undefined') event = {};
+ // column user clicked on
+ if (column == null && event.target) {
+ var tmp = event.target;
+ if (tmp.tagName != 'TD') tmp = $(tmp).parents('td')[0];
+ column = parseInt($(tmp).attr('col'));
+ }
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'dblClick', recid: recid, column: column, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ this.selectNone();
+ var col = this.columns[column];
+ if (col && $.isPlainObject(col.editable)) {
+ this.editField(recid, column, null, event);
+ } else {
+ this.select({ recid: recid, column: column });
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ contextMenu: function (recid, event) {
+ var obj = this;
+ if (obj.last.userSelect == 'text') return;
+ if (typeof event == 'undefined') event = { offsetX: 0, offsetY: 0, target: $('#grid_'+ obj.name +'_rec_'+ recid)[0] };
+ if (typeof event.offsetX === 'undefined') {
+ event.offsetX = event.layerX - event.target.offsetLeft;
+ event.offsetY = event.layerY - event.target.offsetTop;
+ }
+ if (w2utils.isFloat(recid)) recid = parseFloat(recid);
+ if (this.getSelection().indexOf(recid) == -1) obj.click(recid);
+ // need timeout to allow click to finish first
+ setTimeout(function () {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'contextMenu', target: obj.name, originalEvent: event, recid: recid });
+ if (eventData.isCancelled === true) return;
+ // default action
+ if (obj.menu.length > 0) {
+ $(obj.box).find(event.target)
+ .w2menu(obj.menu, {
+ left : event.offsetX,
+ onSelect: function (event) {
+ obj.menuClick(recid, parseInt(event.index), event.originalEvent);
+ }
+ }
+ );
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, 150); // need timer 150 for FF
+ // cancel event
+ if (event.preventDefault) event.preventDefault();
+ },
+
+ menuClick: function (recid, index, event) {
+ var obj = this;
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'menuClick', target: obj.name, originalEvent: event,
+ recid: recid, menuIndex: index, menuItem: obj.menu[index] });
+ if (eventData.isCancelled === true) return;
+ // default action
+ // -- empty
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ toggle: function (recid) {
+ var rec = this.get(recid);
+ if (rec.expanded === true) return this.collapse(recid); else return this.expand(recid);
+ },
+
+ expand: function (recid) {
+ var rec = this.get(recid);
+ var obj = this;
+ var id = w2utils.escapeId(recid);
+ if ($('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').length > 0) return false;
+ if (rec.expanded == 'none') return false;
+ // insert expand row
+ var tmp = 1 + (this.show.selectColumn ? 1 : 0);
+ var addClass = ''; // ($('#grid_'+this.name +'_rec_'+ w2utils.escapeId(recid)).hasClass('w2ui-odd') ? 'w2ui-odd' : 'w2ui-even');
+ $('#grid_'+ this.name +'_rec_'+ id).after(
+ '<tr id="grid_'+ this.name +'_rec_'+ id +'_expanded_row" class="w2ui-expanded-row '+ addClass +'">'+
+ (this.show.lineNumbers ? '<td class="w2ui-col-number"></td>' : '') +
+ ' <td class="w2ui-grid-data w2ui-expanded1" colspan="'+ tmp +'"><div style="display: none"></div></td>'+
+ ' <td colspan="100" class="w2ui-expanded2">'+
+ ' <div id="grid_'+ this.name +'_rec_'+ id +'_expanded" style="opacity: 0"></div>'+
+ ' </td>'+
+ '</tr>');
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'expand', target: this.name, recid: recid,
+ box_id: 'grid_'+ this.name +'_rec_'+ id +'_expanded', ready: ready });
+ if (eventData.isCancelled === true) {
+ $('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').remove();
+ return;
+ }
+ // default action
+ $('#grid_'+ this.name +'_rec_'+ id).attr('expanded', 'yes').addClass('w2ui-expanded');
+ $('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').show();
+ $('#grid_'+ this.name +'_cell_'+ this.get(recid, true) +'_expand div').html('<div class="w2ui-spinner" style="width: 16px; height: 16px; margin: -2px 2px;"></div>');
+ rec.expanded = true;
+ // check if height of expanded row > 5 then remove spinner
+ setTimeout(ready, 300);
+ function ready() {
+ var div1 = $('#grid_'+ obj.name +'_rec_'+ id +'_expanded');
+ var div2 = $('#grid_'+ obj.name +'_rec_'+ id +'_expanded_row .w2ui-expanded1 > div');
+ if (div1.height() < 5) return;
+ div1.css('opacity', 1);
+ div2.show().css('opacity', 1);
+ $('#grid_'+ obj.name +'_cell_'+ obj.get(recid, true) +'_expand div').html('-');
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ this.resizeRecords();
+ return true;
+ },
+
+ collapse: function (recid) {
+ var rec = this.get(recid);
+ var obj = this;
+ var id = w2utils.escapeId(recid);
+ if ($('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').length == 0) return false;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'collapse', target: this.name, recid: recid,
+ box_id: 'grid_'+ this.name +'_rec_'+ id +'_expanded' });
+ if (eventData.isCancelled === true) return;
+ // default action
+ $('#grid_'+ this.name +'_rec_'+ id).removeAttr('expanded').removeClass('w2ui-expanded');
+ $('#grid_'+ this.name +'_rec_'+ id +'_expanded').css('opacity', 0);
+ $('#grid_'+ this.name +'_cell_'+ this.get(recid, true) +'_expand div').html('+');
+ setTimeout(function () {
+ $('#grid_'+ obj.name +'_rec_'+ id +'_expanded').height('0px');
+ setTimeout(function () {
+ $('#grid_'+ obj.name +'_rec_'+ id +'_expanded_row').remove();
+ delete rec.expanded;
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ obj.resizeRecords();
+ }, 300);
+ }, 200);
+ return true;
+ },
+
+ sort: function (field, direction, multiField) { // if no params - clears sort
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'sort', target: this.name, field: field, direction: direction, multiField: multiField });
+ if (eventData.isCancelled === true) return;
+ // check if needed to quit
+ if (typeof field != 'undefined') {
+ // default action
+ var sortIndex = this.sortData.length;
+ for (var s in this.sortData) {
+ if (this.sortData[s].field == field) { sortIndex = s; break; }
+ }
+ if (typeof direction == 'undefined' || direction == null) {
+ if (typeof this.sortData[sortIndex] == 'undefined') {
+ direction = 'asc';
+ } else {
+ switch (String(this.sortData[sortIndex].direction)) {
+ case 'asc' : direction = 'desc'; break;
+ case 'desc' : direction = 'asc'; break;
+ default : direction = 'asc'; break;
+ }
+ }
+ }
+ if (this.multiSort === false) { this.sortData = []; sortIndex = 0; }
+ if (multiField != true) { this.sortData = []; sortIndex = 0; }
+ // set new sort
+ if (typeof this.sortData[sortIndex] == 'undefined') this.sortData[sortIndex] = {};
+ this.sortData[sortIndex].field = field;
+ this.sortData[sortIndex].direction = direction;
+ } else {
+ this.sortData = [];
+ }
+ this.selectNone();
+ // if local
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (!url) {
+ this.localSort();
+ if (this.searchData.length > 0) this.localSearch(true);
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ this.refresh();
+ } else {
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ this.last.xhr_offset = 0;
+ this.reload();
+ }
+ },
+
+ copy: function () {
+ var sel = this.getSelection();
+ if (sel.length == 0) return '';
+ var text = '';
+ if (typeof sel[0] == 'object') { // cell copy
+ // find min/max column
+ var minCol = sel[0].column;
+ var maxCol = sel[0].column;
+ var recs = [];
+ for (var s in sel) {
+ if (sel[s].column < minCol) minCol = sel[s].column;
+ if (sel[s].column > maxCol) maxCol = sel[s].column;
+ if (recs.indexOf(sel[s].index) == -1) recs.push(sel[s].index);
+ }
+ recs.sort();
+ for (var r in recs) {
+ var ind = recs[r];
+ for (var c = minCol; c <= maxCol; c++) {
+ var col = this.columns[c];
+ if (col.hidden === true) continue;
+ text += w2utils.stripTags(this.getCellHTML(ind, c)) + '\t';
+ }
+ text = text.substr(0, text.length-1); // remove last \t
+ text += '\n';
+ }
+ } else { // row copy
+ // copy headers
+ for (var c in this.columns) {
+ var col = this.columns[c];
+ if (col.hidden === true) continue;
+ text += '"' + w2utils.stripTags(col.caption ? col.caption : col.field) + '"\t';
+ }
+ text = text.substr(0, text.length-1); // remove last \t
+ text += '\n';
+ // copy selected text
+ for (var s in sel) {
+ var ind = this.get(sel[s], true);
+ for (var c in this.columns) {
+ var col = this.columns[c];
+ if (col.hidden === true) continue;
+ text += '"' + w2utils.stripTags(this.getCellHTML(ind, c)) + '"\t';
+ }
+ text = text.substr(0, text.length-1); // remove last \t
+ text += '\n';
+ }
+ }
+ text = text.substr(0, text.length - 1);
+ // before event
+ var eventData = this.trigger({ phase: 'before', type: 'copy', target: this.name, text: text });
+ if (eventData.isCancelled === true) return '';
+ text = eventData.text;
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return text;
+ },
+
+ paste: function (text) {
+ var sel = this.getSelection();
+ var ind = this.get(sel[0].recid, true);
+ var col = sel[0].column;
+ // before event
+ var eventData = this.trigger({ phase: 'before', type: 'paste', target: this.name, text: text, index: ind, column: col });
+ if (eventData.isCancelled === true) return;
+ text = eventData.text;
+ // default action
+ if (this.selectType == 'row' || sel.length == 0) {
+ console.log('ERROR: You can paste only if grid.selectType = \'cell\' and when at least one cell selected.');
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return;
+ }
+ var newSel = [];
+ var text = text.split('\n');
+ for (var t in text) {
+ var tmp = text[t].split('\t');
+ var cnt = 0;
+ var rec = this.records[ind];
+ var cols = [];
+ for (var dt in tmp) {
+ if (!this.columns[col + cnt]) continue;
+ var field = this.columns[col + cnt].field;
+ rec.changes = rec.changes || {};
+ rec.changes[field] = tmp[dt];
+ cols.push(col + cnt);
+ cnt++;
+ }
+ for (var c in cols) newSel.push({ recid: rec.recid, column: cols[c] });
+ ind++;
+ }
+ this.selectNone();
+ this.select.apply(this, newSel);
+ this.refresh();
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ // ==================================================
+ // --- Common functions
+
+ resize: function () {
+ var obj = this;
+ var time = (new Date()).getTime();
+ //if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ // make sure the box is right
+ if (!this.box || $(this.box).attr('name') != this.name) return;
+ // determine new width and height
+ $(this.box).find('> div')
+ .css('width', $(this.box).width())
+ .css('height', $(this.box).height());
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'resize', target: this.name });
+ if (eventData.isCancelled === true) return;
+ // resize
+ obj.resizeBoxes();
+ obj.resizeRecords();
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ refreshCell: function (recid, field) {
+ var index = this.get(recid, true);
+ var col_ind = this.getColumn(field, true);
+ var rec = this.records[index];
+ var col = this.columns[col_ind];
+ var cell = $('#grid_'+ this.name + '_rec_'+ recid +' [col='+ col_ind +']');
+ // set cell html and changed flag
+ cell.html(this.getCellHTML(index, col_ind));
+ if (rec.changes && typeof rec.changes[col.field] != 'undefined') {
+ cell.addClass('w2ui-changed');
+ } else {
+ cell.removeClass('w2ui-changed');
+ }
+ },
+
+ refreshRow: function (recid) {
+ var tr = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid));
+ if (tr.length != 0) {
+ var ind = this.get(recid, true);
+ var line = tr.attr('line');
+ // if it is searched, find index in search array
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (this.searchData.length > 0 && !url) for (var s in this.last.searchIds) if (this.last.searchIds[s] == ind) ind = s;
+ $(tr).replaceWith(this.getRecordHTML(ind, line));
+ }
+
+ },
+
+ refresh: function () {
+ var obj = this;
+ var time = (new Date()).getTime();
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (this.total <= 0 && !url && this.searchData.length == 0) {
+ this.total = this.records.length;
+ }
+ //if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ this.toolbar.disable('w2ui-edit', 'w2ui-delete');
+ if (!this.box) return;
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'refresh' });
+ if (eventData.isCancelled === true) return;
+ // -- header
+ if (this.show.header) {
+ $('#grid_'+ this.name +'_header').html(this.header +'&nbsp;').show();
+ } else {
+ $('#grid_'+ this.name +'_header').hide();
+ }
+ // -- toolbar
+ if (this.show.toolbar) {
+ // if select-collumn is checked - no toolbar refresh
+ if (this.toolbar && this.toolbar.get('w2ui-column-on-off') && this.toolbar.get('w2ui-column-on-off').checked) {
+ // no action
+ } else {
+ $('#grid_'+ this.name +'_toolbar').show();
+ // refresh toolbar all but search field
+ if (typeof this.toolbar == 'object') {
+ var tmp = this.toolbar.items;
+ for (var t in tmp) {
+ if (tmp[t].id == 'w2ui-search' || tmp[t].type == 'break') continue;
+ this.toolbar.refresh(tmp[t].id);
+ }
+ }
+ }
+ } else {
+ $('#grid_'+ this.name +'_toolbar').hide();
+ }
+ // -- make sure search is closed
+ this.searchClose();
+ // search placeholder
+ var el = $('#grid_'+ obj.name +'_search_all');
+ if (!this.multiSearch && this.last.field == 'all' && this.searches.length > 0) {
+ this.last.field = this.searches[0].field;
+ this.last.caption = this.searches[0].caption;
+ }
+ for (var s in this.searches) {
+ if (this.searches[s].field == this.last.field) this.last.caption = this.searches[s].caption;
+ }
+ if (this.last.multi) {
+ el.attr('placeholder', '[' + w2utils.lang('Multiple Fields') + ']');
+ } else {
+ el.attr('placeholder', this.last.caption);
+ }
+ if (el.val() != this.last.search) {
+ var val = this.last.search;
+ var tmp = el.data('w2field');
+ if (tmp) val = tmp.format(val);
+ el.val(val);
+ }
+
+ // -- separate summary
+ var tmp = this.find({ summary: true }, true);
+ if (tmp.length > 0) {
+ for (var t in tmp) this.summary.push(this.records[tmp[t]]);
+ for (var t=tmp.length-1; t>=0; t--) this.records.splice(tmp[t], 1);
+ this.total = this.total - tmp.length;
+ }
+
+ // -- body
+ var bodyHTML = '';
+ bodyHTML += '<div id="grid_'+ this.name +'_records" class="w2ui-grid-records"'+
+ ' onscroll="var obj = w2ui[\''+ this.name + '\']; '+
+ ' obj.last.scrollTop = this.scrollTop; '+
+ ' obj.last.scrollLeft = this.scrollLeft; '+
+ ' $(\'#grid_'+ this.name +'_columns\')[0].scrollLeft = this.scrollLeft;'+
+ ' $(\'#grid_'+ this.name +'_summary\')[0].scrollLeft = this.scrollLeft;'+
+ ' obj.scroll(event);">'+
+ this.getRecordsHTML() +
+ '</div>'+
+ '<div id="grid_'+ this.name +'_columns" class="w2ui-grid-columns">'+
+ ' <table>'+ this.getColumnsHTML() +'</table>'+
+ '</div>'; // Columns need to be after to be able to overlap
+ $('#grid_'+ this.name +'_body').html(bodyHTML);
+ // show summary records
+ if (this.summary.length > 0) {
+ $('#grid_'+ this.name +'_summary').html(this.getSummaryHTML()).show();
+ } else {
+ $('#grid_'+ this.name +'_summary').hide();
+ }
+ // -- footer
+ if (this.show.footer) {
+ $('#grid_'+ this.name +'_footer').html(this.getFooterHTML()).show();
+ } else {
+ $('#grid_'+ this.name +'_footer').hide();
+ }
+ // show/hide clear search link
+ if (this.searchData.length > 0) {
+ $('#grid_'+ this.name +'_searchClear').show();
+ } else {
+ $('#grid_'+ this.name +'_searchClear').hide();
+ }
+ // all selected?
+ var sel = this.last.selection;
+ if (sel.indexes.length == this.records.length || (this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)) {
+ $('#grid_'+ this.name +'_check_all').prop('checked', true);
+ } else {
+ $('#grid_'+ this.name +'_check_all').prop('checked', false);
+ }
+ // show number of selected
+ this.status();
+ // collapse all records
+ var rows = obj.find({ expanded: true }, true);
+ for (var r in rows) obj.records[rows[r]].expanded = false;
+ // mark selection
+ setTimeout(function () {
+ var str = $.trim($('#grid_'+ obj.name +'_search_all').val());
+ if (str != '') $(obj.box).find('.w2ui-grid-data > div').w2marker(str);
+ }, 50);
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ obj.resize();
+ obj.addRange('selection');
+ setTimeout(function () { obj.resize(); obj.scroll(); }, 1); // allow to render first
+
+ if ( obj.reorderColumns && !obj.last.columnDrag ) {
+ obj.last.columnDrag = obj.initColumnDrag();
+ } else if ( !obj.reorderColumns && obj.last.columnDrag ) {
+ obj.last.columnDrag.remove();
+ }
+
+ return (new Date()).getTime() - time;
+ },
+
+ render: function (box) {
+ var obj = this;
+ var time = (new Date()).getTime();
+ //if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ if (typeof box != 'undefined' && box != null) {
+ if ($(this.box).find('#grid_'+ this.name +'_body').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-grid')
+ .html('');
+ }
+ this.box = box;
+ }
+ if (!this.box) return;
+ if (this.last.sortData == null) this.last.sortData = this.sortData;
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'render', box: box });
+ if (eventData.isCancelled === true) return;
+ // insert Elements
+ $(this.box)
+ .attr('name', this.name)
+ .addClass('w2ui-reset w2ui-grid')
+ .html('<div>'+
+ ' <div id="grid_'+ this.name +'_header" class="w2ui-grid-header"></div>'+
+ ' <div id="grid_'+ this.name +'_toolbar" class="w2ui-grid-toolbar"></div>'+
+ ' <div id="grid_'+ this.name +'_body" class="w2ui-grid-body"></div>'+
+ ' <div id="grid_'+ this.name +'_summary" class="w2ui-grid-body w2ui-grid-summary"></div>'+
+ ' <div id="grid_'+ this.name +'_footer" class="w2ui-grid-footer"></div>'+
+ '</div>');
+ if (this.selectType != 'row') $(this.box).addClass('w2ui-ss');
+ if ($(this.box).length > 0) $(this.box)[0].style.cssText += this.style;
+ // init toolbar
+ this.initToolbar();
+ if (this.toolbar != null) this.toolbar.render($('#grid_'+ this.name +'_toolbar')[0]);
+ // reinit search_all
+ if (this.last.field && this.last.field != 'all') {
+ var sd = this.searchData;
+ this.initAllField(this.last.field, (sd.length == 1 ? sd[0].value : null));
+ }
+ // init footer
+ $('#grid_'+ this.name +'_footer').html(this.getFooterHTML());
+ // refresh
+ if (!this.last.state) this.last.state = this.stateSave(true); // initial default state
+ this.stateRestore();
+ if (this.url) this.refresh(); // show empty grid (need it) - should it be only for remote data source
+ this.reload();
+
+ // init mouse events for mouse selection
+ $(this.box).on('mousedown', mouseStart);
+ $(this.box).on('selectstart', function () { return false; }); // fixes chrome cursor bug
+
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ // attach to resize event
+ if ($('.w2ui-layout').length == 0) { // if there is layout, it will send a resize event
+ this.tmp_resize = function (event) { w2ui[obj.name].resize(); }
+ $(window).off('resize', this.tmp_resize).on('resize', this.tmp_resize);
+ }
+ return (new Date()).getTime() - time;
+
+ function mouseStart (event) {
+ if (event.which != 1) return; // if not left mouse button
+ // restore css user-select
+ if (obj.last.userSelect == 'text') {
+ delete obj.last.userSelect;
+ $(obj.box).find('.w2ui-grid-body')
+ .css('user-select', 'none')
+ .css('-webkit-user-select', 'none')
+ .css('-moz-user-select', 'none')
+ .css('-ms-user-select', 'none');
+ $(this.box).on('selectstart', function () { return false; });
+ }
+ // regular record select
+ if ($(event.target).parents().hasClass('w2ui-head') || $(event.target).hasClass('w2ui-head')) return;
+ if (obj.last.move && obj.last.move.type == 'expand') return;
+ // if altKey - alow text selection
+ if (event.altKey) {
+ $(obj.box).off('selectstart');
+ $(obj.box).find('.w2ui-grid-body')
+ .css('user-select', 'text')
+ .css('-webkit-user-select', 'text')
+ .css('-moz-user-select', 'text')
+ .css('-ms-user-select', 'text');
+ obj.selectNone();
+ obj.last.move = { type: 'text-select' };
+ obj.last.userSelect = 'text';
+ } else {
+ if (!obj.multiSelect) return;
+ obj.last.move = {
+ x : event.screenX,
+ y : event.screenY,
+ divX : 0,
+ divY : 0,
+ recid : $(event.target).parents('tr').attr('recid'),
+ column : (event.target.tagName == 'TD' ? $(event.target).attr('col') : $(event.target).parents('td').attr('col')),
+ type : 'select',
+ ghost : false,
+ start : true
+ };
+ }
+ $(document).on('mousemove', mouseMove);
+ $(document).on('mouseup', mouseStop);
+ }
+
+ function mouseMove (event) {
+ var mv = obj.last.move;
+ if (!mv || mv.type != 'select') return;
+ mv.divX = (event.screenX - mv.x);
+ mv.divY = (event.screenY - mv.y);
+ if (Math.abs(mv.divX) <= 1 && Math.abs(mv.divY) <= 1) return; // only if moved more then 1px
+ obj.last.cancelClick = true;
+ if (obj.reorderRows == true) {
+ if (!mv.ghost) {
+ var row = $('#grid_'+ obj.name + '_rec_'+ mv.recid);
+ var tmp = row.parents('table').find('tr:first-child').clone();
+ mv.offsetY = event.offsetY;
+ mv.from = mv.recid;
+ mv.pos = row.position();
+ mv.ghost = $(row).clone(true);
+ mv.ghost.removeAttr('id');
+ row.find('td:first-child').replaceWith('<td colspan="1000" style="height: '+ obj.recordHeight +'px; background-color: #ddd"></td>');
+ var recs = $(obj.box).find('.w2ui-grid-records');
+ recs.append('<table id="grid_'+ obj.name + '_ghost" style="position: absolute; z-index: 999999; opacity: 0.8; border-bottom: 2px dashed #aaa; border-top: 2px dashed #aaa; pointer-events: none;"></table>');
+ $('#grid_'+ obj.name + '_ghost').append(tmp).append(mv.ghost);
+ }
+ var recid = $(event.target).parents('tr').attr('recid');
+ if (recid != mv.from) {
+ var row1 = $('#grid_'+ obj.name + '_rec_'+ mv.recid);
+ var row2 = $('#grid_'+ obj.name + '_rec_'+ recid);
+ if (event.screenY - mv.lastY < 0) row1.after(row2); else row2.after(row1);
+ mv.lastY = event.screenY;
+ mv.to = recid;
+ }
+ var ghost = $('#grid_'+ obj.name + '_ghost');
+ var recs = $(obj.box).find('.w2ui-grid-records');
+ ghost.css({
+ top : mv.pos.top + mv.divY + recs.scrollTop(), // + mv.offsetY - obj.recordHeight / 2,
+ left : mv.pos.left
+ });
+ return;
+ }
+ if (mv.start && mv.recid) {
+ obj.selectNone();
+ mv.start = false;
+ }
+ var newSel= [];
+ var recid = (event.target.tagName == 'TR' ? $(event.target).attr('recid') : $(event.target).parents('tr').attr('recid'));
+ if (typeof recid == 'undefined') return;
+ var ind1 = obj.get(mv.recid, true);
+ // |:wolfmanx:| this happens when selection is started on summary row
+ if (ind1 === null) return;
+ var ind2 = obj.get(recid, true);
+ // this happens when selection is extended into summary row (a good place to implement scrolling)
+ if (ind2 === null) return;
+ var col1 = parseInt(mv.column);
+ var col2 = parseInt(event.target.tagName == 'TD' ? $(event.target).attr('col') : $(event.target).parents('td').attr('col'));
+ if (ind1 > ind2) { var tmp = ind1; ind1 = ind2; ind2 = tmp; }
+ // check if need to refresh
+ var tmp = 'ind1:'+ ind1 +',ind2;'+ ind2 +',col1:'+ col1 +',col2:'+ col2;
+ if (mv.range == tmp) return;
+ mv.range = tmp;
+ for (var i = ind1; i <= ind2; i++) {
+ if (obj.last.searchIds.length > 0 && obj.last.searchIds.indexOf(i) == -1) continue;
+ if (obj.selectType != 'row') {
+ if (col1 > col2) { var tmp = col1; col1 = col2; col2 = tmp; }
+ var tmp = [];
+ for (var c = col1; c <= col2; c++) {
+ if (obj.columns[c].hidden) continue;
+ newSel.push({ recid: obj.records[i].recid, column: parseInt(c) });
+ }
+ } else {
+ newSel.push(obj.records[i].recid);
+ }
+ }
+ if (obj.selectType != 'row') {
+ var sel = obj.getSelection();
+ // add more items
+ var tmp = [];
+ for (var ns in newSel) {
+ var flag = false;
+ for (var s in sel) if (newSel[ns].recid == sel[s].recid && newSel[ns].column == sel[s].column) flag = true;
+ if (!flag) tmp.push({ recid: newSel[ns].recid, column: newSel[ns].column });
+ }
+ obj.select.apply(obj, tmp);
+ // remove items
+ var tmp = [];
+ for (var s in sel) {
+ var flag = false;
+ for (var ns in newSel) if (newSel[ns].recid == sel[s].recid && newSel[ns].column == sel[s].column) flag = true;
+ if (!flag) tmp.push({ recid: sel[s].recid, column: sel[s].column });
+ }
+ obj.unselect.apply(obj, tmp);
+ } else {
+ if (obj.multiSelect) {
+ var sel = obj.getSelection();
+ for (var ns in newSel) if (sel.indexOf(newSel[ns]) == -1) obj.select(newSel[ns]); // add more items
+ for (var s in sel) if (newSel.indexOf(sel[s]) == -1) obj.unselect(sel[s]); // remove items
+ }
+ }
+ }
+
+ function mouseStop (event) {
+ var mv = obj.last.move;
+ setTimeout(function () { delete obj.last.cancelClick; }, 1);
+ if ($(event.target).parents().hasClass('.w2ui-head') || $(event.target).hasClass('.w2ui-head')) return;
+ if (mv && mv.type == 'select') {
+ if (obj.reorderRows == true) {
+ var ind1 = obj.get(mv.from, true);
+ var tmp = obj.records[ind1];
+ obj.records.splice(ind1, 1);
+ var ind2 = obj.get(mv.to, true);
+ if (ind1 > ind2) obj.records.splice(ind2, 0, tmp); else obj.records.splice(ind2+1, 0, tmp);
+ $('#grid_'+ obj.name + '_ghost').remove();
+ obj.refresh();
+ }
+ }
+ delete obj.last.move;
+ $(document).off('mousemove', mouseMove);
+ $(document).off('mouseup', mouseStop);
+ }
+ },
+
+ destroy: function () {
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'destroy' });
+ if (eventData.isCancelled === true) return;
+ // remove events
+ $(window).off('resize', this.tmp_resize);
+ // clean up
+ if (typeof this.toolbar == 'object' && this.toolbar.destroy) this.toolbar.destroy();
+ if ($(this.box).find('#grid_'+ this.name +'_body').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-grid')
+ .html('');
+ }
+ delete w2ui[this.name];
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ // ===========================================
+ // --- Internal Functions
+
+ initColumnOnOff: function () {
+ if (!this.show.toolbarColumns) return;
+ var obj = this;
+ var col_html = '<div class="w2ui-col-on-off">'+
+ '<table><tr>'+
+ '<td style="width: 30px">'+
+ ' <input id="grid_'+ this.name +'_column_ln_check" type="checkbox" tabIndex="-1" '+ (obj.show.lineNumbers ? 'checked' : '') +
+ ' onclick="w2ui[\''+ obj.name +'\'].columnOnOff(this, event, \'line-numbers\');">'+
+ '</td>'+
+ '<td onclick="w2ui[\''+ obj.name +'\'].columnOnOff(this, event, \'line-numbers\'); $(\'#w2ui-overlay\')[0].hide();">'+
+ ' <label for="grid_'+ this.name +'_column_ln_check">'+ w2utils.lang('Line #') +'</label>'+
+ '</td></tr>';
+ for (var c in this.columns) {
+ var col = this.columns[c];
+ var tmp = this.columns[c].caption;
+ if (col.hideable === false) continue;
+ if (!tmp && this.columns[c].hint) tmp = this.columns[c].hint;
+ if (!tmp) tmp = '- column '+ (parseInt(c) + 1) +' -';
+ col_html += '<tr>'+
+ '<td style="width: 30px">'+
+ ' <input id="grid_'+ this.name +'_column_'+ c +'_check" type="checkbox" tabIndex="-1" '+ (col.hidden ? '' : 'checked') +
+ ' onclick="w2ui[\''+ obj.name +'\'].columnOnOff(this, event, \''+ col.field +'\');">'+
+ '</td>'+
+ '<td>'+
+ ' <label for="grid_'+ this.name +'_column_'+ c +'_check">'+ tmp + '</label>'+
+ '</td>'+
+ '</tr>';
+ }
+ col_html += '<tr><td colspan="2"><div style="border-top: 1px solid #ddd;"></div></td></tr>';
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (url && obj.show.skipRecords) {
+ col_html +=
+ '<tr><td colspan="2" style="padding: 0px">'+
+ ' <div style="cursor: pointer; padding: 2px 8px; cursor: default">'+ w2utils.lang('Skip') +
+ ' <input type="text" style="width: 45px" value="'+ this.offset +'" '+
+ ' onkeypress="if (event.keyCode == 13) { '+
+ ' w2ui[\''+ obj.name +'\'].skip(this.value); '+
+ ' $(\'#w2ui-overlay\')[0].hide(); '+
+ ' }"> '+ w2utils.lang('Records')+
+ ' </div>'+
+ '</td></tr>';
+ }
+ col_html += '<tr><td colspan="2" onclick="w2ui[\''+ obj.name +'\'].stateSave(); $(\'#w2ui-overlay\')[0].hide();">'+
+ ' <div style="cursor: pointer; padding: 4px 8px; cursor: default">'+ w2utils.lang('Save Grid State') + '</div>'+
+ '</td></tr>'+
+ '<tr><td colspan="2" onclick="w2ui[\''+ obj.name +'\'].stateReset(); $(\'#w2ui-overlay\')[0].hide();">'+
+ ' <div style="cursor: pointer; padding: 4px 8px; cursor: default">'+ w2utils.lang('Restore Default State') + '</div>'+
+ '</td></tr>';
+ col_html += "</table></div>";
+ this.toolbar.get('w2ui-column-on-off').html = col_html;
+ },
+
+ /**
+ *
+ * @param box, grid object
+ * @returns {{remove: Function}} contains a closure around all events to ensure they are removed from the dom
+ */
+ initColumnDrag: function ( box ) {
+ //throw error if using column groups
+ if ( this.columnGroups && this.columnGroups.length ) throw 'Draggable columns are not currently supported with column groups.';
+
+ var obj = this,
+ _dragData = {};
+ _dragData.lastInt = null;
+ _dragData.pressed = false;
+ _dragData.timeout = null;_dragData.columnHead = null;
+
+ //attach orginal event listener
+ $(obj.box).on('mousedown', dragColStart);
+ $(obj.box).on('mouseup', catchMouseup);
+
+ function catchMouseup(){
+ _dragData.pressed = false;
+ clearTimeout( _dragData.timeout );
+ }
+ /**
+ *
+ * @param event, mousedown
+ * @returns {boolean} false, preventsDefault
+ */
+ function dragColStart ( event ) {
+ if ( _dragData.timeout ) clearTimeout( _dragData.timeout );
+ var self = this;
+ _dragData.pressed = true;
+
+ _dragData.timeout = setTimeout(function(){
+ if ( !_dragData.pressed ) return;
+
+ var eventData,
+ columns,
+ selectedCol,
+ origColumn,
+ origColumnNumber,
+ invalidPreColumns = [ 'w2ui-col-number', 'w2ui-col-expand', 'w2ui-col-select' ],
+ invalidPostColumns = [ 'w2ui-head-last' ],
+ invalidColumns = invalidPreColumns.concat( invalidPostColumns ),
+ preColumnsSelector = '.w2ui-col-number, .w2ui-col-expand, .w2ui-col-select',
+ preColHeadersSelector = '.w2ui-head.w2ui-col-number, .w2ui-head.w2ui-col-expand, .w2ui-head.w2ui-col-select';
+
+ // do nothing if it is not a header
+ if ( !$( event.originalEvent.target ).parents().hasClass( 'w2ui-head' ) ) return;
+
+ // do nothing if it is an invalid column
+ for ( var i = 0, l = invalidColumns.length; i < l; i++ ){
+ if ( $( event.originalEvent.target ).parents().hasClass( invalidColumns[ i ] ) ) return;
+ }
+
+ _dragData.numberPreColumnsPresent = $( obj.box ).find( preColHeadersSelector ).length;
+
+ //start event for drag start
+ _dragData.columnHead = origColumn = $( event.originalEvent.target ).parents( '.w2ui-head' );
+ origColumnNumber = parseInt( origColumn.attr( 'col' ), 10);
+ eventData = obj.trigger({ type: 'columnDragStart', phase: 'before', originalEvent: event, origColumnNumber: origColumnNumber, target: origColumn[0] });
+ if ( eventData.isCancelled === true ) return false;
+
+ columns = _dragData.columns = $( obj.box ).find( '.w2ui-head:not(.w2ui-head-last)' );
+
+ //add events
+ $( document ).on( 'mouseup', dragColEnd );
+ $( document ).on( 'mousemove', dragColOver );
+
+ _dragData.originalPos = parseInt( $( event.originalEvent.target ).parent( '.w2ui-head' ).attr( 'col' ), 10 );
+ //_dragData.columns.css({ overflow: 'visible' }).children( 'div' ).css({ overflow: 'visible' });
+
+ //configure and style ghost image
+ _dragData.ghost = $( self ).clone( true );
+
+ //hide other elements on ghost except the grid body
+ $( _dragData.ghost ).find( '[col]:not([col="' + _dragData.originalPos + '"]), .w2ui-toolbar, .w2ui-grid-header' ).remove();
+ $( _dragData.ghost ).find( preColumnsSelector ).remove();
+ $( _dragData.ghost ).find( '.w2ui-grid-body' ).css({ top: 0 });
+
+ selectedCol = $( _dragData.ghost ).find( '[col="' + _dragData.originalPos + '"]' );
+ $( document.body ).append( _dragData.ghost );
+
+ $( _dragData.ghost ).css({
+ width: 0,
+ height: 0,
+ margin: 0,
+ position: 'fixed',
+ zIndex: 999999,
+ opacity: 0
+ }).addClass( '.w2ui-grid-ghost' ).animate({
+ width: selectedCol.width(),
+ height: $(obj.box).find('.w2ui-grid-body:first').height(),
+ left : event.pageX,
+ top : event.pageY,
+ opacity: .8
+ }, 0 );
+
+ //establish current offsets
+ _dragData.offsets = [];
+ for ( var i = 0, l = columns.length; i < l; i++ ) {
+ _dragData.offsets.push( $( columns[ i ] ).offset().left );
+ }
+
+ //conclude event
+ obj.trigger( $.extend( eventData, { phase: 'after' } ) );
+ }, 150 );//end timeout wrapper
+ }
+
+ function dragColOver ( event ) {
+ if ( !_dragData.pressed ) return;
+
+ var cursorX = event.originalEvent.pageX,
+ cursorY = event.originalEvent.pageY,
+ offsets = _dragData.offsets,
+ lastWidth = $( '.w2ui-head:not(.w2ui-head-last)' ).width();
+
+ _dragData.targetInt = Math.max(_dragData.numberPreColumnsPresent,targetIntersection( cursorX, offsets, lastWidth ));
+
+ markIntersection( _dragData.targetInt );
+ trackGhost( cursorX, cursorY );
+ }
+
+ function dragColEnd ( event ) {
+ _dragData.pressed = false;
+
+ var eventData,
+ target,
+ selected,
+ columnConfig,
+ targetColumn,
+ ghosts = $( '.w2ui-grid-ghost' );
+
+ //start event for drag start
+ eventData = obj.trigger({ type: 'columnDragEnd', phase: 'before', originalEvent: event, target: _dragData.columnHead[0] });
+ if ( eventData.isCancelled === true ) return false;
+
+ selected = obj.columns[ _dragData.originalPos ];
+ columnConfig = obj.columns;
+ targetColumn = $( _dragData.columns[ Math.min(_dragData.lastInt, _dragData.columns.length - 1) ] );
+ target = (_dragData.lastInt < _dragData.columns.length) ? parseInt(targetColumn.attr('col')) : columnConfig.length;
+
+ if ( target !== _dragData.originalPos + 1 && target !== _dragData.originalPos && targetColumn && targetColumn.length ) {
+ $( _dragData.ghost ).animate({
+ top: $( obj.box ).offset().top,
+ left: targetColumn.offset().left,
+ width: 0,
+ height: 0,
+ opacity:.2
+ }, 300, function(){
+ $( this ).remove();
+ ghosts.remove();
+ });
+
+ columnConfig.splice( target, 0, $.extend( {}, selected ) );
+ columnConfig.splice( columnConfig.indexOf( selected ), 1);
+
+ } else {
+ $( _dragData.ghost ).remove();
+ ghosts.remove();
+ }
+
+ //_dragData.columns.css({ overflow: '' }).children( 'div' ).css({ overflow: '' });
+
+ $( document ).off( 'mouseup', dragColEnd );
+ $( document ).off( 'mousemove', dragColOver );
+ if ( _dragData.marker ) _dragData.marker.remove();
+ _dragData = {};
+
+ obj.refresh();
+
+ //conclude event
+ obj.trigger( $.extend( eventData, { phase: 'after', targetColumnNumber: target - 1 } ) );
+ }
+
+ function markIntersection( intersection ){
+ if ( !_dragData.marker && !_dragData.markerLeft ) {
+ _dragData.marker = $('<div class="col-intersection-marker">' +
+ '<div class="top-marker"></div>' +
+ '<div class="bottom-marker"></div>' +
+ '</div>');
+ _dragData.markerLeft = $('<div class="col-intersection-marker">' +
+ '<div class="top-marker"></div>' +
+ '<div class="bottom-marker"></div>' +
+ '</div>');
+ }
+
+ if ( !_dragData.lastInt || _dragData.lastInt !== intersection ){
+ _dragData.lastInt = intersection;
+ _dragData.marker.remove();
+ _dragData.markerLeft.remove();
+ $('.w2ui-head').removeClass('w2ui-col-intersection');
+
+ //if the current intersection is greater than the number of columns add the marker to the end of the last column only
+ if ( intersection >= _dragData.columns.length ) {
+ $( _dragData.columns[ _dragData.columns.length - 1 ] ).children( 'div:last' ).append( _dragData.marker.addClass( 'right' ).removeClass( 'left' ) );
+ $( _dragData.columns[ _dragData.columns.length - 1 ] ).addClass('w2ui-col-intersection');
+ } else if ( intersection <= _dragData.numberPreColumnsPresent ) {
+ //if the current intersection is on the column numbers place marker on first available column only
+ $( _dragData.columns[ _dragData.numberPreColumnsPresent ] ).prepend( _dragData.marker.addClass( 'left' ).removeClass( 'right' ) ).css({ position: 'relative' });
+ $( _dragData.columns[ _dragData.numberPreColumnsPresent ] ).prev().addClass('w2ui-col-intersection');
+ } else {
+ //otherwise prepend the marker to the targeted column and append it to the previous column
+ $( _dragData.columns[intersection] ).children( 'div:last' ).prepend( _dragData.marker.addClass( 'left' ).removeClass( 'right' ) );
+ $( _dragData.columns[intersection] ).prev().children( 'div:last' ).append( _dragData.markerLeft.addClass( 'right' ).removeClass( 'left' ) ).css({ position: 'relative' });
+ $( _dragData.columns[intersection - 1] ).addClass('w2ui-col-intersection');
+ }
+ }
+ }
+
+ function targetIntersection( cursorX, offsets, lastWidth ){
+ if ( cursorX <= offsets[0] ) {
+ return 0;
+ } else if ( cursorX >= offsets[offsets.length - 1] + lastWidth ) {
+ return offsets.length;
+ } else {
+ for ( var i = 0, l = offsets.length; i < l; i++ ) {
+ var thisOffset = offsets[ i ];
+ var nextOffset = offsets[ i + 1 ] || offsets[ i ] + lastWidth;
+ var midpoint = ( nextOffset - offsets[ i ]) / 2 + offsets[ i ];
+
+ if ( cursorX > thisOffset && cursorX <= midpoint ) {
+ return i;
+ } else if ( cursorX > midpoint && cursorX <= nextOffset ) {
+ return i + 1;
+ }
+ }
+ return intersection;
+ }
+ }
+
+ function trackGhost( cursorX, cursorY ){
+ $( _dragData.ghost ).css({
+ left: cursorX - 10,
+ top: cursorY - 10
+ });
+ }
+
+ //return an object to remove drag if it has ever been enabled
+ return {
+ remove: function(){
+ $( obj.box ).off( 'mousedown', dragColStart );
+ $( obj.box ).off( 'mouseup', catchMouseup );
+ $( obj.box ).find( '.w2ui-head' ).removeAttr( 'draggable' );
+ obj.last.columnDrag = false;
+ }
+ }
+ },
+
+ columnOnOff: function (el, event, field) {
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'columnOnOff', checkbox: el, field: field, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // regular processing
+ var obj = this;
+ // collapse expanded rows
+ for (var r in this.records) {
+ if (this.records[r].expanded === true) this.records[r].expanded = false
+ }
+ // show/hide
+ var hide = true;
+ if (field == 'line-numbers') {
+ this.show.lineNumbers = !this.show.lineNumbers;
+ this.refresh();
+ } else {
+ var col = this.getColumn(field);
+ if (col.hidden) {
+ $(el).prop('checked', true);
+ this.showColumn(col.field);
+ } else {
+ $(el).prop('checked', false);
+ this.hideColumn(col.field);
+ }
+ hide = false;
+ }
+ if (hide) {
+ setTimeout(function () {
+ $().w2overlay('', { name: 'searches-'+ this.name });
+ obj.toolbar.uncheck('column-on-off');
+ }, 100);
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ initToolbar: function () {
+ // -- if toolbar is true
+ if (typeof this.toolbar['render'] == 'undefined') {
+ var tmp_items = this.toolbar.items;
+ this.toolbar.items = [];
+ this.toolbar = $().w2toolbar($.extend(true, {}, this.toolbar, { name: this.name +'_toolbar', owner: this }));
+
+ // =============================================
+ // ------ Toolbar Generic buttons
+
+ if (this.show.toolbarReload) {
+ this.toolbar.items.push($.extend(true, {}, this.buttons['reload']));
+ }
+ if (this.show.toolbarColumns) {
+ this.toolbar.items.push($.extend(true, {}, this.buttons['columns']));
+ }
+ if (this.show.toolbarReload || this.show.toolbarColumn) {
+ this.toolbar.items.push({ type: 'break', id: 'w2ui-break0' });
+ }
+ if (this.show.toolbarSearch) {
+ var html =
+ '<div class="w2ui-toolbar-search">'+
+ '<table cellpadding="0" cellspacing="0"><tr>'+
+ ' <td>'+ this.buttons['search'].html +'</td>'+
+ ' <td>'+
+ ' <input id="grid_'+ this.name +'_search_all" class="w2ui-search-all" '+
+ ' placeholder="'+ this.last.caption +'" value="'+ this.last.search +'"'+
+ ' onkeydown="if (event.keyCode == 13 && w2utils.isIE) this.onchange();"'+
+ ' onchange="'+
+ ' var val = this.value; '+
+ ' var fld = $(this).data(\'w2field\'); '+
+ ' if (fld) val = fld.clean(val);'+
+ ' w2ui[\''+ this.name +'\'].search(w2ui[\''+ this.name +'\'].last.field, val); '+
+ ' ">'+
+ ' </td>'+
+ ' <td>'+
+ ' <div title="'+ w2utils.lang('Clear Search') +'" class="w2ui-search-clear" id="grid_'+ this.name +'_searchClear" '+
+ ' onclick="var obj = w2ui[\''+ this.name +'\']; obj.searchReset();" '+
+ ' >&nbsp;&nbsp;</div>'+
+ ' </td>'+
+ '</tr></table>'+
+ '</div>';
+ this.toolbar.items.push({ type: 'html', id: 'w2ui-search', html: html });
+ if (this.multiSearch && this.searches.length > 0) {
+ this.toolbar.items.push($.extend(true, {}, this.buttons['search-go']));
+ }
+ }
+ if (this.show.toolbarSearch && (this.show.toolbarAdd || this.show.toolbarEdit || this.show.toolbarDelete || this.show.toolbarSave)) {
+ this.toolbar.items.push({ type: 'break', id: 'w2ui-break1' });
+ }
+ if (this.show.toolbarAdd) {
+ this.toolbar.items.push($.extend(true, {}, this.buttons['add']));
+ }
+ if (this.show.toolbarEdit) {
+ this.toolbar.items.push($.extend(true, {}, this.buttons['edit']));
+ }
+ if (this.show.toolbarDelete) {
+ this.toolbar.items.push($.extend(true, {}, this.buttons['delete']));
+ }
+ if (this.show.toolbarSave) {
+ if (this.show.toolbarAdd || this.show.toolbarDelete || this.show.toolbarEdit) {
+ this.toolbar.items.push({ type: 'break', id: 'w2ui-break2' });
+ }
+ this.toolbar.items.push($.extend(true, {}, this.buttons['save']));
+ }
+ // add original buttons
+ for (var i in tmp_items) this.toolbar.items.push(tmp_items[i]);
+
+ // =============================================
+ // ------ Toolbar onClick processing
+
+ var obj = this;
+ this.toolbar.on('click', function (event) {
+ var eventData = obj.trigger({ phase: 'before', type: 'toolbar', target: event.target, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ var id = event.target;
+ switch (id) {
+ case 'w2ui-reload':
+ var eventData2 = obj.trigger({ phase: 'before', type: 'reload', target: obj.name });
+ if (eventData2.isCancelled === true) return false;
+ obj.reload();
+ obj.trigger($.extend(eventData2, { phase: 'after' }));
+ break;
+ case 'w2ui-column-on-off':
+ obj.initColumnOnOff();
+ obj.initResize();
+ obj.resize();
+ break;
+ case 'w2ui-search-advanced':
+ var tb = this;
+ var it = this.get(id);
+ if (it.checked) {
+ obj.searchClose();
+ setTimeout(function () { tb.uncheck(id); }, 1);
+ } else {
+ obj.searchOpen();
+ event.originalEvent.stopPropagation();
+ function tmp_close() {
+ if ($('#w2ui-overlay-searches-'+ obj.name).data('keepOpen') === true) return;
+ tb.uncheck(id);
+ $(document).off('click', 'body', tmp_close);
+ }
+ $(document).on('click', 'body', tmp_close);
+ }
+ break;
+ case 'w2ui-add':
+ // events
+ var eventData = obj.trigger({ phase: 'before', target: obj.name, type: 'add', recid: null });
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ break;
+ case 'w2ui-edit':
+ var sel = obj.getSelection();
+ var recid = null;
+ if (sel.length == 1) recid = sel[0];
+ // events
+ var eventData = obj.trigger({ phase: 'before', target: obj.name, type: 'edit', recid: recid });
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ break;
+ case 'w2ui-delete':
+ obj["delete"]();
+ break;
+ case 'w2ui-save':
+ obj.save();
+ break;
+ }
+ // no default action
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ });
+ }
+ return;
+ },
+
+ initResize: function () {
+ var obj = this;
+ //if (obj.resizing === true) return;
+ $(this.box).find('.w2ui-resizer')
+ .off('click')
+ .on('click', function (event) {
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ if (event.preventDefault) event.preventDefault();
+ })
+ .off('mousedown')
+ .on('mousedown', function (event) {
+ if (!event) event = window.event;
+ if (!window.addEventListener) { window.document.attachEvent('onselectstart', function() { return false; } ); }
+ obj.resizing = true;
+ obj.last.tmp = {
+ x : event.screenX,
+ y : event.screenY,
+ gx : event.screenX,
+ gy : event.screenY,
+ col : parseInt($(this).attr('name'))
+ };
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ if (event.preventDefault) event.preventDefault();
+ // fix sizes
+ for (var c in obj.columns) {
+ if (typeof obj.columns[c].sizeOriginal == 'undefined') obj.columns[c].sizeOriginal = obj.columns[c].size;
+ obj.columns[c].size = obj.columns[c].sizeCalculated;
+ }
+ var eventData = { phase: 'before', type: 'columnResize', target: obj.name, column: obj.last.tmp.col, field: obj.columns[obj.last.tmp.col].field };
+ eventData = obj.trigger($.extend(eventData, { resizeBy: 0, originalEvent: event }));
+ // set move event
+ var mouseMove = function (event) {
+ if (obj.resizing != true) return;
+ if (!event) event = window.event;
+ // event before
+ eventData = obj.trigger($.extend(eventData, { resizeBy: (event.screenX - obj.last.tmp.gx), originalEvent: event }));
+ if (eventData.isCancelled === true) { eventData.isCancelled = false; return; }
+ // default action
+ obj.last.tmp.x = (event.screenX - obj.last.tmp.x);
+ obj.last.tmp.y = (event.screenY - obj.last.tmp.y);
+ obj.columns[obj.last.tmp.col].size = (parseInt(obj.columns[obj.last.tmp.col].size) + obj.last.tmp.x) + 'px';
+ obj.resizeRecords();
+ // reset
+ obj.last.tmp.x = event.screenX;
+ obj.last.tmp.y = event.screenY;
+ }
+ var mouseUp = function (event) {
+ delete obj.resizing;
+ $(document).off('mousemove', 'body');
+ $(document).off('mouseup', 'body');
+ obj.resizeRecords();
+ // event before
+ obj.trigger($.extend(eventData, { phase: 'after', originalEvent: event }));
+ }
+ $(document).on('mousemove', 'body', mouseMove);
+ $(document).on('mouseup', 'body', mouseUp);
+ })
+ .each(function (index, el) {
+ var td = $(el).parent();
+ $(el).css({
+ "height" : '25px',
+ "margin-left" : (td.width() - 3) + 'px'
+ })
+ });
+ },
+
+ resizeBoxes: function () {
+ // elements
+ var main = $(this.box).find('> div');
+ var header = $('#grid_'+ this.name +'_header');
+ var toolbar = $('#grid_'+ this.name +'_toolbar');
+ var summary = $('#grid_'+ this.name +'_summary');
+ var footer = $('#grid_'+ this.name +'_footer');
+ var body = $('#grid_'+ this.name +'_body');
+ var columns = $('#grid_'+ this.name +'_columns');
+ var records = $('#grid_'+ this.name +'_records');
+
+ if (this.show.header) {
+ header.css({
+ top: '0px',
+ left: '0px',
+ right: '0px'
+ });
+ }
+
+ if (this.show.toolbar) {
+ toolbar.css({
+ top: ( 0 + (this.show.header ? w2utils.getSize(header, 'height') : 0) ) + 'px',
+ left: '0px',
+ right: '0px'
+ });
+ }
+ if (this.show.footer) {
+ footer.css({
+ bottom: '0px',
+ left: '0px',
+ right: '0px'
+ });
+ }
+ if (this.summary.length > 0) {
+ summary.css({
+ bottom: ( 0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0) ) + 'px',
+ left: '0px',
+ right: '0px'
+ });
+ }
+ body.css({
+ top: ( 0 + (this.show.header ? w2utils.getSize(header, 'height') : 0) + (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0) ) + 'px',
+ bottom: ( 0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0) + (this.summary.length > 0 ? w2utils.getSize(summary, 'height') : 0) ) + 'px',
+ left: '0px',
+ right: '0px'
+ });
+ },
+
+ resizeRecords: function () {
+ var obj = this;
+ // remove empty records
+ $(this.box).find('.w2ui-empty-record').remove();
+ // -- Calculate Column size in PX
+ var box = $(this.box);
+ var grid = $(this.box).find('> div');
+ var header = $('#grid_'+ this.name +'_header');
+ var toolbar = $('#grid_'+ this.name +'_toolbar');
+ var summary = $('#grid_'+ this.name +'_summary');
+ var footer = $('#grid_'+ this.name +'_footer');
+ var body = $('#grid_'+ this.name +'_body');
+ var columns = $('#grid_'+ this.name +'_columns');
+ var records = $('#grid_'+ this.name +'_records');
+
+ // body might be expanded by data
+ if (!this.fixedBody) {
+ // allow it to render records, then resize
+ var calculatedHeight = w2utils.getSize(columns, 'height')
+ + w2utils.getSize($('#grid_'+ obj.name +'_records table'), 'height');
+ obj.height = calculatedHeight
+ + w2utils.getSize(grid, '+height')
+ + (obj.show.header ? w2utils.getSize(header, 'height') : 0)
+ + (obj.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0)
+ + (summary.css('display') != 'none' ? w2utils.getSize(summary, 'height') : 0)
+ + (obj.show.footer ? w2utils.getSize(footer, 'height') : 0);
+ grid.css('height', obj.height);
+ body.css('height', calculatedHeight);
+ box.css('height', w2utils.getSize(grid, 'height') + w2utils.getSize(box, '+height'));
+ } else {
+ // fixed body height
+ var calculatedHeight = grid.height()
+ - (this.show.header ? w2utils.getSize(header, 'height') : 0)
+ - (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0)
+ - (summary.css('display') != 'none' ? w2utils.getSize(summary, 'height') : 0)
+ - (this.show.footer ? w2utils.getSize(footer, 'height') : 0);
+ body.css('height', calculatedHeight);
+ }
+
+ var buffered = this.records.length;
+ if (this.searchData.length != 0 && !this.url) buffered = this.last.searchIds.length;
+ // check overflow
+ var bodyOverflowX = false;
+ var bodyOverflowY = false;
+ if (body.width() < $(records).find('>table').width()) bodyOverflowX = true;
+ if (body.height() - columns.height() < $(records).find('>table').height() + (bodyOverflowX ? w2utils.scrollBarSize() : 0)) bodyOverflowY = true;
+ if (!this.fixedBody) { bodyOverflowY = false; bodyOverflowX = false; }
+ if (bodyOverflowX || bodyOverflowY) {
+ columns.find('> table > tbody > tr:nth-child(1) td.w2ui-head-last').css('width', w2utils.scrollBarSize()).show();
+ records.css({
+ top: ((this.columnGroups.length > 0 && this.show.columns ? 1 : 0) + w2utils.getSize(columns, 'height')) +'px',
+ "-webkit-overflow-scrolling": "touch",
+ "overflow-x": (bodyOverflowX ? 'auto' : 'hidden'),
+ "overflow-y": (bodyOverflowY ? 'auto' : 'hidden') });
+ } else {
+ columns.find('> table > tbody > tr:nth-child(1) td.w2ui-head-last').hide();
+ records.css({
+ top: ((this.columnGroups.length > 0 && this.show.columns ? 1 : 0) + w2utils.getSize(columns, 'height')) +'px',
+ overflow: 'hidden'
+ });
+ if (records.length > 0) { this.last.scrollTop = 0; this.last.scrollLeft = 0; } // if no scrollbars, always show top
+ }
+ if (this.show.emptyRecords && !bodyOverflowY) {
+ var max = Math.floor(records.height() / this.recordHeight) + 1;
+ if (this.fixedBody) {
+ for (var di = buffered; di <= max; di++) {
+ var html = '';
+ html += '<tr class="'+ (di % 2 ? 'w2ui-even' : 'w2ui-odd') + ' w2ui-empty-record" style="height: '+ this.recordHeight +'px">';
+ if (this.show.lineNumbers) html += '<td class="w2ui-col-number"></td>';
+ if (this.show.selectColumn) html += '<td class="w2ui-grid-data w2ui-col-select"></td>';
+ if (this.show.expandColumn) html += '<td class="w2ui-grid-data w2ui-col-expand"></td>';
+ var j = 0;
+ while (true && this.columns.length > 0) {
+ var col = this.columns[j];
+ if (col.hidden) { j++; if (typeof this.columns[j] == 'undefined') break; else continue; }
+ html += '<td class="w2ui-grid-data" '+ (typeof col.attr != 'undefined' ? col.attr : '') +' col="'+ j +'"></td>';
+ j++;
+ if (typeof this.columns[j] == 'undefined') break;
+ }
+ html += '<td class="w2ui-grid-data-last"></td>';
+ html += '</tr>';
+ $('#grid_'+ this.name +'_records > table').append(html);
+ }
+ }
+ }
+ if (body.length > 0) {
+ var width_max = parseInt(body.width())
+ - (bodyOverflowY ? w2utils.scrollBarSize() : 0)
+ - (this.show.lineNumbers ? 34 : 0)
+ - (this.show.selectColumn ? 26 : 0)
+ - (this.show.expandColumn ? 26 : 0);
+ var width_box = width_max;
+ var percent = 0;
+ // gridMinWidth processiong
+ var restart = false;
+ for (var i = 0; i < this.columns.length; i++) {
+ var col = this.columns[i];
+ if (col.gridMinWidth > 0) {
+ if (col.gridMinWidth > width_box && col.hidden !== true) {
+ col.hidden = true;
+ restart = true;
+ }
+ if (col.gridMinWidth < width_box && col.hidden === true) {
+ col.hidden = false;
+ restart = true;
+ }
+ }
+ }
+ if (restart === true) {
+ this.refresh();
+ return;
+ }
+ // assign PX column s
+ for (var i = 0; i < this.columns.length; i++) {
+ var col = this.columns[i];
+ if (col.hidden) continue;
+ if (String(col.size).substr(String(col.size).length-2).toLowerCase() == 'px') {
+ width_max -= parseFloat(col.size);
+ this.columns[i].sizeCalculated = col.size;
+ this.columns[i].sizeType = 'px';
+ } else {
+ percent += parseFloat(col.size);
+ this.columns[i].sizeType = '%';
+ delete col.sizeCorrected;
+ }
+ }
+ // if sum != 100% -- reassign proportionally
+ if (percent != 100 && percent > 0) {
+ for (var i = 0; i < this.columns.length; i++) {
+ var col = this.columns[i];
+ if (col.hidden) continue;
+ if (col.sizeType == '%') {
+ col.sizeCorrected = Math.round(parseFloat(col.size) * 100 * 100 / percent) / 100 + '%';
+ }
+ }
+ }
+ // calculate % columns
+ for (var i = 0; i < this.columns.length; i++) {
+ var col = this.columns[i];
+ if (col.hidden) continue;
+ if (col.sizeType == '%') {
+ if (typeof this.columns[i].sizeCorrected != 'undefined') {
+ // make it 1px smaller, so margin of error can be calculated correctly
+ this.columns[i].sizeCalculated = Math.floor(width_max * parseFloat(col.sizeCorrected) / 100) - 1 + 'px';
+ } else {
+ // make it 1px smaller, so margin of error can be calculated correctly
+ this.columns[i].sizeCalculated = Math.floor(width_max * parseFloat(col.size) / 100) - 1 + 'px';
+ }
+ }
+ }
+ }
+ // fix margin of error that is due percentage calculations
+ var width_cols = 0;
+ for (var i = 0; i < this.columns.length; i++) {
+ var col = this.columns[i];
+ if (col.hidden) continue;
+ if (typeof col.min == 'undefined') col.min = 20;
+ if (parseInt(col.sizeCalculated) < parseInt(col.min)) col.sizeCalculated = col.min + 'px';
+ if (parseInt(col.sizeCalculated) > parseInt(col.max)) col.sizeCalculated = col.max + 'px';
+ width_cols += parseInt(col.sizeCalculated);
+ }
+ var width_diff = parseInt(width_box) - parseInt(width_cols);
+ if (width_diff > 0 && percent > 0) {
+ var i = 0;
+ while (true) {
+ var col = this.columns[i];
+ if (typeof col == 'undefined') { i = 0; continue; }
+ if (col.hidden || col.sizeType == 'px') { i++; continue; }
+ col.sizeCalculated = (parseInt(col.sizeCalculated) + 1) + 'px';
+ width_diff--;
+ if (width_diff == 0) break;
+ i++;
+ }
+ } else if (width_diff > 0) {
+ columns.find('> table > tbody > tr:nth-child(1) td.w2ui-head-last').css('width', w2utils.scrollBarSize()).show();
+ }
+ // resize columns
+ columns.find('> table > tbody > tr:nth-child(1) td').each(function (index, el) {
+ var ind = $(el).attr('col');
+ if (typeof ind != 'undefined' && obj.columns[ind]) $(el).css('width', obj.columns[ind].sizeCalculated);
+ // last column
+ if ($(el).hasClass('w2ui-head-last')) {
+ $(el).css('width', w2utils.scrollBarSize() + (width_diff > 0 && percent == 0 ? width_diff : 0) + 'px');
+ }
+ });
+ // if there are column groups - hide first row (needed for sizing)
+ if (columns.find('> table > tbody > tr').length == 3) {
+ columns.find('> table > tbody > tr:nth-child(1) td').html('').css({
+ 'height' : '0px',
+ 'border' : '0px',
+ 'padding': '0px',
+ 'margin' : '0px'
+ });
+ }
+ // resize records
+ records.find('> table > tbody > tr:nth-child(1) td').each(function (index, el) {
+ var ind = $(el).attr('col');
+ if (typeof ind != 'undefined' && obj.columns[ind]) $(el).css('width', obj.columns[ind].sizeCalculated);
+ // last column
+ if ($(el).hasClass('w2ui-grid-data-last')) {
+ $(el).css('width', (width_diff > 0 && percent == 0 ? width_diff : 0) + 'px');
+ }
+ });
+ // resize summary
+ summary.find('> table > tbody > tr:nth-child(1) td').each(function (index, el) {
+ var ind = $(el).attr('col');
+ if (typeof ind != 'undefined' && obj.columns[ind]) $(el).css('width', obj.columns[ind].sizeCalculated);
+ // last column
+ if ($(el).hasClass('w2ui-grid-data-last')) {
+ $(el).css('width', w2utils.scrollBarSize() + (width_diff > 0 && percent == 0 ? width_diff : 0) + 'px');
+ }
+ });
+ this.initResize();
+ this.refreshRanges();
+ // apply last scroll if any
+ if (this.last.scrollTop != '' && records.length > 0) {
+ columns.prop('scrollLeft', this.last.scrollLeft);
+ records.prop('scrollTop', this.last.scrollTop);
+ records.prop('scrollLeft', this.last.scrollLeft);
+ }
+ },
+
+ getSearchesHTML: function () {
+ var html = '<table cellspacing="0">';
+ var showBtn = false;
+ for (var i = 0; i < this.searches.length; i++) {
+ var s = this.searches[i];
+ s.type = String(s.type).toLowerCase();
+ if (s.hidden) continue;
+ var btn = '';
+ if (showBtn == false) {
+ btn = '<button class="btn close-btn" onclick="obj = w2ui[\''+ this.name +'\']; if (obj) { obj.searchClose(); }">X</button';
+ showBtn = true;
+ }
+ if (typeof s.inTag == 'undefined') s.inTag = '';
+ if (typeof s.outTag == 'undefined') s.outTag = '';
+ if (typeof s.type == 'undefined') s.type = 'text';
+ if (['text', 'alphanumeric', 'combo'].indexOf(s.type) != -1) {
+ var operator = '<select id="grid_'+ this.name +'_operator_'+ i +'" onclick="event.stopPropagation();">'+
+ ' <option value="is">'+ w2utils.lang('is') +'</option>'+
+ ' <option value="begins">'+ w2utils.lang('begins') +'</option>'+
+ ' <option value="contains">'+ w2utils.lang('contains') +'</option>'+
+ ' <option value="ends">'+ w2utils.lang('ends') +'</option>'+
+ '</select>';
+ }
+ if (['int', 'float', 'money', 'currency', 'percent', 'date', 'time'].indexOf(s.type) != -1) {
+ var operator = '<select id="grid_'+ this.name +'_operator_'+ i +'" '+
+ ' onchange="w2ui[\''+ this.name + '\'].initOperator(this, '+ i +');" onclick="event.stopPropagation();">'+
+ ' <option value="is">'+ w2utils.lang('is') +'</option>'+
+ (['int'].indexOf(s.type) != -1 ? '<option value="in">'+ w2utils.lang('in') +'</option>' : '') +
+ (['int'].indexOf(s.type) != -1 ? '<option value="not in">'+ w2utils.lang('not in') +'</option>' : '') +
+ '<option value="between">'+ w2utils.lang('between') +'</option>'+
+ '</select>';
+ }
+ if (['select', 'list', 'hex'].indexOf(s.type) != -1) {
+ var operator = '<select id="grid_'+ this.name +'_operator_'+ i +'" onclick="event.stopPropagation();">'+
+ ' <option value="is">'+ w2utils.lang('is') +'</option>'+
+ '</select>';
+ }
+ if (['enum'].indexOf(s.type) != -1) {
+ var operator = '<select id="grid_'+ this.name +'_operator_'+ i +'" onclick="event.stopPropagation();">'+
+ ' <option value="in">'+ w2utils.lang('in') +'</option>'+
+ ' <option value="in">'+ w2utils.lang('not in') +'</option>'+
+ '</select>';
+ }
+ html += '<tr>'+
+ ' <td class="close-btn">'+ btn +'</td>' +
+ ' <td class="caption">'+ s.caption +'</td>' +
+ ' <td class="operator">'+ operator +'</td>'+
+ ' <td class="value">';
+
+ switch (s.type) {
+ case 'text':
+ case 'alphanumeric':
+ case 'hex':
+ case 'list':
+ case 'combo':
+ case 'enum':
+ html += '<input rel="search" type="text" style="width: 300px;" id="grid_'+ this.name +'_field_'+ i +'" name="'+ s.field +'" '+ s.inTag +'>';
+ break;
+
+ case 'int':
+ case 'float':
+ case 'money':
+ case 'currency':
+ case 'percent':
+ case 'date':
+ case 'time':
+ html += '<input rel="search" type="text" size="12" id="grid_'+ this.name +'_field_'+ i +'" name="'+ s.field +'" '+ s.inTag +'>'+
+ '<span id="grid_'+ this.name +'_range_'+ i +'" style="display: none">'+
+ '&nbsp;-&nbsp;&nbsp;<input rel="search" type="text" style="width: 90px" id="grid_'+ this.name +'_field2_'+i+'" name="'+ s.field +'" '+ s.inTag +'>'+
+ '</span>';
+ break;
+
+ case 'select':
+ html += '<select rel="search" id="grid_'+ this.name +'_field_'+ i +'" name="'+ s.field +'" '+ s.inTag +' onclick="event.stopPropagation();"></select>';
+ break;
+
+ }
+ html += s.outTag +
+ ' </td>' +
+ '</tr>';
+ }
+ html += '<tr>'+
+ ' <td colspan="4" class="actions">'+
+ ' <div>'+
+ ' <button class="btn" onclick="obj = w2ui[\''+ this.name +'\']; if (obj) { obj.searchReset(); }">'+ w2utils.lang('Reset') + '</button>'+
+ ' <button class="btn btn-blue" onclick="obj = w2ui[\''+ this.name +'\']; if (obj) { obj.search(); }">'+ w2utils.lang('Search') + '</button>'+
+ ' </div>'+
+ ' </td>'+
+ '</tr></table>';
+ return html;
+ },
+
+ initOperator: function (el, search_ind) {
+ var obj = this;
+ var search = obj.searches[search_ind];
+ var range = $('#grid_'+ obj.name + '_range_'+ search_ind);
+ var fld1 = $('#grid_'+ obj.name +'_field_'+ search_ind);
+ var fld2 = fld1.parent().find('span input');
+ if ($(el).val() == 'in' || $(el).val() == 'not in') { fld1.w2field('clear'); } else { fld1.w2field(search.type); }
+ if ($(el).val() == 'between') { range.show(); fld2.w2field(search.type); } else { range.hide(); }
+ },
+
+ initSearches: function () {
+ var obj = this;
+ // init searches
+ for (var s in this.searches) {
+ var search = this.searches[s];
+ var sdata = this.getSearchData(search.field);
+ search.type = String(search.type).toLowerCase();
+ if (typeof search.options != 'object') search.options = {};
+ // init types
+ switch (search.type) {
+ case 'text':
+ case 'alphanumeric':
+ $('#grid_'+ this.name +'_operator_'+s).val('begins');
+ if (['alphanumeric', 'hex'].indexOf(search.type) != -1) {
+ $('#grid_'+ this.name +'_field_' + s).w2field(search.type, search.options);
+ }
+ break;
+
+ case 'int':
+ case 'float':
+ case 'money':
+ case 'currency':
+ case 'percent':
+ case 'date':
+ case 'time':
+ if (sdata && sdata.type == 'int' && ['in', 'not in'].indexOf(sdata.operator) != -1) break;
+ $('#grid_'+ this.name +'_field_'+s).w2field(search.type, search.options);
+ $('#grid_'+ this.name +'_field2_'+s).w2field(search.type, search.options);
+ setTimeout(function () { // convert to date if it is number
+ $('#grid_'+ obj.name +'_field_'+s).keydown();
+ $('#grid_'+ obj.name +'_field2_'+s).keydown();
+ }, 1);
+ break;
+
+ case 'hex':
+ break;
+
+ case 'list':
+ case 'combo':
+ case 'enum':
+ var options = search.options;
+ if (search.type == 'list') options.selected = {};
+ if (search.type == 'enum') options.selected = [];
+ if (sdata) options.selected = sdata.value;
+ $('#grid_'+ this.name +'_field_'+s).w2field(search.type, options);
+ if (search.type == 'combo') {
+ $('#grid_'+ this.name +'_operator_'+s).val('begins');
+ }
+ break;
+
+ case 'select':
+ // build options
+ var options = '<option value="">--</option>';
+ for (var i in search.options.items) {
+ var si = search.options.items[i];
+ if ($.isPlainObject(search.options.items[i])) {
+ var val = si.id;
+ var txt = si.text;
+ if (typeof val == 'undefined' && typeof si.value != 'undefined') val = si.value;
+ if (typeof txt == 'undefined' && typeof si.caption != 'undefined') txt = si.caption;
+ if (val == null) val = '';
+ options += '<option value="'+ val +'">'+ txt +'</option>';
+ } else {
+ options += '<option value="'+ si +'">'+ si +'</option>';
+ }
+ }
+ $('#grid_'+ this.name +'_field_'+s).html(options);
+ break;
+ }
+ if (sdata != null) {
+ if (sdata.type == 'int' && ['in', 'not in'].indexOf(sdata.operator) != -1) {
+ $('#grid_'+ this.name +'_field_'+ s).w2field('clear').val(sdata.value);
+ }
+ $('#grid_'+ this.name +'_operator_'+ s).val(sdata.operator).trigger('change');
+ if (!$.isArray(sdata.value)) {
+ if (typeof sdata.value != 'udefined') $('#grid_'+ this.name +'_field_'+ s).val(sdata.value).trigger('change');
+ } else {
+ if (['in', 'not in'].indexOf(sdata.operator) != -1) {
+ $('#grid_'+ this.name +'_field_'+ s).val(sdata.value).trigger('change');
+ } else {
+ $('#grid_'+ this.name +'_field_'+ s).val(sdata.value[0]).trigger('change');
+ $('#grid_'+ this.name +'_field2_'+ s).val(sdata.value[1]).trigger('change');
+ }
+ }
+ }
+ }
+ // add on change event
+ $('#w2ui-overlay-searches-'+ this.name +' .w2ui-grid-searches *[rel=search]').on('keypress', function (evnt) {
+ if (evnt.keyCode == 13) {
+ obj.search();
+ $().w2overlay();
+ }
+ });
+ },
+
+ getColumnsHTML: function () {
+ var obj = this;
+ var html = '';
+ if (this.show.columnHeaders) {
+ if (this.columnGroups.length > 0) {
+ html = getColumns(true) + getGroups() + getColumns(false);
+ } else {
+ html = getColumns(true);
+ }
+ }
+ return html;
+
+ function getGroups () {
+ var html = '<tr>';
+ // add empty group at the end
+ if (obj.columnGroups[obj.columnGroups.length-1].caption != '') obj.columnGroups.push({ caption: '' });
+
+ if (obj.show.lineNumbers) {
+ html += '<td class="w2ui-head w2ui-col-number">'+
+ ' <div>&nbsp;</div>'+
+ '</td>';
+ }
+ if (obj.show.selectColumn) {
+ html += '<td class="w2ui-head w2ui-col-select">'+
+ ' <div>&nbsp;</div>'+
+ '</td>';
+ }
+ if (obj.show.expandColumn) {
+ html += '<td class="w2ui-head w2ui-col-expand">'+
+ ' <div>&nbsp;</div>'+
+ '</td>';
+ }
+ var ii = 0;
+ for (var i=0; i<obj.columnGroups.length; i++) {
+ var colg = obj.columnGroups[i];
+ var col = obj.columns[ii];
+ if (typeof colg.span == 'undefined' || colg.span != parseInt(colg.span)) colg.span = 1;
+ if (typeof colg.colspan != 'undefined') colg.span = colg.colspan;
+ if (colg.master === true) {
+ var sortStyle = '';
+ for (var si in obj.sortData) {
+ if (obj.sortData[si].field == col.field) {
+ if (RegExp('asc', 'i').test(obj.sortData[si].direction)) sortStyle = 'w2ui-sort-up';
+ if (RegExp('desc', 'i').test(obj.sortData[si].direction)) sortStyle = 'w2ui-sort-down';
+ }
+ }
+ var resizer = "";
+ if (col.resizable !== false) {
+ resizer = '<div class="w2ui-resizer" name="'+ ii +'"></div>';
+ }
+ html += '<td class="w2ui-head '+ sortStyle +'" col="'+ ii + '" rowspan="2" colspan="'+ (colg.span + (i == obj.columnGroups.length-1 ? 1 : 0) ) +'" '+
+ ' onclick="w2ui[\''+ obj.name +'\'].columnClick(\''+ col.field +'\', event);">'+
+ resizer +
+ ' <div class="w2ui-col-group w2ui-col-header '+ (sortStyle ? 'w2ui-col-sorted' : '') +'">'+
+ ' <div class="'+ sortStyle +'"></div>'+
+ (!col.caption ? '&nbsp;' : col.caption) +
+ ' </div>'+
+ '</td>';
+ } else {
+ html += '<td class="w2ui-head" col="'+ ii + '" '+
+ ' colspan="'+ (colg.span + (i == obj.columnGroups.length-1 ? 1 : 0) ) +'">'+
+ ' <div class="w2ui-col-group">'+
+ (!colg.caption ? '&nbsp;' : colg.caption) +
+ ' </div>'+
+ '</td>';
+ }
+ ii += colg.span;
+ }
+ html += '</tr>';
+ return html;
+ }
+
+ function getColumns (master) {
+ var html = '<tr>',
+ reorderCols = (obj.reorderColumns && (!obj.columnGroups || !obj.columnGroups.length)) ? ' w2ui-reorder-cols-head ' : '';
+ if (obj.show.lineNumbers) {
+ html += '<td class="w2ui-head w2ui-col-number" onclick="w2ui[\''+ obj.name +'\'].columnClick(\'line-number\', event);">'+
+ ' <div>#</div>'+
+ '</td>';
+ }
+ if (obj.show.selectColumn) {
+ html += '<td class="w2ui-head w2ui-col-select" '+
+ ' onclick="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;">'+
+ ' <div>'+
+ ' <input type="checkbox" id="grid_'+ obj.name +'_check_all" tabIndex="-1"'+
+ ' style="' + (obj.multiSelect == false ? 'display: none;' : '') + '"'+
+ ' onclick="if (this.checked) w2ui[\''+ obj.name +'\'].selectAll(); '+
+ ' else w2ui[\''+ obj.name +'\'].selectNone(); '+
+ ' if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;">'+
+ ' </div>'+
+ '</td>';
+ }
+ if (obj.show.expandColumn) {
+ html += '<td class="w2ui-head w2ui-col-expand">'+
+ ' <div>&nbsp;</div>'+
+ '</td>';
+ }
+ var ii = 0;
+ var id = 0;
+ for (var i=0; i<obj.columns.length; i++) {
+ var col = obj.columns[i];
+ var colg = {};
+ if (i == id) {
+ id = id + (typeof obj.columnGroups[ii] != 'undefined' ? parseInt(obj.columnGroups[ii].span) : 0);
+ ii++;
+ }
+ if (typeof obj.columnGroups[ii-1] != 'undefined') var colg = obj.columnGroups[ii-1];
+ if (col.hidden) continue;
+ var sortStyle = '';
+ for (var si in obj.sortData) {
+ if (obj.sortData[si].field == col.field) {
+ if (RegExp('asc', 'i').test(obj.sortData[si].direction)) sortStyle = 'w2ui-sort-up';
+ if (RegExp('desc', 'i').test(obj.sortData[si].direction)) sortStyle = 'w2ui-sort-down';
+ }
+ }
+ if (colg['master'] !== true || master) { // grouping of columns
+ var resizer = "";
+ if (col.resizable !== false) {
+ resizer = '<div class="w2ui-resizer" name="'+ i +'"></div>';
+ }
+ html += '<td col="'+ i +'" class="w2ui-head '+ sortStyle + reorderCols + '" ' +
+ ' onclick="w2ui[\''+ obj.name +'\'].columnClick(\''+ col.field +'\', event);">'+
+ resizer +
+ ' <div class="w2ui-col-header '+ (sortStyle ? 'w2ui-col-sorted' : '') +'">'+
+ ' <div class="'+ sortStyle +'"></div>'+
+ (!col.caption ? '&nbsp;' : col.caption) +
+ ' </div>'+
+ '</td>';
+ }
+ }
+ html += '<td class="w2ui-head w2ui-head-last"><div>&nbsp;</div></td>';
+ html += '</tr>';
+ return html;
+ }
+ },
+
+ getRecordsHTML: function () {
+ var buffered = this.records.length;
+ if (this.searchData.length != 0 && !this.url) buffered = this.last.searchIds.length;
+ // larget number works better with chrome, smaller with FF.
+ if (buffered > 300) this.show_extra = 30; else this.show_extra = 300;
+ var records = $('#grid_'+ this.name +'_records');
+ var limit = Math.floor(records.height() / this.recordHeight) + this.show_extra + 1;
+ if (!this.fixedBody || limit > buffered) limit = buffered;
+ // always need first record for resizing purposes
+ var html = '<table>' + this.getRecordHTML(-1, 0);
+ // first empty row with height
+ html += '<tr id="grid_'+ this.name + '_rec_top" line="top" style="height: '+ 0 +'px">'+
+ ' <td colspan="200"></td>'+
+ '</tr>';
+ for (var i = 0; i < limit; i++) {
+ html += this.getRecordHTML(i, i+1);
+ }
+ html += '<tr id="grid_'+ this.name + '_rec_bottom" line="bottom" style="height: '+ ((buffered - limit) * this.recordHeight) +'px">'+
+ ' <td colspan="200"></td>'+
+ '</tr>'+
+ '<tr id="grid_'+ this.name +'_rec_more" style="display: none">'+
+ ' <td colspan="200" class="w2ui-load-more"></td>'+
+ '</tr>'+
+ '</table>';
+ this.last.range_start = 0;
+ this.last.range_end = limit;
+ return html;
+ },
+
+ getSummaryHTML: function () {
+ if (this.summary.length == 0) return;
+ var html = '<table>';
+ for (var i = 0; i < this.summary.length; i++) {
+ html += this.getRecordHTML(i, i+1, true);
+ }
+ html += '</table>';
+ return html;
+ },
+
+ scroll: function (event) {
+ var time = (new Date()).getTime();
+ var obj = this;
+ var records = $('#grid_'+ this.name +'_records');
+ var buffered = this.records.length;
+ if (this.searchData.length != 0 && !this.url) buffered = this.last.searchIds.length;
+ if (buffered == 0 || records.length == 0 || records.height() == 0) return;
+ if (buffered > 300) this.show_extra = 30; else this.show_extra = 300;
+ // need this to enable scrolling when this.limit < then a screen can fit
+ if (records.height() < buffered * this.recordHeight && records.css('overflow-y') == 'hidden') {
+ if (this.total > 0) this.refresh();
+ return;
+ }
+ // update footer
+ var t1 = Math.round(records[0].scrollTop / this.recordHeight + 1);
+ var t2 = t1 + (Math.round(records.height() / this.recordHeight) - 1);
+ if (t1 > buffered) t1 = buffered;
+ if (t2 > buffered) t2 = buffered;
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ $('#grid_'+ this.name + '_footer .w2ui-footer-right').html(w2utils.formatNumber(this.offset + t1) + '-' + w2utils.formatNumber(this.offset + t2) + ' ' + w2utils.lang('of') + ' ' + w2utils.formatNumber(this.total) +
+ (url ? ' ('+ w2utils.lang('buffered') + ' '+ w2utils.formatNumber(buffered) + (this.offset > 0 ? ', skip ' + w2utils.formatNumber(this.offset) : '') + ')' : '')
+ );
+ // only for local data source, else no extra records loaded
+ if (!url && (!this.fixedBody || this.total <= 300)) return;
+ // regular processing
+ var start = Math.floor(records[0].scrollTop / this.recordHeight) - this.show_extra;
+ var end = start + Math.floor(records.height() / this.recordHeight) + this.show_extra * 2 + 1;
+ // var div = start - this.last.range_start;
+ if (start < 1) start = 1;
+ if (end > this.total) end = this.total;
+ var tr1 = records.find('#grid_'+ this.name +'_rec_top');
+ var tr2 = records.find('#grid_'+ this.name +'_rec_bottom');
+ // if row is expanded
+ if (String(tr1.next().prop('id')).indexOf('_expanded_row') != -1) tr1.next().remove();
+ if (this.total > end && String(tr2.prev().prop('id')).indexOf('_expanded_row') != -1) tr2.prev().remove();
+ var first = parseInt(tr1.next().attr('line'));
+ var last = parseInt(tr2.prev().attr('line'));
+ //$('#log').html('buffer: '+ this.buffered +' start-end: ' + start + '-'+ end + ' ===> first-last: ' + first + '-' + last);
+ if (first < start || first == 1 || this.last.pull_refresh) { // scroll down
+ // console.log('end', end, 'last', last, 'show_extre', this.show_extra, this.last.pull_refresh);
+ if (end <= last + this.show_extra - 2 && end != this.total) return;
+ this.last.pull_refresh = false;
+ // remove from top
+ while (true) {
+ var tmp = records.find('#grid_'+ this.name +'_rec_top').next();
+ if (tmp.attr('line') == 'bottom') break;
+ if (parseInt(tmp.attr('line')) < start) tmp.remove(); else break;
+ }
+ // add at bottom
+ var tmp = records.find('#grid_'+ this.name +'_rec_bottom').prev();
+ var rec_start = tmp.attr('line');
+ if (rec_start == 'top') rec_start = start;
+ for (var i = parseInt(rec_start) + 1; i <= end; i++) {
+ if (!this.records[i-1]) continue;
+ if (this.records[i-1].expanded === true) this.records[i-1].expanded = false;
+ tr2.before(this.getRecordHTML(i-1, i));
+ }
+ markSearch();
+ setTimeout(function() { obj.refreshRanges(); }, 0);
+ } else { // scroll up
+ if (start >= first - this.show_extra + 2 && start > 1) return;
+ // remove from bottom
+ while (true) {
+ var tmp = records.find('#grid_'+ this.name +'_rec_bottom').prev();
+ if (tmp.attr('line') == 'top') break;
+ if (parseInt(tmp.attr('line')) > end) tmp.remove(); else break;
+ }
+ // add at top
+ var tmp = records.find('#grid_'+ this.name +'_rec_top').next();
+ var rec_start = tmp.attr('line');
+ if (rec_start == 'bottom') rec_start = end;
+ for (var i = parseInt(rec_start) - 1; i >= start; i--) {
+ if (!this.records[i-1]) continue;
+ if (this.records[i-1].expanded === true) this.records[i-1].expanded = false;
+ tr1.after(this.getRecordHTML(i-1, i));
+ }
+ markSearch();
+ setTimeout(function() { obj.refreshRanges(); }, 0);
+ }
+ // first/last row size
+ var h1 = (start - 1) * obj.recordHeight;
+ var h2 = (buffered - end) * obj.recordHeight;
+ if (h2 < 0) h2 = 0;
+ tr1.css('height', h1 + 'px');
+ tr2.css('height', h2 + 'px');
+ obj.last.range_start = start;
+ obj.last.range_end = end;
+ // load more if needed
+ var s = Math.floor(records[0].scrollTop / this.recordHeight);
+ var e = s + Math.floor(records.height() / this.recordHeight);
+ if (e + 10 > buffered && this.last.pull_more !== true && buffered < this.total - this.offset) {
+ if (this.autoLoad === true) {
+ this.last.pull_more = true;
+ this.last.xhr_offset += this.limit;
+ this.request('get-records');
+ } else {
+ var more = $('#grid_'+ this.name +'_rec_more');
+ if (more.css('display') == 'none') {
+ more.show()
+ .on('click', function () {
+ obj.last.pull_more = true;
+ obj.last.xhr_offset += obj.limit;
+ obj.request('get-records');
+ // show spinner the last
+ $(this).find('td').html('<div><div style="width: 20px; height: 20px;" class="w2ui-spinner"></div></div>');
+ });
+ }
+ if (more.find('td').text().indexOf('Load') == -1) {
+ more.find('td').html('<div>'+ w2utils.lang('Load') + ' ' + obj.limit + ' ' + w2utils.lang('More') + '...</div>');
+ }
+ }
+ }
+ // check for grid end
+ if (buffered >= this.total - this.offset) $('#grid_'+ this.name +'_rec_more').hide();
+ return;
+
+ function markSearch() {
+ // mark search
+ if(obj.markSearch === false) return;
+ clearTimeout(obj.last.marker_timer);
+ obj.last.marker_timer = setTimeout(function () {
+ // mark all search strings
+ var str = [];
+ for (var s in obj.searchData) {
+ var tmp = obj.searchData[s];
+ if ($.inArray(tmp.value, str) == -1) str.push(tmp.value);
+ }
+ if (str.length > 0) $(obj.box).find('.w2ui-grid-data > div').w2marker(str);
+ }, 50);
+ }
+ },
+
+ getRecordHTML: function (ind, lineNum, summary) {
+ var rec_html = '';
+ var sel = this.last.selection;
+ var record;
+ // first record needs for resize purposes
+ if (ind == -1) {
+ rec_html += '<tr line="0">';
+ if (this.show.lineNumbers) rec_html += '<td class="w2ui-col-number" style="height: 0px;"></td>';
+ if (this.show.selectColumn) rec_html += '<td class="w2ui-col-select" style="height: 0px;"></td>';
+ if (this.show.expandColumn) rec_html += '<td class="w2ui-col-expand" style="height: 0px;"></td>';
+ for (var i in this.columns) {
+ if (this.columns[i].hidden) continue;
+ rec_html += '<td class="w2ui-grid-data" col="'+ i +'" style="height: 0px;"></td>';
+ }
+ rec_html += '<td class="w2ui-grid-data-last" style="height: 0px;"></td>';
+ rec_html += '</tr>';
+ return rec_html;
+ }
+ // regular record
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (summary !== true) {
+ if (this.searchData.length > 0 && !url) {
+ if (ind >= this.last.searchIds.length) return '';
+ ind = this.last.searchIds[ind];
+ record = this.records[ind];
+ } else {
+ if (ind >= this.records.length) return '';
+ record = this.records[ind];
+ }
+ } else {
+ if (ind >= this.summary.length) return '';
+ record = this.summary[ind];
+ }
+ if (!record) return '';
+ var id = w2utils.escapeId(record.recid);
+ var isRowSelected = false;
+ if (sel.indexes.indexOf(ind) != -1) isRowSelected = true;
+ // render TR
+ rec_html += '<tr id="grid_'+ this.name +'_rec_'+ record.recid +'" recid="'+ record.recid +'" line="'+ lineNum +'" '+
+ ' class="'+ (lineNum % 2 == 0 ? 'w2ui-even' : 'w2ui-odd') + (isRowSelected && this.selectType == 'row' ? ' w2ui-selected' : '') + (record.expanded === true ? ' w2ui-expanded' : '') + '" ' +
+ (summary !== true ?
+ (w2utils.isIOS ?
+ ' onclick = "w2ui[\''+ this.name +'\'].dblClick(\''+ record.recid +'\', event);"'
+ :
+ ' onclick = "w2ui[\''+ this.name +'\'].click(\''+ record.recid +'\', event);"'+
+ ' oncontextmenu = "w2ui[\''+ this.name +'\'].contextMenu(\''+ record.recid +'\', event);"'
+ )
+ : ''
+ ) +
+ ' style="height: '+ this.recordHeight +'px; '+ (!isRowSelected && typeof record['style'] == 'string' ? record['style'] : '') +'" '+
+ ( typeof record['style'] == 'string' ? 'custom_style="'+ record['style'] +'"' : '') +
+ '>';
+ if (this.show.lineNumbers) {
+ rec_html += '<td id="grid_'+ this.name +'_cell_'+ ind +'_number' + (summary ? '_s' : '') + '" class="w2ui-col-number">'+
+ (summary !== true ? '<div>'+ lineNum +'</div>' : '') +
+ '</td>';
+ }
+ if (this.show.selectColumn) {
+ rec_html +=
+ '<td id="grid_'+ this.name +'_cell_'+ ind +'_select' + (summary ? '_s' : '') + '" class="w2ui-grid-data w2ui-col-select" '+
+ ' onclick="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;">'+
+ (summary !== true ?
+ ' <div>'+
+ ' <input class="w2ui-grid-select-check" type="checkbox" tabIndex="-1"'+
+ ' '+ (isRowSelected ? 'checked="checked"' : '') +
+ ' onclick="var obj = w2ui[\''+ this.name +'\']; '+
+ ' if (!obj.multiSelect) { obj.selectNone(); }'+
+ ' if (this.checked) obj.select(\''+ record.recid + '\'); else obj.unselect(\''+ record.recid + '\'); '+
+ ' if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;">'+
+ ' </div>'
+ :
+ '' ) +
+ '</td>';
+ }
+ if (this.show.expandColumn) {
+ var tmp_img = '';
+ if (record.expanded === true) tmp_img = '-'; else tmp_img = '+';
+ if (record.expanded == 'none') tmp_img = '';
+ if (record.expanded == 'spinner') tmp_img = '<div class="w2ui-spinner" style="width: 16px; margin: -2px 2px;"></div>';
+ rec_html +=
+ '<td id="grid_'+ this.name +'_cell_'+ ind +'_expand' + (summary ? '_s' : '') + '" class="w2ui-grid-data w2ui-col-expand">'+
+ (summary !== true ?
+ ' <div ondblclick="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;" '+
+ ' onclick="w2ui[\''+ this.name +'\'].toggle(\''+ record.recid +'\', event); '+
+ ' if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;">'+
+ ' '+ tmp_img +' </div>'
+ :
+ '' ) +
+ '</td>';
+ }
+ var col_ind = 0;
+ while (true) {
+ var col = this.columns[col_ind];
+ if (col.hidden) { col_ind++; if (typeof this.columns[col_ind] == 'undefined') break; else continue; }
+ var isChanged = !summary && record.changes && typeof record.changes[col.field] != 'undefined';
+ var rec_cell = this.getCellHTML(ind, col_ind, summary);
+ var addStyle = '';
+ if (typeof col.render == 'string') {
+ var tmp = col.render.toLowerCase().split(':');
+ if (['number', 'int', 'float', 'money', 'currency', 'percent'].indexOf(tmp[0]) != -1) addStyle += 'text-align: right;';
+ }
+ if (typeof record.style == 'object' && typeof record.style[col_ind] == 'string') {
+ addStyle += record.style[col_ind] + ';';
+ }
+ var isCellSelected = false;
+ if (isRowSelected && $.inArray(col_ind, sel.columns[ind]) != -1) isCellSelected = true;
+ rec_html += '<td class="w2ui-grid-data'+ (isCellSelected ? ' w2ui-selected' : '') + (isChanged ? ' w2ui-changed' : '') +'" col="'+ col_ind +'" '+
+ ' style="'+ addStyle + (typeof col.style != 'undefined' ? col.style : '') +'" '+
+ (typeof col.attr != 'undefined' ? col.attr : '') +'>'+
+ rec_cell +
+ '</td>';
+ col_ind++;
+ if (typeof this.columns[col_ind] == 'undefined') break;
+ }
+ rec_html += '<td class="w2ui-grid-data-last"></td>';
+ rec_html += '</tr>';
+ return rec_html;
+ },
+
+ getCellHTML: function (ind, col_ind, summary) {
+ var col = this.columns[col_ind];
+ var record = (summary !== true ? this.records[ind] : this.summary[ind]);
+ var data = this.getCellValue(ind, col_ind, summary);
+ var edit = col.editable;
+ // various renderers
+ if (typeof col.render != 'undefined') {
+ if (typeof col.render == 'function') {
+ data = $.trim(col.render.call(this, record, ind, col_ind));
+ if (data.length < 4 || data.substr(0, 4).toLowerCase() != '<div') data = '<div>' + data + '</div>';
+ }
+ if (typeof col.render == 'object') data = '<div>' + (col.render[data] || '') + '</div>';
+ if (typeof col.render == 'string') {
+ var tmp = col.render.toLowerCase().split(':');
+ var prefix = '';
+ var suffix = '';
+ if (['number', 'int', 'float', 'money', 'currency', 'percent'].indexOf(tmp[0]) != -1) {
+ if (typeof tmp[1] == 'undefined' || !w2utils.isInt(tmp[1])) tmp[1] = 0;
+ if (tmp[1] > 20) tmp[1] = 20;
+ if (tmp[1] < 0) tmp[1] = 0;
+ if (['money', 'currency'].indexOf(tmp[0]) != -1) { tmp[1] = w2utils.settings.currencyPrecision; prefix = w2utils.settings.currencyPrefix; suffix = w2utils.settings.currencySuffix }
+ if (tmp[0] == 'percent') { suffix = '%'; if (tmp[1] !== '0') tmp[1] = 1; }
+ if (tmp[0] == 'int') { tmp[1] = 0; }
+ // format
+ data = '<div>' + (data !== '' ? prefix + w2utils.formatNumber(Number(data).toFixed(tmp[1])) + suffix : '') + '</div>';
+ }
+ if (tmp[0] == 'time') {
+ if (typeof tmp[1] == 'undefined' || tmp[1] == '') tmp[1] = w2utils.settings.time_format;
+ data = '<div>' + prefix + w2utils.formatTime(data, tmp[1] == 'h12' ? 'hh:mi pm': 'h24:min') + suffix + '</div>';
+ }
+ if (tmp[0] == 'date') {
+ if (typeof tmp[1] == 'undefined' || tmp[1] == '') tmp[1] = w2utils.settings.date_display;
+ data = '<div>' + prefix + w2utils.formatDate(data, tmp[1]) + suffix + '</div>';
+ }
+ if (tmp[0] == 'age') {
+ data = '<div>' + prefix + w2utils.age(data) + suffix + '</div>';
+ }
+ if (tmp[0] == 'toggle') {
+ data = '<div>' + prefix + (data ? 'Yes' : '') + suffix + '</div>';
+ }
+ }
+ } else {
+ // if editable checkbox
+ var addStyle = '';
+ if (edit && ['checkbox', 'check'].indexOf(edit.type) != -1) {
+ var changeInd = summary ? -(ind + 1) : ind;
+ addStyle = 'text-align: center';
+ data = '<input type="checkbox" '+ (data ? 'checked' : '') +' onclick="' +
+ ' var obj = w2ui[\''+ this.name + '\']; '+
+ ' obj.editChange.call(obj, this, '+ changeInd +', '+ col_ind +', event); ' +
+ '">';
+ }
+ if (!this.show.recordTitles) {
+ var data = '<div style="'+ addStyle +'">'+ data +'</div>';
+ } else {
+ // title overwrite
+ var title = String(data).replace(/"/g, "''");
+ if (typeof col.title != 'undefined') {
+ if (typeof col.title == 'function') title = col.title.call(this, record, ind, col_ind);
+ if (typeof col.title == 'string') title = col.title;
+ }
+ var data = '<div title="'+ w2utils.stripTags(title) +'" style="'+ addStyle +'">'+ data +'</div>';
+ }
+ }
+ if (data == null || typeof data == 'undefined') data = '';
+ return data;
+ },
+
+ getCellValue: function (ind, col_ind, summary) {
+ var col = this.columns[col_ind];
+ var record = (summary !== true ? this.records[ind] : this.summary[ind]);
+ var data = this.parseField(record, col.field);
+ if (record.changes && typeof record.changes[col.field] != 'undefined') data = record.changes[col.field];
+ if (data == null || typeof data == 'undefined') data = '';
+ return data;
+ },
+
+ getFooterHTML: function () {
+ return '<div>'+
+ ' <div class="w2ui-footer-left"></div>'+
+ ' <div class="w2ui-footer-right"></div>'+
+ ' <div class="w2ui-footer-center"></div>'+
+ '</div>';
+ },
+
+ status: function (msg) {
+ if (typeof msg != 'undefined') {
+ $('#grid_'+ this.name +'_footer').find('.w2ui-footer-left').html(msg);
+ } else {
+ // show number of selected
+ var msgLeft = '';
+ var sel = this.getSelection();
+ if (sel.length > 0) {
+ msgLeft = String(sel.length).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") + ' ' + w2utils.lang('selected');
+ var tmp = sel[0];
+ if (typeof tmp == 'object') tmp = tmp.recid + ', '+ w2utils.lang('Column') +': '+ tmp.column;
+ if (sel.length == 1) msgLeft = w2utils.lang('Record ID') + ': '+ tmp + ' ';
+ }
+ $('#grid_'+ this.name +'_footer .w2ui-footer-left').html(msgLeft);
+ // toolbar
+ if (sel.length == 1) this.toolbar.enable('w2ui-edit'); else this.toolbar.disable('w2ui-edit');
+ if (sel.length >= 1) this.toolbar.enable('w2ui-delete'); else this.toolbar.disable('w2ui-delete');
+ }
+ },
+
+ lock: function (msg, showSpinner) {
+ var box = $(this.box).find('> div:first-child');
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift(box);
+ setTimeout(function () { w2utils.lock.apply(window, args); }, 10);
+ },
+
+ unlock: function () {
+ var box = this.box;
+ setTimeout(function () { w2utils.unlock(box); }, 25); // needed timer so if server fast, it will not flash
+ },
+
+ stateSave: function (returnOnly) {
+ if (!localStorage) return null;
+ var state = {
+ columns : [],
+ show : $.extend({}, this.show),
+ last : {
+ search : this.last.search,
+ multi : this.last.multi,
+ logic : this.last.logic,
+ caption : this.last.caption,
+ field : this.last.field,
+ scrollTop : this.last.scrollTop,
+ scrollLeft : this.last.scrollLeft
+ },
+ sortData : [],
+ searchData : []
+ };
+ for (var i in this.columns) {
+ var col = this.columns[i];
+ state.columns.push({
+ field : col.field,
+ hidden : col.hidden,
+ size : col.size,
+ sizeCalculated : col.sizeCalculated,
+ sizeOriginal : col.sizeOriginal,
+ sizeType : col.sizeType
+ });
+ }
+ for (var i in this.sortData) state.sortData.push($.extend({}, this.sortData[i]));
+ for (var i in this.searchData) state.searchData.push($.extend({}, this.searchData[i]));
+ // save into local storage
+ if (returnOnly !== true) {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'stateSave', target: this.name, state: state });
+ if (eventData.isCancelled === true) { if (typeof callBack == 'function') callBack({ status: 'error', message: 'Request aborted.' }); return; }
+ try {
+ var savedState = $.parseJSON(localStorage.w2ui || '{}');
+ if (!savedState) savedState = {};
+ if (!savedState.states) savedState.states = {};
+ savedState.states[this.name] = state;
+ localStorage.w2ui = JSON.stringify(savedState);
+ } catch (e) {
+ delete localStorage.w2ui;
+ return null;
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ return state;
+ },
+
+ stateRestore: function (newState) {
+ var obj = this;
+ if (!newState) {
+ // read it from local storage
+ try {
+ if (!localStorage) return false;
+ var tmp = $.parseJSON(localStorage.w2ui || '{}');
+ if (!tmp) tmp = {};
+ if (!tmp.states) tmp.states = {};
+ newState = tmp.states[this.name];
+ } catch (e) {
+ delete localStorage.w2ui;
+ return null;
+ }
+ }
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'stateRestore', target: this.name, state: newState });
+ if (eventData.isCancelled === true) { if (typeof callBack == 'function') callBack({ status: 'error', message: 'Request aborted.' }); return; }
+ // default behavior
+ if ($.isPlainObject(newState)) {
+ $.extend(this.show, newState.show);
+ $.extend(this.last, newState.last);
+ var sTop = this.last.scrollTop;
+ var sLeft = this.last.scrollLeft;
+ for (var c in newState.columns) {
+ var tmp = newState.columns[c];
+ var col = this.getColumn(tmp.field);
+ if (col) $.extend(col, tmp);
+ }
+ this.sortData.splice(0, this.sortData.length);
+ for (var c in newState.sortData) this.sortData.push(newState.sortData[c]);
+ this.searchData.splice(0, this.searchData.length);
+ for (var c in newState.searchData) this.searchData.push(newState.searchData[c]);
+ // apply sort and search
+ setTimeout(function () {
+ // needs timeout as records need to be populated
+ if (obj.sortData.length > 0) obj.localSort();
+ if (obj.searchData.length > 0) obj.localSearch();
+ obj.last.scrollTop = sTop;
+ obj.last.scrollLeft = sLeft;
+ obj.refresh();
+ }, 1);
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return true;
+ },
+
+ stateReset: function () {
+ this.stateRestore(this.last.state);
+ // remove from local storage
+ if (localStorage) {
+ try {
+ var tmp = $.parseJSON(localStorage.w2ui || '{}');
+ if (tmp.states && tmp.states[this.name]) {
+ delete tmp.states[this.name];
+ }
+ localStorage.w2ui = JSON.stringify(tmp);
+ } catch (e) {
+ delete localStorage.w2ui;
+ return null;
+ }
+ }
+ },
+
+ parseField: function (obj, field) {
+ var val = '';
+ try { // need this to make sure no error in fields
+ val = obj;
+ var tmp = String(field).split('.');
+ for (var i in tmp) {
+ val = val[tmp[i]];
+ }
+ } catch (event) {
+ val = '';
+ }
+ return val;
+ },
+
+ prepareData: function () {
+ // loops thru records and prepares date and time objects
+ for (var r in this.records) {
+ var rec = this.records[r];
+ for (var c in this.columns) {
+ var column = this.columns[c];
+ if (rec[column.field] == null || typeof column.render != 'string') continue;
+ // number
+ if (['number', 'int', 'float', 'money', 'currency', 'percent'].indexOf(column.render.split(':')[0]) != -1) {
+ if (typeof rec[column.field] != 'number') rec[column.field] = parseFloat(rec[column.field]);
+ }
+ // date
+ if (['date', 'age'].indexOf(column.render.split(':')[0]) != -1) {
+ if (!rec[column.field + '_']) {
+ var dt = rec[column.field];
+ if (w2utils.isInt(dt)) dt = parseInt(dt);
+ rec[column.field + '_'] = new Date(dt);
+ }
+ }
+ // time
+ if (['time'].indexOf(column.render) != -1) {
+ if (w2utils.isTime(rec[column.field])) { // if string
+ var tmp = w2utils.isTime(rec[column.field], true);
+ var dt = new Date();
+ dt.setHours(tmp.hours, tmp.minutes, (tmp.seconds ? tmp.seconds : 0), 0); // sets hours, min, sec, mills
+ if (!rec[column.field + '_']) rec[column.field + '_'] = dt;
+ } else { // if date object
+ var tmp = rec[column.field];
+ if (w2utils.isInt(tmp)) tmp = parseInt(tmp);
+ var tmp = (tmp != null ? new Date(tmp) : new Date());
+ var dt = new Date();
+ dt.setHours(tmp.getHours(), tmp.getMinutes(), tmp.getSeconds(), 0); // sets hours, min, sec, mills
+ if (!rec[column.field + '_']) rec[column.field + '_'] = dt;
+ }
+ }
+ }
+ }
+ },
+
+ nextCell: function (col_ind, editable) {
+ var check = col_ind + 1;
+ if (this.columns.length == check) return null;
+ if (editable === true) {
+ var edit = this.columns[check].editable;
+ if (this.columns[check].hidden || typeof edit == 'undefined'
+ || (edit && ['checkbox', 'check'].indexOf(edit.type) != -1)) return this.nextCell(check, editable);
+ }
+ return check;
+ },
+
+ prevCell: function (col_ind, editable) {
+ var check = col_ind - 1;
+ if (check < 0) return null;
+ if (editable === true) {
+ var edit = this.columns[check].editable;
+ if (this.columns[check].hidden || typeof edit == 'undefined'
+ || (edit && ['checkbox', 'check'].indexOf(edit.type) != -1)) return this.prevCell(check, editable);
+ }
+ return check;
+ },
+
+ nextRow: function (ind) {
+ if ((ind + 1 < this.records.length && this.last.searchIds.length == 0) // if there are more records
+ || (this.last.searchIds.length > 0 && ind < this.last.searchIds[this.last.searchIds.length-1])) {
+ ind++;
+ if (this.last.searchIds.length > 0) {
+ while (true) {
+ if ($.inArray(ind, this.last.searchIds) != -1 || ind > this.records.length) break;
+ ind++;
+ }
+ }
+ return ind;
+ } else {
+ return null;
+ }
+ },
+
+ prevRow: function (ind) {
+ if ((ind > 0 && this.last.searchIds.length == 0) // if there are more records
+ || (this.last.searchIds.length > 0 && ind > this.last.searchIds[0])) {
+ ind--;
+ if (this.last.searchIds.length > 0) {
+ while (true) {
+ if ($.inArray(ind, this.last.searchIds) != -1 || ind < 0) break;
+ ind--;
+ }
+ }
+ return ind;
+ } else {
+ return null;
+ }
+ }
+ };
+
+ $.extend(w2grid.prototype, w2utils.event);
+ w2obj.grid = w2grid;
+})();
+
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2layout - layout widget
+* - $().w2layout - jQuery wrapper
+* - Dependencies: jQuery, w2utils, w2toolbar, w2tabs
+*
+* == NICE TO HAVE ==
+* - onResize for the panel
+* - add more panel title positions (left=rotated, right=rotated, bottom)
+* - bug: resizer is visible (and onHover) when panel is hidden.
+* - bug: when you assign content before previous transition completed.
+*
+************************************************************************/
+
+(function () {
+ var w2layout = function (options) {
+ this.box = null; // DOM Element that holds the element
+ this.name = null; // unique name for w2ui
+ this.panels = [];
+ this.tmp = {};
+
+ this.padding = 1; // panel padding
+ this.resizer = 4; // resizer width or height
+ this.style = '';
+
+ this.onShow = null;
+ this.onHide = null;
+ this.onResizing = null;
+ this.onResizerClick = null;
+ this.onRender = null;
+ this.onRefresh = null;
+ this.onResize = null;
+ this.onDestroy = null;
+
+ $.extend(true, this, w2obj.layout, options);
+ };
+
+ /* @const */ var w2layout_panels = ['top', 'left', 'main', 'preview', 'right', 'bottom'];
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2layout = function(method) {
+ if (typeof method === 'object' || !method ) {
+ // check name parameter
+ if (!w2utils.checkName(method, 'w2layout')) return;
+ var panels = method.panels || [];
+ var object = new w2layout(method);
+ $.extend(object, { handlers: [], panels: [] });
+ // add defined panels
+ for (var p = 0, len = panels.length; p < len; p++) {
+ object.panels[p] = $.extend(true, {}, w2layout.prototype.panel, panels[p]);
+ if ($.isPlainObject(object.panels[p].tabs) || $.isArray(object.panels[p].tabs)) initTabs(object, panels[p].type);
+ if ($.isPlainObject(object.panels[p].toolbar) || $.isArray(object.panels[p].toolbar)) initToolbar(object, panels[p].type);
+ }
+ // add all other panels
+ for (var p1 in w2layout_panels) {
+ p1 = w2layout_panels[p1];
+ if (object.get(p1) !== null) continue;
+ object.panels.push($.extend(true, {}, w2layout.prototype.panel, { type: p1, hidden: (p1 !== 'main'), size: 50 }));
+ }
+ if ($(this).length > 0) {
+ object.render($(this)[0]);
+ }
+ w2ui[object.name] = object;
+ return object;
+
+ } else if (w2ui[$(this).attr('name')]) {
+ var obj = w2ui[$(this).attr('name')];
+ obj[method].apply(obj, Array.prototype.slice.call(arguments, 1));
+ return this;
+ } else {
+ console.log('ERROR: Method ' + method + ' does not exist on jQuery.w2layout' );
+ }
+
+ function initTabs(object, panel, tabs) {
+ var pan = object.get(panel);
+ if (pan !== null && typeof tabs == 'undefined') tabs = pan.tabs;
+ if (pan === null || tabs === null) return false;
+ // instanciate tabs
+ if ($.isArray(tabs)) tabs = { tabs: tabs };
+ $().w2destroy(object.name + '_' + panel + '_tabs'); // destroy if existed
+ pan.tabs = $().w2tabs($.extend({}, tabs, { owner: object, name: object.name + '_' + panel + '_tabs' }));
+ pan.show.tabs = true;
+ return true;
+ }
+
+ function initToolbar(object, panel, toolbar) {
+ var pan = object.get(panel);
+ if (pan !== null && typeof toolbar == 'undefined') toolbar = pan.toolbar;
+ if (pan === null || toolbar === null) return false;
+ // instanciate toolbar
+ if ($.isArray(toolbar)) toolbar = { items: toolbar };
+ $().w2destroy(object.name + '_' + panel + '_toolbar'); // destroy if existed
+ pan.toolbar = $().w2toolbar($.extend({}, toolbar, { owner: object, name: object.name + '_' + panel + '_toolbar' }));
+ pan.show.toolbar = true;
+ return true;
+ }
+ };
+
+ // ====================================================
+ // -- Implementation of core functionality
+
+ w2layout.prototype = {
+ // default setting for a panel
+ panel: {
+ type : null, // left, right, top, bottom
+ title : '',
+ size : 100, // width or height depending on panel name
+ minSize : 20,
+ maxSize : false,
+ hidden : false,
+ resizable : false,
+ overflow : 'auto',
+ style : '',
+ content : '', // can be String or Object with .render(box) method
+ tabs : null,
+ toolbar : null,
+ width : null, // read only
+ height : null, // read only
+ show : {
+ toolbar : false,
+ tabs : false
+ },
+ onRefresh : null,
+ onShow : null,
+ onHide : null
+ },
+
+ // alias for content
+ html: function (panel, data, transition) {
+ return this.content(panel, data, transition);
+ },
+
+ content: function (panel, data, transition) {
+ var obj = this;
+ var p = this.get(panel);
+ // if it is CSS panel
+ if (panel == 'css') {
+ $('#layout_'+ obj.name +'_panel_css').html('<style>'+ data +'</style>');
+ return true;
+ }
+ if (p === null) return false;
+ if (typeof data == 'undefined' || data === null) {
+ return p.content;
+ } else {
+ if (data instanceof jQuery) {
+ console.log('ERROR: You can not pass jQuery object to w2layout.content() method');
+ return false;
+ }
+ var pname = '#layout_'+ this.name + '_panel_'+ p.type;
+ var current = $(pname + '> .w2ui-panel-content');
+ var panelTop = 0;
+ if (current.length > 0) {
+ $(pname).scrollTop(0);
+ panelTop = $(current).position().top;
+ }
+ if (p.content === '') {
+ p.content = data;
+ this.refresh(panel);
+ } else {
+ p.content = data;
+ if (!p.hidden) {
+ if (transition !== null && transition !== '' && typeof transition != 'undefined') {
+ // apply transition
+ var div1 = $(pname + '> .w2ui-panel-content');
+ div1.after('<div class="w2ui-panel-content new-panel" style="'+ div1[0].style.cssText +'"></div>');
+ var div2 = $(pname + '> .w2ui-panel-content.new-panel');
+ div1.css('top', panelTop);
+ div2.css('top', panelTop);
+ if (typeof data == 'object') {
+ data.box = div2[0]; // do not do .render(box);
+ data.render();
+ } else {
+ div2.html(data);
+ }
+ w2utils.transition(div1[0], div2[0], transition, function () {
+ div1.remove();
+ div2.removeClass('new-panel');
+ div2.css('overflow', p.overflow);
+ // IE Hack
+ obj.resize();
+ if (window.navigator.userAgent.indexOf('MSIE') != -1) setTimeout(function () { obj.resize(); }, 100);
+ });
+ }
+ }
+ this.refresh(panel);
+ }
+ }
+ // IE Hack
+ obj.resize();
+ if (window.navigator.userAgent.indexOf('MSIE') != -1) setTimeout(function () { obj.resize(); }, 100);
+ return true;
+ },
+
+ load: function (panel, url, transition, onLoad) {
+ var obj = this;
+ if (panel == 'css') {
+ $.get(url, function (data, status, xhr) { // should always be $.get as it is template
+ obj.content(panel, xhr.responseText);
+ if (onLoad) onLoad();
+ });
+ return true;
+ }
+ if (this.get(panel) !== null) {
+ $.get(url, function (data, status, xhr) { // should always be $.get as it is template
+ obj.content(panel, xhr.responseText, transition);
+ if (onLoad) onLoad();
+ // IE Hack
+ obj.resize();
+ if (window.navigator.userAgent.indexOf('MSIE') != -1) setTimeout(function () { obj.resize(); }, 100);
+ });
+ return true;
+ }
+ return false;
+ },
+
+ sizeTo: function (panel, size) {
+ var obj = this;
+ var pan = obj.get(panel);
+ if (pan === null) return false;
+ // resize
+ $(obj.box).find(' > div > .w2ui-panel').css({
+ '-webkit-transition' : '.2s',
+ '-moz-transition' : '.2s',
+ '-ms-transition' : '.2s',
+ '-o-transition' : '.2s'
+ });
+ setTimeout(function () {
+ obj.set(panel, { size: size });
+ }, 1);
+ // clean
+ setTimeout(function () {
+ $(obj.box).find(' > div > .w2ui-panel').css({
+ '-webkit-transition' : '0s',
+ '-moz-transition' : '0s',
+ '-ms-transition' : '0s',
+ '-o-transition' : '0s'
+ });
+ obj.resize();
+ }, 500);
+ return true;
+ },
+
+ show: function (panel, immediate) {
+ var obj = this;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'show', target: panel, object: this.get(panel), immediate: immediate });
+ if (eventData.isCancelled === true) return;
+
+ var p = obj.get(panel);
+ if (p === null) return false;
+ p.hidden = false;
+ if (immediate === true) {
+ $('#layout_'+ obj.name +'_panel_'+panel).css({ 'opacity': '1' });
+ if (p.resizable) $('#layout_'+ obj.name +'_resizer_'+panel).show();
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ obj.resize();
+ } else {
+ if (p.resizable) $('#layout_'+ obj.name +'_resizer_'+panel).show();
+ // resize
+ $('#layout_'+ obj.name +'_panel_'+panel).css({ 'opacity': '0' });
+ $(obj.box).find(' > div > .w2ui-panel').css({
+ '-webkit-transition' : '.2s',
+ '-moz-transition' : '.2s',
+ '-ms-transition' : '.2s',
+ '-o-transition' : '.2s'
+ });
+ setTimeout(function () { obj.resize(); }, 1);
+ // show
+ setTimeout(function() {
+ $('#layout_'+ obj.name +'_panel_'+ panel).css({ 'opacity': '1' });
+ }, 250);
+ // clean
+ setTimeout(function () {
+ $(obj.box).find(' > div > .w2ui-panel').css({
+ '-webkit-transition' : '0s',
+ '-moz-transition' : '0s',
+ '-ms-transition' : '0s',
+ '-o-transition' : '0s'
+ });
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ obj.resize();
+ }, 500);
+ }
+ return true;
+ },
+
+ hide: function (panel, immediate) {
+ var obj = this;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'hide', target: panel, object: this.get(panel), immediate: immediate });
+ if (eventData.isCancelled === true) return;
+
+ var p = obj.get(panel);
+ if (p === null) return false;
+ p.hidden = true;
+ if (immediate === true) {
+ $('#layout_'+ obj.name +'_panel_'+panel).css({ 'opacity': '0' });
+ $('#layout_'+ obj.name +'_resizer_'+panel).hide();
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ obj.resize();
+ } else {
+ $('#layout_'+ obj.name +'_resizer_'+panel).hide();
+ // hide
+ $(obj.box).find(' > div > .w2ui-panel').css({
+ '-webkit-transition' : '.2s',
+ '-moz-transition' : '.2s',
+ '-ms-transition' : '.2s',
+ '-o-transition' : '.2s'
+ });
+ $('#layout_'+ obj.name +'_panel_'+panel).css({ 'opacity': '0' });
+ setTimeout(function () { obj.resize(); }, 1);
+ // clean
+ setTimeout(function () {
+ $(obj.box).find(' > div > .w2ui-panel').css({
+ '-webkit-transition' : '0s',
+ '-moz-transition' : '0s',
+ '-ms-transition' : '0s',
+ '-o-transition' : '0s'
+ });
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ obj.resize();
+ }, 500);
+ }
+ return true;
+ },
+
+ toggle: function (panel, immediate) {
+ var p = this.get(panel);
+ if (p === null) return false;
+ if (p.hidden) return this.show(panel, immediate); else return this.hide(panel, immediate);
+ },
+
+ set: function (panel, options) {
+ var obj = this.get(panel, true);
+ if (obj === null) return false;
+ $.extend(this.panels[obj], options);
+ if (typeof options['content'] != 'undefined') this.refresh(panel); // refresh only when content changed
+ this.resize(); // resize is needed when panel size is changed
+ return true;
+ },
+
+ get: function (panel, returnIndex) {
+ for (var p in this.panels) {
+ if (this.panels[p].type == panel) {
+ if (returnIndex === true) return p; else return this.panels[p];
+ }
+ }
+ return null;
+ },
+
+ el: function (panel) {
+ var el = $('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-content');
+ if (el.length != 1) return null;
+ return el[0];
+ },
+
+ hideToolbar: function (panel) {
+ var pan = this.get(panel);
+ if (!pan) return;
+ pan.show.toolbar = false;
+ $('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-toolbar').hide();
+ this.resize();
+ },
+
+ showToolbar: function (panel) {
+ var pan = this.get(panel);
+ if (!pan) return;
+ pan.show.toolbar = true;
+ $('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-toolbar').show();
+ this.resize();
+ },
+
+ toggleToolbar: function (panel) {
+ var pan = this.get(panel);
+ if (!pan) return;
+ if (pan.show.toolbar) this.hideToolbar(panel); else this.showToolbar(panel);
+ },
+
+ hideTabs: function (panel) {
+ var pan = this.get(panel);
+ if (!pan) return;
+ pan.show.tabs = false;
+ $('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-tabs').hide();
+ this.resize();
+ },
+
+ showTabs: function (panel) {
+ var pan = this.get(panel);
+ if (!pan) return;
+ pan.show.tabs = true;
+ $('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-tabs').show();
+ this.resize();
+ },
+
+ toggleTabs: function (panel) {
+ var pan = this.get(panel);
+ if (!pan) return;
+ if (pan.show.tabs) this.hideTabs(panel); else this.showTabs(panel);
+ },
+
+ render: function (box) {
+ var obj = this;
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'render', target: obj.name, box: box });
+ if (eventData.isCancelled === true) return;
+
+ if (typeof box != 'undefined' && box !== null) {
+ if ($(obj.box).find('#layout_'+ obj.name +'_panel_main').length > 0) {
+ $(obj.box)
+ .removeAttr('name')
+ .removeClass('w2ui-layout')
+ .html('');
+ }
+ obj.box = box;
+ }
+ if (!obj.box) return false;
+ $(obj.box)
+ .attr('name', obj.name)
+ .addClass('w2ui-layout')
+ .html('<div></div>');
+ if ($(obj.box).length > 0) $(obj.box)[0].style.cssText += obj.style;
+ // create all panels
+ for (var p1 in w2layout_panels) {
+ p1 = w2layout_panels[p1];
+ var pan = obj.get(p1);
+ var html = '<div id="layout_'+ obj.name + '_panel_'+ p1 +'" class="w2ui-panel">'+
+ ' <div class="w2ui-panel-title"></div>'+
+ ' <div class="w2ui-panel-tabs"></div>'+
+ ' <div class="w2ui-panel-toolbar"></div>'+
+ ' <div class="w2ui-panel-content"></div>'+
+ '</div>'+
+ '<div id="layout_'+ obj.name + '_resizer_'+ p1 +'" class="w2ui-resizer"></div>';
+ $(obj.box).find(' > div').append(html);
+ // tabs are rendered in refresh()
+ }
+ $(obj.box).find(' > div')
+ .append('<div id="layout_'+ obj.name + '_panel_css" style="position: absolute; top: 10000px;"></div');
+ obj.refresh(); // if refresh is not called here, the layout will not be available right after initialization
+ // process event
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ // reinit events
+ setTimeout(function () { // needed this timeout to allow browser to render first if there are tabs or toolbar
+ initEvents();
+ obj.resize();
+ }, 0);
+ return (new Date()).getTime() - time;
+
+ function initEvents() {
+ obj.tmp.events = {
+ resize : function (event) {
+ w2ui[obj.name].resize();
+ },
+ resizeStart : resizeStart,
+ mouseMove : resizeMove,
+ mouseUp : resizeStop
+ };
+ $(window).on('resize', obj.tmp.events.resize);
+ }
+
+ function resizeStart(type, evnt) {
+ if (!obj.box) return;
+ if (!evnt) evnt = window.event;
+ if (!window.addEventListener) { window.document.attachEvent('onselectstart', function() { return false; } ); }
+ $(document).off('mousemove', obj.tmp.events.mouseMove).on('mousemove', obj.tmp.events.mouseMove);
+ $(document).off('mouseup', obj.tmp.events.mouseUp).on('mouseup', obj.tmp.events.mouseUp);
+ obj.tmp.resize = {
+ type : type,
+ x : evnt.screenX,
+ y : evnt.screenY,
+ diff_x : 0,
+ diff_y : 0,
+ value : 0
+ };
+ // lock all panels
+ for (var p1 in w2layout_panels) {
+ p1 = w2layout_panels[p1];
+ obj.lock(p1, { opacity: 0 });
+ }
+ if (type == 'left' || type == 'right') {
+ obj.tmp.resize.value = parseInt($('#layout_'+ obj.name +'_resizer_'+ type)[0].style.left);
+ }
+ if (type == 'top' || type == 'preview' || type == 'bottom') {
+ obj.tmp.resize.value = parseInt($('#layout_'+ obj.name +'_resizer_'+ type)[0].style.top);
+ }
+ }
+
+ function resizeStop(evnt) {
+ if (!obj.box) return;
+ if (!evnt) evnt = window.event;
+ if (!window.addEventListener) { window.document.attachEvent('onselectstart', function() { return false; } ); }
+ $(document).off('mousemove', obj.tmp.events.mouseMove);
+ $(document).off('mouseup', obj.tmp.events.mouseUp);
+ if (typeof obj.tmp.resize == 'undefined') return;
+ // unlock all panels
+ for (var p1 in w2layout_panels) {
+ obj.unlock(w2layout_panels[p1]);
+ }
+ // set new size
+ if (obj.tmp.diff_x !== 0 || obj.tmp.resize.diff_y !== 0) { // only recalculate if changed
+ var ptop = obj.get('top');
+ var pbottom = obj.get('bottom');
+ var panel = obj.get(obj.tmp.resize.type);
+ var height = parseInt($(obj.box).height());
+ var width = parseInt($(obj.box).width());
+ var str = String(panel.size);
+ var ns, nd;
+ switch (obj.tmp.resize.type) {
+ case 'top':
+ ns = parseInt(panel.sizeCalculated) + obj.tmp.resize.diff_y;
+ nd = 0;
+ break;
+ case 'bottom':
+ ns = parseInt(panel.sizeCalculated) - obj.tmp.resize.diff_y;
+ nd = 0;
+ break;
+ case 'preview':
+ ns = parseInt(panel.sizeCalculated) - obj.tmp.resize.diff_y;
+ nd = (ptop && !ptop.hidden ? ptop.sizeCalculated : 0) +
+ (pbottom && !pbottom.hidden ? pbottom.sizeCalculated : 0);
+ break;
+ case 'left':
+ ns = parseInt(panel.sizeCalculated) + obj.tmp.resize.diff_x;
+ nd = 0;
+ break;
+ case 'right':
+ ns = parseInt(panel.sizeCalculated) - obj.tmp.resize.diff_x;
+ nd = 0;
+ break;
+ }
+ // set size
+ if (str.substr(str.length-1) == '%') {
+ panel.size = Math.floor(ns * 100 /
+ (panel.type == 'left' || panel.type == 'right' ? width : height - nd) * 100) / 100 + '%';
+ } else {
+ panel.size = ns;
+ }
+ obj.resize();
+ }
+ $('#layout_'+ obj.name + '_resizer_'+ obj.tmp.resize.type).removeClass('active');
+ delete obj.tmp.resize;
+ }
+
+ function resizeMove(evnt) {
+ if (!obj.box) return;
+ if (!evnt) evnt = window.event;
+ if (typeof obj.tmp.resize == 'undefined') return;
+ var panel = obj.get(obj.tmp.resize.type);
+ // event before
+ var tmp = obj.tmp.resize;
+ var eventData = obj.trigger({ phase: 'before', type: 'resizing', target: obj.name, object: panel, originalEvent: evnt,
+ panel: tmp ? tmp.type : 'all', diff_x: tmp ? tmp.diff_x : 0, diff_y: tmp ? tmp.diff_y : 0 });
+ if (eventData.isCancelled === true) return;
+
+ var p = $('#layout_'+ obj.name + '_resizer_'+ tmp.type);
+ var resize_x = (evnt.screenX - tmp.x);
+ var resize_y = (evnt.screenY - tmp.y);
+ var mainPanel = obj.get('main');
+
+ if (!p.hasClass('active')) p.addClass('active');
+
+ switch (tmp.type) {
+ case 'left':
+ if (panel.minSize - resize_x > panel.width) {
+ resize_x = panel.minSize - panel.width;
+ }
+ if (panel.maxSize && (panel.width + resize_x > panel.maxSize)) {
+ resize_x = panel.maxSize - panel.width;
+ }
+ if (mainPanel.minSize + resize_x > mainPanel.width) {
+ resize_x = mainPanel.width - mainPanel.minSize;
+ }
+ break;
+
+ case 'right':
+ if (panel.minSize + resize_x > panel.width) {
+ resize_x = panel.width - panel.minSize;
+ }
+ if (panel.maxSize && (panel.width - resize_x > panel.maxSize)) {
+ resize_x = panel.width - panel.maxSize;
+ }
+ if (mainPanel.minSize - resize_x > mainPanel.width) {
+ resize_x = mainPanel.minSize - mainPanel.width;
+ }
+ break;
+
+ case 'top':
+ if (panel.minSize - resize_y > panel.height) {
+ resize_y = panel.minSize - panel.height;
+ }
+ if (panel.maxSize && (panel.height + resize_y > panel.maxSize)) {
+ resize_y = panel.maxSize - panel.height;
+ }
+ if (mainPanel.minSize + resize_y > mainPanel.height) {
+ resize_y = mainPanel.height - mainPanel.minSize;
+ }
+ break;
+
+ case 'preview':
+ case 'bottom':
+ if (panel.minSize + resize_y > panel.height) {
+ resize_y = panel.height - panel.minSize;
+ }
+ if (panel.maxSize && (panel.height - resize_y > panel.maxSize)) {
+ resize_y = panel.height - panel.maxSize;
+ }
+ if (mainPanel.minSize - resize_y > mainPanel.height) {
+ resize_y = mainPanel.minSize - mainPanel.height;
+ }
+ break;
+ }
+ tmp.diff_x = resize_x;
+ tmp.diff_y = resize_y;
+
+ switch (tmp.type) {
+ case 'top':
+ case 'preview':
+ case 'bottom':
+ tmp.diff_x = 0;
+ if (p.length > 0) p[0].style.top = (tmp.value + tmp.diff_y) + 'px';
+ break;
+
+ case 'left':
+ case 'right':
+ tmp.diff_y = 0;
+ if (p.length > 0) p[0].style.left = (tmp.value + tmp.diff_x) + 'px';
+ break;
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ },
+
+ refresh: function (panel) {
+ var obj = this;
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ if (typeof panel == 'undefined') panel = null;
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'refresh', target: (typeof panel != 'undefined' ? panel : obj.name), object: obj.get(panel) });
+ if (eventData.isCancelled === true) return;
+ // obj.unlock(panel);
+ if (typeof panel == 'string') {
+ var p = obj.get(panel);
+ if (p === null) return;
+ var pname = '#layout_'+ obj.name + '_panel_'+ p.type;
+ var rname = '#layout_'+ obj.name +'_resizer_'+ p.type;
+ // apply properties to the panel
+ $(pname).css({ display: p.hidden ? 'none' : 'block' });
+ if (p.resizable) $(rname).show(); else $(rname).hide();
+ // insert content
+ if (typeof p.content == 'object' && typeof p.content.render === 'function') {
+ p.content.box = $(pname +'> .w2ui-panel-content')[0];
+ setTimeout(function () {
+ // need to remove unnecessary classes
+ if ($(pname +'> .w2ui-panel-content').length > 0) {
+ $(pname +'> .w2ui-panel-content')
+ .removeClass()
+ .addClass('w2ui-panel-content')
+ .css('overflow', p.overflow)[0].style.cssText += ';' + p.style;
+ }
+ p.content.render(); // do not do .render(box);
+ }, 1);
+ } else {
+ // need to remove unnecessary classes
+ if ($(pname +'> .w2ui-panel-content').length > 0) {
+ $(pname +'> .w2ui-panel-content')
+ .removeClass()
+ .addClass('w2ui-panel-content')
+ .html(p.content)
+ .css('overflow', p.overflow)[0].style.cssText += ';' + p.style;
+ }
+ }
+ // if there are tabs and/or toolbar - render it
+ var tmp = $(obj.box).find(pname +'> .w2ui-panel-tabs');
+ if (p.show.tabs) {
+ if (tmp.find('[name='+ p.tabs.name +']').length === 0 && p.tabs !== null) tmp.w2render(p.tabs); else p.tabs.refresh();
+ } else {
+ tmp.html('').removeClass('w2ui-tabs').hide();
+ }
+ tmp = $(obj.box).find(pname +'> .w2ui-panel-toolbar');
+ if (p.show.toolbar) {
+ if (tmp.find('[name='+ p.toolbar.name +']').length === 0 && p.toolbar !== null) tmp.w2render(p.toolbar); else p.toolbar.refresh();
+ } else {
+ tmp.html('').removeClass('w2ui-toolbar').hide();
+ }
+ // show title
+ tmp = $(obj.box).find(pname +'> .w2ui-panel-title');
+ if (p.title) {
+ tmp.html(p.title).show();
+ } else {
+ tmp.html('').hide();
+ }
+ } else {
+ if ($('#layout_'+ obj.name +'_panel_main').length == 0) {
+ obj.render();
+ return;
+ }
+ obj.resize();
+ // refresh all of them
+ for (var p1 in this.panels) { obj.refresh(this.panels[p1].type); }
+ }
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ resize: function () {
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ if (!this.box) return false;
+ var time = (new Date()).getTime();
+ // event before
+ var tmp = this.tmp.resize;
+ var eventData = this.trigger({ phase: 'before', type: 'resize', target: this.name,
+ panel: tmp ? tmp.type : 'all', diff_x: tmp ? tmp.diff_x : 0, diff_y: tmp ? tmp.diff_y : 0 });
+ if (eventData.isCancelled === true) return;
+ if (this.padding < 0) this.padding = 0;
+
+ // layout itself
+ var width = parseInt($(this.box).width());
+ var height = parseInt($(this.box).height());
+ $(this.box).find(' > div').css({
+ width : width + 'px',
+ height : height + 'px'
+ });
+ var obj = this;
+ // panels
+ var pmain = this.get('main');
+ var pprev = this.get('preview');
+ var pleft = this.get('left');
+ var pright = this.get('right');
+ var ptop = this.get('top');
+ var pbottom = this.get('bottom');
+ var smain = true; // main always on
+ var sprev = (pprev !== null && pprev.hidden !== true ? true : false);
+ var sleft = (pleft !== null && pleft.hidden !== true ? true : false);
+ var sright = (pright !== null && pright.hidden !== true ? true : false);
+ var stop = (ptop !== null && ptop.hidden !== true ? true : false);
+ var sbottom = (pbottom !== null && pbottom.hidden !== true ? true : false);
+ var l, t, w, h, e;
+ // calculate %
+ for (var p in w2layout_panels) {
+ p = w2layout_panels[p];
+ if (p === 'main') continue;
+ var tmp = this.get(p);
+ if (!tmp) continue;
+ var str = String(tmp.size || 0);
+ if (str.substr(str.length-1) == '%') {
+ var tmph = height;
+ if (tmp.type == 'preview') {
+ tmph = tmph -
+ (ptop && !ptop.hidden ? ptop.sizeCalculated : 0) -
+ (pbottom && !pbottom.hidden ? pbottom.sizeCalculated : 0);
+ }
+ tmp.sizeCalculated = parseInt((tmp.type == 'left' || tmp.type == 'right' ? width : tmph) * parseFloat(tmp.size) / 100);
+ } else {
+ tmp.sizeCalculated = parseInt(tmp.size);
+ }
+ tmp.sizeCalculated = Math.max(tmp.sizeCalculated, parseInt(tmp.minSize));
+ }
+ // top if any
+ if (ptop !== null && ptop.hidden !== true) {
+ l = 0;
+ t = 0;
+ w = width;
+ h = ptop.sizeCalculated;
+ $('#layout_'+ this.name +'_panel_top').css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px'
+ }).show();
+ ptop.width = w;
+ ptop.height = h;
+ // resizer
+ if (ptop.resizable) {
+ t = ptop.sizeCalculated - (this.padding === 0 ? this.resizer : 0);
+ h = (this.resizer > this.padding ? this.resizer : this.padding);
+ $('#layout_'+ this.name +'_resizer_top').show().css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px',
+ 'cursor': 'ns-resize'
+ }).off('mousedown').on('mousedown', function (event) {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'resizerClick', target: 'top', originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ w2ui[obj.name].tmp.events.resizeStart('top', event);
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ return false;
+ });
+ }
+ } else {
+ $('#layout_'+ this.name +'_panel_top').hide();
+ }
+ // left if any
+ if (pleft !== null && pleft.hidden !== true) {
+ l = 0;
+ t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0);
+ w = pleft.sizeCalculated;
+ h = height - (stop ? ptop.sizeCalculated + this.padding : 0) -
+ (sbottom ? pbottom.sizeCalculated + this.padding : 0);
+ e = $('#layout_'+ this.name +'_panel_left');
+ if (window.navigator.userAgent.indexOf('MSIE') != -1 && e.length > 0 && e[0].clientHeight < e[0].scrollHeight) w += 17; // IE hack
+ e.css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px'
+ }).show();
+ pleft.width = w;
+ pleft.height = h;
+ // resizer
+ if (pleft.resizable) {
+ l = pleft.sizeCalculated - (this.padding === 0 ? this.resizer : 0);
+ w = (this.resizer > this.padding ? this.resizer : this.padding);
+ $('#layout_'+ this.name +'_resizer_left').show().css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px',
+ 'cursor': 'ew-resize'
+ }).off('mousedown').on('mousedown', function (event) {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'resizerClick', target: 'left', originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ w2ui[obj.name].tmp.events.resizeStart('left', event);
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ return false;
+ });
+ }
+ } else {
+ $('#layout_'+ this.name +'_panel_left').hide();
+ $('#layout_'+ this.name +'_resizer_left').hide();
+ }
+ // right if any
+ if (pright !== null && pright.hidden !== true) {
+ l = width - pright.sizeCalculated;
+ t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0);
+ w = pright.sizeCalculated;
+ h = height - (stop ? ptop.sizeCalculated + this.padding : 0) -
+ (sbottom ? pbottom.sizeCalculated + this.padding : 0);
+ $('#layout_'+ this.name +'_panel_right').css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px'
+ }).show();
+ pright.width = w;
+ pright.height = h;
+ // resizer
+ if (pright.resizable) {
+ l = l - this.padding;
+ w = (this.resizer > this.padding ? this.resizer : this.padding);
+ $('#layout_'+ this.name +'_resizer_right').show().css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px',
+ 'cursor': 'ew-resize'
+ }).off('mousedown').on('mousedown', function (event) {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'resizerClick', target: 'right', originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ w2ui[obj.name].tmp.events.resizeStart('right', event);
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ return false;
+ });
+ }
+ } else {
+ $('#layout_'+ this.name +'_panel_right').hide();
+ }
+ // bottom if any
+ if (pbottom !== null && pbottom.hidden !== true) {
+ l = 0;
+ t = height - pbottom.sizeCalculated;
+ w = width;
+ h = pbottom.sizeCalculated;
+ $('#layout_'+ this.name +'_panel_bottom').css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px'
+ }).show();
+ pbottom.width = w;
+ pbottom.height = h;
+ // resizer
+ if (pbottom.resizable) {
+ t = t - (this.padding === 0 ? 0 : this.padding);
+ h = (this.resizer > this.padding ? this.resizer : this.padding);
+ $('#layout_'+ this.name +'_resizer_bottom').show().css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px',
+ 'cursor': 'ns-resize'
+ }).off('mousedown').on('mousedown', function (event) {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'resizerClick', target: 'bottom', originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ w2ui[obj.name].tmp.events.resizeStart('bottom', event);
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ return false;
+ });
+ }
+ } else {
+ $('#layout_'+ this.name +'_panel_bottom').hide();
+ }
+ // main - always there
+ l = 0 + (sleft ? pleft.sizeCalculated + this.padding : 0);
+ t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0);
+ w = width - (sleft ? pleft.sizeCalculated + this.padding : 0) -
+ (sright ? pright.sizeCalculated + this.padding: 0);
+ h = height - (stop ? ptop.sizeCalculated + this.padding : 0) -
+ (sbottom ? pbottom.sizeCalculated + this.padding : 0) -
+ (sprev ? pprev.sizeCalculated + this.padding : 0);
+ e = $('#layout_'+ this.name +'_panel_main');
+ if (window.navigator.userAgent.indexOf('MSIE') != -1 && e.length > 0 && e[0].clientHeight < e[0].scrollHeight) w += 17; // IE hack
+ e.css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px'
+ });
+ pmain.width = w;
+ pmain.height = h;
+
+ // preview if any
+ if (pprev !== null && pprev.hidden !== true) {
+ l = 0 + (sleft ? pleft.sizeCalculated + this.padding : 0);
+ t = height - (sbottom ? pbottom.sizeCalculated + this.padding : 0) - pprev.sizeCalculated;
+ w = width - (sleft ? pleft.sizeCalculated + this.padding : 0) -
+ (sright ? pright.sizeCalculated + this.padding : 0);
+ h = pprev.sizeCalculated;
+ e = $('#layout_'+ this.name +'_panel_preview');
+ if (window.navigator.userAgent.indexOf('MSIE') != -1 && e.length > 0 && e[0].clientHeight < e[0].scrollHeight) w += 17; // IE hack
+ e.css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px'
+ }).show();
+ pprev.width = w;
+ pprev.height = h;
+ // resizer
+ if (pprev.resizable) {
+ t = t - (this.padding === 0 ? 0 : this.padding);
+ h = (this.resizer > this.padding ? this.resizer : this.padding);
+ $('#layout_'+ this.name +'_resizer_preview').show().css({
+ 'display': 'block',
+ 'left': l + 'px',
+ 'top': t + 'px',
+ 'width': w + 'px',
+ 'height': h + 'px',
+ 'cursor': 'ns-resize'
+ }).off('mousedown').on('mousedown', function (event) {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'resizerClick', target: 'preview', originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ w2ui[obj.name].tmp.events.resizeStart('preview', event);
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ return false;
+ });
+ }
+ } else {
+ $('#layout_'+ this.name +'_panel_preview').hide();
+ }
+
+ // display tabs and toolbar if needed
+ for (var p1 in w2layout_panels) {
+ p1 = w2layout_panels[p1];
+ var pan = this.get(p1);
+ var tmp2 = '#layout_'+ this.name +'_panel_'+ p1 +' > .w2ui-panel-';
+ var tabHeight = 0;
+ if (pan) {
+ if (pan.title) {
+ tabHeight += w2utils.getSize($(tmp2 + 'title').css({ top: tabHeight + 'px', display: 'block' }), 'height');
+ }
+ if (pan.show.tabs) {
+ if (pan.tabs !== null && w2ui[this.name +'_'+ p1 +'_tabs']) w2ui[this.name +'_'+ p1 +'_tabs'].resize();
+ tabHeight += w2utils.getSize($(tmp2 + 'tabs').css({ top: tabHeight + 'px', display: 'block' }), 'height');
+ }
+ if (pan.show.toolbar) {
+ if (pan.toolbar !== null && w2ui[this.name +'_'+ p1 +'_toolbar']) w2ui[this.name +'_'+ p1 +'_toolbar'].resize();
+ tabHeight += w2utils.getSize($(tmp2 + 'toolbar').css({ top: tabHeight + 'px', display: 'block' }), 'height');
+ }
+ }
+ $(tmp2 + 'content').css({ display: 'block' }).css({ top: tabHeight + 'px' });
+ }
+ // send resize to all objects
+ clearTimeout(this._resize_timer);
+ this._resize_timer = setTimeout(function () {
+ for (var e in w2ui) {
+ if (typeof w2ui[e].resize == 'function') {
+ // sent to all none-layouts
+ if (w2ui[e].panels == 'undefined') w2ui[e].resize();
+ // only send to nested layouts
+ var parent = $(w2ui[e].box).parents('.w2ui-layout');
+ if (parent.length > 0 && parent.attr('name') == obj.name) w2ui[e].resize();
+ }
+ }
+ }, 100);
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ destroy: function () {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'destroy', target: this.name });
+ if (eventData.isCancelled === true) return;
+ if (typeof w2ui[this.name] == 'undefined') return false;
+ // clean up
+ if ($(this.box).find('#layout_'+ this.name +'_panel_main').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-layout')
+ .html('');
+ }
+ delete w2ui[this.name];
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ if (this.tmp.events && this.tmp.events.resize) $(window).off('resize', this.tmp.events.resize);
+ return true;
+ },
+
+ lock: function (panel, msg, showSpinner) {
+ if (w2layout_panels.indexOf(panel) == -1) {
+ console.log('ERROR: First parameter needs to be the a valid panel name.');
+ return;
+ }
+ var args = Array.prototype.slice.call(arguments, 0);
+ args[0] = '#layout_'+ this.name + '_panel_' + panel;
+ w2utils.lock.apply(window, args);
+ },
+
+ unlock: function (panel) {
+ if (w2layout_panels.indexOf(panel) == -1) {
+ console.log('ERROR: First parameter needs to be the a valid panel name.');
+ return;
+ }
+ var nm = '#layout_'+ this.name + '_panel_' + panel;
+ w2utils.unlock(nm);
+ }
+ };
+
+ $.extend(w2layout.prototype, w2utils.event);
+ w2obj.layout = w2layout;
+})();
+
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2popup - popup widget
+* - $().w2popup - jQuery wrapper
+* - Dependencies: jQuery, w2utils
+*
+* == NICE TO HAVE ==
+* - transition should include title, body and buttons, not just body
+*
+************************************************************************/
+
+var w2popup = {};
+
+(function () {
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2popup = function(method, options) {
+ if (typeof method === 'undefined') {
+ options = {};
+ method = 'open';
+ }
+ if ($.isPlainObject(method)) {
+ options = method;
+ method = 'open';
+ }
+ method = method.toLowerCase();
+ if (method === 'load' && typeof options === 'string') {
+ options = $.extend({ url: options }, arguments.length > 2 ? arguments[2] : {});
+ }
+ if (method === 'open' && options.url != null) method = 'load';
+ options = options || {};
+ // load options from markup
+ var dlgOptions = {};
+ if ($(this).length > 0) {
+ if ($(this).find('div[rel=title], div[rel=body], div[rel=buttons]').length > 0) {
+ if ($(this).find('div[rel=title]').length > 0) {
+ dlgOptions['title'] = $(this).find('div[rel=title]').html();
+ }
+ if ($(this).find('div[rel=body]').length > 0) {
+ dlgOptions['body'] = $(this).find('div[rel=body]').html();
+ dlgOptions['style'] = $(this).find('div[rel=body]')[0].style.cssText;
+ }
+ if ($(this).find('div[rel=buttons]').length > 0) {
+ dlgOptions['buttons'] = $(this).find('div[rel=buttons]').html();
+ }
+ } else {
+ dlgOptions['title'] = '&nbsp;';
+ dlgOptions['body'] = $(this).html();
+ }
+ if (parseInt($(this).css('width')) != 0) dlgOptions['width'] = parseInt($(this).css('width'));
+ if (parseInt($(this).css('height')) != 0) dlgOptions['height'] = parseInt($(this).css('height'));
+ }
+ // show popup
+ return w2popup[method]($.extend({}, dlgOptions, options));
+ };
+
+ // ====================================================
+ // -- Implementation of core functionality (SINGELTON)
+
+ w2popup = {
+ defaults: {
+ title : '',
+ body : '',
+ buttons : '',
+ style : '',
+ color : '#000',
+ opacity : 0.4,
+ speed : 0.3,
+ modal : false,
+ maximized : false,
+ keyboard : true, // will close popup on esc if not modal
+ width : 500,
+ height : 300,
+ showClose : true,
+ showMax : false,
+ transition: null
+ },
+ status : 'closed', // string that describes current status
+ handlers : [],
+ onOpen : null,
+ onClose : null,
+ onMax : null,
+ onMin : null,
+ onToggle : null,
+ onKeydown : null,
+
+ open: function (options) {
+ var obj = this;
+ if (w2popup.status == 'closing') {
+ setTimeout(function () { obj.open.call(obj, options); }, 100);
+ return;
+ }
+ // get old options and merge them
+ var old_options = $('#w2ui-popup').data('options');
+ var options = $.extend({}, this.defaults, old_options, { title: '', body : '', buttons: '' }, options, { maximized: false });
+ // need timer because popup might not be open
+ setTimeout(function () { $('#w2ui-popup').data('options', options); }, 100);
+ // if new - reset event handlers
+ if ($('#w2ui-popup').length == 0) {
+ w2popup.handlers = [];
+ w2popup.onMax = null;
+ w2popup.onMin = null;
+ w2popup.onToggle = null;
+ w2popup.onOpen = null;
+ w2popup.onClose = null;
+ w2popup.onKeydown = null;
+ }
+ if (options.onOpen) w2popup.onOpen = options.onOpen;
+ if (options.onClose) w2popup.onClose = options.onClose;
+ if (options.onMax) w2popup.onMax = options.onMax;
+ if (options.onMin) w2popup.onMin = options.onMin;
+ if (options.onToggle) w2popup.onToggle = options.onToggle;
+ if (options.onKeydown) w2popup.onKeydown = options.onKeydown;
+
+ if (window.innerHeight == undefined) {
+ var width = document.documentElement.offsetWidth;
+ var height = document.documentElement.offsetHeight;
+ if (w2utils.engine === 'IE7') { width += 21; height += 4; }
+ } else {
+ var width = window.innerWidth;
+ var height = window.innerHeight;
+ }
+ if (parseInt(width) - 10 < parseInt(options.width)) options.width = parseInt(width) - 10;
+ if (parseInt(height) - 10 < parseInt(options.height)) options.height = parseInt(height) - 10;
+ var top = parseInt(((parseInt(height) - parseInt(options.height)) / 2) * 0.6);
+ var left = parseInt((parseInt(width) - parseInt(options.width)) / 2);
+ // check if message is already displayed
+ if ($('#w2ui-popup').length == 0) {
+ // trigger event
+ var eventData = this.trigger({ phase: 'before', type: 'open', target: 'popup', options: options, present: false });
+ if (eventData.isCancelled === true) return;
+ w2popup.status = 'opening';
+ // output message
+ w2popup.lockScreen(options);
+ var btn = '';
+ if (options.showClose) {
+ btn += '<div class="w2ui-msg-button w2ui-msg-close" onmousedown="event.stopPropagation()" onclick="w2popup.close()">Close</div>';
+ }
+ if (options.showMax) {
+ btn += '<div class="w2ui-msg-button w2ui-msg-max" onmousedown="event.stopPropagation()" onclick="w2popup.toggle()">Max</div>';
+ }
+ var msg='<div id="w2ui-popup" class="w2ui-popup" style="opacity: 0; left: '+ left +'px; top: '+ top +'px;'+
+ ' width: ' + parseInt(options.width) + 'px; height: ' + parseInt(options.height) + 'px; '+
+ ' -webkit-transform: scale(0.8); -moz-transform: scale(0.8); -ms-transform: scale(0.8); -o-transform: scale(0.8); "'+
+ '>'+
+ ' <div class="w2ui-msg-title" style="'+ (options.title == '' ? 'display: none' : '') +'">' + btn + options.title + '</div>'+
+ ' <div class="w2ui-box1" style="'+ (options.title == '' ? 'top: 0px !important;' : '') +
+ (options.buttons == '' ? 'bottom: 0px !important;' : '') + '">'+
+ ' <div class="w2ui-msg-body' + (!options.title != '' ? ' w2ui-msg-no-title' : '') +
+ (!options.buttons != '' ? ' w2ui-msg-no-buttons' : '') + '" style="' + options.style + '">' + options.body + '</div>'+
+ ' </div>'+
+ ' <div class="w2ui-box2" style="' + (options.title == '' ? 'top: 0px !important;' : '') +
+ (options.buttons == '' ? 'bottom: 0px !important;' : '') + '">'+
+ ' <div class="w2ui-msg-body' + (!options.title != '' ? ' w2ui-msg-no-title' : '') +
+ (!options.buttons != '' ? ' w2ui-msg-no-buttons' : '') + '" style="' + options.style + '"></div>'+
+ ' </div>'+
+ ' <div class="w2ui-msg-buttons" style="'+ (options.buttons == '' ? 'display: none' : '') +'">' + options.buttons + '</div>'+
+ '</div>';
+ $('body').append(msg);
+ // allow element to render
+ setTimeout(function () {
+ $('#w2ui-popup .w2ui-box2').hide();
+ $('#w2ui-popup').css({
+ '-webkit-transition': options.speed + 's opacity, ' + options.speed + 's -webkit-transform',
+ '-webkit-transform': 'scale(1)',
+ '-moz-transition': options.speed + 's opacity, ' + options.speed + 's -moz-transform',
+ '-moz-transform': 'scale(1)',
+ '-ms-transition': options.speed + 's opacity, ' + options.speed + 's -ms-transform',
+ '-ms-transform': 'scale(1)',
+ '-o-transition': options.speed + 's opacity, ' + options.speed + 's -o-transform',
+ '-o-transform': 'scale(1)',
+ 'opacity': '1'
+ });
+ }, 1);
+ // clean transform
+ setTimeout(function () {
+ $('#w2ui-popup').css({
+ '-webkit-transform': '',
+ '-moz-transform': '',
+ '-ms-transform': '',
+ '-o-transform': ''
+ });
+ // event after
+ w2popup.status = 'open';
+ setTimeout(function () {
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, 100);
+ }, options.speed * 1000);
+ } else {
+ // trigger event
+ var eventData = this.trigger({ phase: 'before', type: 'open', target: 'popup', options: options, present: true });
+ if (eventData.isCancelled === true) return;
+ // check if size changed
+ w2popup.status = 'opening';
+ if (typeof old_options == 'undefined' || old_options['width'] != options['width'] || old_options['height'] != options['height']) {
+ w2popup.resize(options.width, options.height);
+ }
+ if (typeof old_options != 'undefined') {
+ options.prevSize = options.width + ':' + options.height;
+ options.maximized = old_options.maximized;
+ }
+ // show new items
+ var body = $('#w2ui-popup .w2ui-box2 > .w2ui-msg-body').html(options.body);
+ if (body.length > 0) body[0].style.cssText = options.style;
+ if (options.buttons != '') {
+ $('#w2ui-popup .w2ui-msg-buttons').show().html(options.buttons);
+ $('#w2ui-popup .w2ui-msg-body').removeClass('w2ui-msg-no-buttons');
+ $('#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2').css('bottom', '');
+ } else {
+ $('#w2ui-popup .w2ui-msg-buttons').hide().html('');
+ $('#w2ui-popup .w2ui-msg-body').addClass('w2ui-msg-no-buttons');
+ $('#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2').css('bottom', '0px');
+ }
+ if (options.title != '') {
+ $('#w2ui-popup .w2ui-msg-title').show().html(
+ (options.showClose ? '<div class="w2ui-msg-button w2ui-msg-close" onmousedown="event.stopPropagation()" onclick="w2popup.close()">Close</div>' : '') +
+ (options.showMax ? '<div class="w2ui-msg-button w2ui-msg-max" onmousedown="event.stopPropagation()" onclick="w2popup.toggle()">Max</div>' : '') +
+ options.title);
+ $('#w2ui-popup .w2ui-msg-body').removeClass('w2ui-msg-no-title');
+ $('#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2').css('top', '');
+ } else {
+ $('#w2ui-popup .w2ui-msg-title').hide().html('');
+ $('#w2ui-popup .w2ui-msg-body').addClass('w2ui-msg-no-title');
+ $('#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2').css('top', '0px');
+ }
+ // transition
+ var div_old = $('#w2ui-popup .w2ui-box1')[0];
+ var div_new = $('#w2ui-popup .w2ui-box2')[0];
+ w2utils.transition(div_old, div_new, options.transition);
+ div_new.className = 'w2ui-box1';
+ div_old.className = 'w2ui-box2';
+ $(div_new).addClass('w2ui-current-box');
+ // remove max state
+ $('#w2ui-popup').data('prev-size', null);
+ // call event onChange
+ setTimeout(function () {
+ w2popup.status = 'open';
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, 100);
+ }
+ // save new options
+ options._last_w2ui_name = w2utils.keyboard.active();
+ w2utils.keyboard.active(null);
+ // keyboard events
+ if (options.keyboard) $(document).on('keydown', this.keydown);
+
+ // initialize move
+ var tmp = {
+ resizing : false,
+ mvMove : mvMove,
+ mvStop : mvStop
+ };
+ $('#w2ui-popup .w2ui-msg-title').on('mousedown', function (event) { mvStart(event); })
+
+ // handlers
+ function mvStart(evnt) {
+ if (!evnt) evnt = window.event;
+ if (!window.addEventListener) { window.document.attachEvent('onselectstart', function() { return false; } ); }
+ w2popup.status = 'moving';
+ tmp.resizing = true;
+ tmp.x = evnt.screenX;
+ tmp.y = evnt.screenY;
+ tmp.pos_x = $('#w2ui-popup').position().left;
+ tmp.pos_y = $('#w2ui-popup').position().top;
+ w2popup.lock({ opacity: 0 });
+ $(document).on('mousemove', tmp.mvMove);
+ $(document).on('mouseup', tmp.mvStop);
+ if (evnt.stopPropagation) evnt.stopPropagation(); else evnt.cancelBubble = true;
+ if (evnt.preventDefault) evnt.preventDefault(); else return false;
+ }
+
+ function mvMove(evnt) {
+ if (tmp.resizing != true) return;
+ if (!evnt) evnt = window.event;
+ tmp.div_x = evnt.screenX - tmp.x;
+ tmp.div_y = evnt.screenY - tmp.y;
+ $('#w2ui-popup').css({
+ '-webkit-transition': 'none',
+ '-webkit-transform': 'translate3d('+ tmp.div_x +'px, '+ tmp.div_y +'px, 0px)',
+ '-moz-transition': 'none',
+ '-moz-transform': 'translate('+ tmp.div_x +'px, '+ tmp.div_y +'px)',
+ '-ms-transition': 'none',
+ '-ms-transform': 'translate('+ tmp.div_x +'px, '+ tmp.div_y +'px)',
+ '-o-transition': 'none',
+ '-o-transform': 'translate('+ tmp.div_x +'px, '+ tmp.div_y +'px)'
+ });
+ }
+
+ function mvStop(evnt) {
+ if (tmp.resizing != true) return;
+ if (!evnt) evnt = window.event;
+ w2popup.status = 'open';
+ tmp.div_x = (evnt.screenX - tmp.x);
+ tmp.div_y = (evnt.screenY - tmp.y);
+ $('#w2ui-popup').css({
+ 'left': (tmp.pos_x + tmp.div_x) + 'px',
+ 'top': (tmp.pos_y + tmp.div_y) + 'px',
+ '-webkit-transition': 'none',
+ '-webkit-transform': 'translate3d(0px, 0px, 0px)',
+ '-moz-transition': 'none',
+ '-moz-transform': 'translate(0px, 0px)',
+ '-ms-transition': 'none',
+ '-ms-transform': 'translate(0px, 0px)',
+ '-o-transition': 'none',
+ '-o-transform': 'translate(0px, 0px)'
+ });
+ tmp.resizing = false;
+ $(document).off('mousemove', tmp.mvMove);
+ $(document).off('mouseup', tmp.mvStop);
+ w2popup.unlock();
+ }
+ return this;
+ },
+
+ keydown: function (event) {
+ var options = $('#w2ui-popup').data('options');
+ if (!options.keyboard) return;
+ // trigger event
+ var eventData = w2popup.trigger({ phase: 'before', type: 'keydown', target: 'popup', options: options, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ switch (event.keyCode) {
+ case 27:
+ event.preventDefault();
+ if ($('#w2ui-popup .w2ui-popup-message').length > 0) w2popup.message(); else w2popup.close();
+ break;
+ }
+ // event after
+ w2popup.trigger($.extend(eventData, { phase: 'after'}));
+ },
+
+ close: function (options) {
+ var obj = this;
+ var options = $.extend({}, $('#w2ui-popup').data('options'), options);
+ if ($('#w2ui-popup').length == 0) return;
+ // trigger event
+ var eventData = this.trigger({ phase: 'before', type: 'close', target: 'popup', options: options });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ w2popup.status = 'closing';
+ $('#w2ui-popup').css({
+ '-webkit-transition': options.speed + 's opacity, ' + options.speed + 's -webkit-transform',
+ '-webkit-transform': 'scale(0.9)',
+ '-moz-transition': options.speed + 's opacity, ' + options.speed + 's -moz-transform',
+ '-moz-transform': 'scale(0.9)',
+ '-ms-transition': options.speed + 's opacity, ' + options.speed + 's -ms-transform',
+ '-ms-transform': 'scale(0.9)',
+ '-o-transition': options.speed + 's opacity, ' + options.speed + 's -o-transform',
+ '-o-transform': 'scale(0.9)',
+ 'opacity': '0'
+ });
+ w2popup.unlockScreen(options);
+ setTimeout(function () {
+ $('#w2ui-popup').remove();
+ w2popup.status = 'closed';
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after'}));
+ }, options.speed * 1000);
+ // restore active
+ w2utils.keyboard.active(options._last_w2ui_name);
+ // remove keyboard events
+ if (options.keyboard) $(document).off('keydown', this.keydown);
+ },
+
+ toggle: function () {
+ var obj = this;
+ var options = $('#w2ui-popup').data('options');
+ // trigger event
+ var eventData = this.trigger({ phase: 'before', type: 'toggle', target: 'popup', options: options });
+ if (eventData.isCancelled === true) return;
+ // defatul action
+ if (options.maximized === true) w2popup.min(); else w2popup.max();
+ // event after
+ setTimeout(function () {
+ obj.trigger($.extend(eventData, { phase: 'after'}));
+ }, (options.speed * 1000) + 50);
+ },
+
+ max: function () {
+ var obj = this;
+ var options = $('#w2ui-popup').data('options');
+ if (options.maximized === true) return;
+ // trigger event
+ var eventData = this.trigger({ phase: 'before', type: 'max', target: 'popup', options: options });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ w2popup.status = 'resizing';
+ options.prevSize = $('#w2ui-popup').css('width') + ':' + $('#w2ui-popup').css('height');
+ // do resize
+ w2popup.resize(10000, 10000, function () {
+ w2popup.status = 'open';
+ options.maximized = true;
+ obj.trigger($.extend(eventData, { phase: 'after'}));
+ });
+ },
+
+ min: function () {
+ var obj = this;
+ var options = $('#w2ui-popup').data('options');
+ if (options.maximized !== true) return;
+ var size = options.prevSize.split(':');
+ // trigger event
+ var eventData = this.trigger({ phase: 'before', type: 'min', target: 'popup', options: options });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ w2popup.status = 'resizing';
+ // do resize
+ w2popup.resize(size[0], size[1], function () {
+ w2popup.status = 'open';
+ options.maximized = false;
+ options.prevSize = null;
+ obj.trigger($.extend(eventData, { phase: 'after'}));
+ });
+ },
+
+ get: function () {
+ return $('#w2ui-popup').data('options');
+ },
+
+ set: function (options) {
+ w2popup.open(options);
+ },
+
+ clear: function() {
+ $('#w2ui-popup .w2ui-msg-title').html('');
+ $('#w2ui-popup .w2ui-msg-body').html('');
+ $('#w2ui-popup .w2ui-msg-buttons').html('');
+ },
+
+ reset: function () {
+ w2popup.open(w2popup.defaults);
+ },
+
+ load: function (options) {
+ w2popup.status = 'loading';
+ if (String(options.url) == 'undefined') {
+ console.log('ERROR: The url parameter is empty.');
+ return;
+ }
+ var tmp = String(options.url).split('#');
+ var url = tmp[0];
+ var selector = tmp[1];
+ if (String(options) == 'undefined') options = {};
+ // load url
+ var html = $('#w2ui-popup').data(url);
+ if (typeof html != 'undefined' && html != null) {
+ popup(html, selector);
+ } else {
+ $.get(url, function (data, status, obj) { // should always be $.get as it is template
+ popup(obj.responseText, selector);
+ $('#w2ui-popup').data(url, obj.responseText); // remember for possible future purposes
+ });
+ }
+ function popup(html, selector) {
+ delete options.url;
+ $('body').append('<div id="w2ui-tmp" style="display: none">' + html + '</div>');
+ if (typeof selector != 'undefined' && $('#w2ui-tmp #'+selector).length > 0) {
+ $('#w2ui-tmp #' + selector).w2popup(options);
+ } else {
+ $('#w2ui-tmp > div').w2popup(options);
+ }
+ // link styles
+ if ($('#w2ui-tmp > style').length > 0) {
+ var style = $('<div>').append($('#w2ui-tmp > style').clone()).html();
+ if ($('#w2ui-popup #div-style').length == 0) {
+ $('#w2ui-popup').append('<div id="div-style" style="position: absolute; left: -100; width: 1px"></div>');
+ }
+ $('#w2ui-popup #div-style').html(style);
+ }
+ $('#w2ui-tmp').remove();
+ }
+ },
+
+ message: function (options) {
+ $().w2tag(); // hide all tags
+ if (!options) options = { width: 200, height: 100 };
+ if (parseInt(options.width) < 10) options.width = 10;
+ if (parseInt(options.height) < 10) options.height = 10;
+ if (typeof options.hideOnClick == 'undefined') options.hideOnClick = false;
+ var poptions = $('#w2ui-popup').data('options') || {};
+ if (typeof options.width == 'undefined' || options.width > poptions.width - 10) options.width = poptions.width - 10;
+ if (typeof options.height == 'undefined' || options.height > poptions.height - 40) options.height = poptions.height - 40; // title is 30px or so
+
+ var head = $('#w2ui-popup .w2ui-msg-title');
+ var pwidth = parseInt($('#w2ui-popup').width());
+ var msgCount = $('#w2ui-popup .w2ui-popup-message').length;
+ // remove message
+ if ($.trim(options.html) == '') {
+ $('#w2ui-popup #w2ui-message'+ (msgCount-1)).css('z-Index', 250);
+ var options = $('#w2ui-popup #w2ui-message'+ (msgCount-1)).data('options') || {};
+ $('#w2ui-popup #w2ui-message'+ (msgCount-1)).remove();
+ if (typeof options.onClose == 'function') options.onClose();
+ if (msgCount == 1) {
+ w2popup.unlock();
+ } else {
+ $('#w2ui-popup #w2ui-message'+ (msgCount-2)).show();
+ }
+ } else {
+ // hide previous messages
+ $('#w2ui-popup .w2ui-popup-message').hide();
+ // add message
+ $('#w2ui-popup .w2ui-box1')
+ .before('<div id="w2ui-message' + msgCount + '" class="w2ui-popup-message" style="display: none; ' +
+ (head.length == 0 ? 'top: 0px;' : 'top: ' + w2utils.getSize(head, 'height') + 'px;') +
+ (typeof options.width != 'undefined' ? 'width: ' + options.width + 'px; left: ' + ((pwidth - options.width) / 2) + 'px;' : 'left: 10px; right: 10px;') +
+ (typeof options.height != 'undefined' ? 'height: ' + options.height + 'px;' : 'bottom: 6px;') +
+ '-webkit-transition: .3s; -moz-transition: .3s; -ms-transition: .3s; -o-transition: .3s;"' +
+ (options.hideOnClick === true ? 'onclick="w2popup.message();"' : '') + '>' +
+ '</div>');
+ $('#w2ui-popup #w2ui-message'+ msgCount).data('options', options);
+ var display = $('#w2ui-popup #w2ui-message'+ msgCount).css('display');
+ $('#w2ui-popup #w2ui-message'+ msgCount).css({
+ '-webkit-transform': (display == 'none' ? 'translateY(-' + options.height + 'px)' : 'translateY(0px)'),
+ '-moz-transform': (display == 'none' ? 'translateY(-' + options.height + 'px)' : 'translateY(0px)'),
+ '-ms-transform': (display == 'none' ? 'translateY(-' + options.height + 'px)' : 'translateY(0px)'),
+ '-o-transform': (display == 'none' ? 'translateY(-' + options.height + 'px)' : 'translateY(0px)')
+ });
+ if (display == 'none') {
+ $('#w2ui-popup #w2ui-message'+ msgCount).show().html(options.html);
+ // timer needs to animation
+ setTimeout(function () {
+ $('#w2ui-popup #w2ui-message'+ msgCount).css({
+ '-webkit-transform': (display == 'none' ? 'translateY(0px)' : 'translateY(-' + options.height + 'px)'),
+ '-moz-transform': (display == 'none' ? 'translateY(0px)' : 'translateY(-' + options.height + 'px)'),
+ '-ms-transform': (display == 'none' ? 'translateY(0px)' : 'translateY(-' + options.height + 'px)'),
+ '-o-transform': (display == 'none' ? 'translateY(0px)' : 'translateY(-' + options.height + 'px)')
+ });
+ }, 1);
+ // timer for lock
+ setTimeout(function() {
+ $('#w2ui-popup #w2ui-message'+ msgCount).css({
+ '-webkit-transition': '0s', '-moz-transition': '0s', '-ms-transition': '0s', '-o-transition': '0s',
+ 'z-Index': 1500
+ }); // has to be on top of lock
+ if (msgCount == 0) w2popup.lock();
+ if (typeof options.onOpen == 'function') options.onOpen();
+ }, 300);
+ }
+ }
+ },
+
+ lock: function (msg, showSpinner) {
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift($('#w2ui-popup'));
+ w2utils.lock.apply(window, args);
+ },
+
+ unlock: function () {
+ w2utils.unlock($('#w2ui-popup'));
+ },
+
+ // --- INTERNAL FUNCTIONS
+
+ lockScreen: function (options) {
+ if ($('#w2ui-lock').length > 0) return false;
+ if (typeof options == 'undefined') options = $('#w2ui-popup').data('options');
+ if (typeof options == 'undefined') options = {};
+ options = $.extend({}, w2popup.defaults, options);
+ // show element
+ $('body').append('<div id="w2ui-lock" ' +
+ ' onmousewheel="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true; if (event.preventDefault) event.preventDefault(); else return false;"'+
+ ' style="position: ' + (w2utils.engine == 'IE5' ? 'absolute' : 'fixed') + '; z-Index: 1199; left: 0px; top: 0px; ' +
+ ' padding: 0px; margin: 0px; background-color: ' + options.color + '; width: 100%; height: 100%; opacity: 0;"></div>');
+ // lock screen
+ setTimeout(function () {
+ $('#w2ui-lock').css({
+ '-webkit-transition': options.speed + 's opacity',
+ '-moz-transition': options.speed + 's opacity',
+ '-ms-transition': options.speed + 's opacity',
+ '-o-transition': options.speed + 's opacity',
+ 'opacity': options.opacity
+ });
+ }, 1);
+ // add events
+ if (options.modal == true) {
+ $('#w2ui-lock').on('mousedown', function () {
+ $('#w2ui-lock').css({
+ '-webkit-transition': '.1s',
+ '-moz-transition': '.1s',
+ '-ms-transition': '.1s',
+ '-o-transition': '.1s',
+ 'opacity': '0.6'
+ });
+ // if (window.getSelection) window.getSelection().removeAllRanges();
+ });
+ $('#w2ui-lock').on('mouseup', function () {
+ setTimeout(function () {
+ $('#w2ui-lock').css({
+ '-webkit-transition': '.1s',
+ '-moz-transition': '.1s',
+ '-ms-transition': '.1s',
+ '-o-transition': '.1s',
+ 'opacity': options.opacity
+ });
+ }, 100);
+ // if (window.getSelection) window.getSelection().removeAllRanges();
+ });
+ } else {
+ $('#w2ui-lock').on('mouseup', function () { w2popup.close(); });
+ }
+ return true;
+ },
+
+ unlockScreen: function (options) {
+ if ($('#w2ui-lock').length == 0) return false;
+ if (typeof options == 'undefined') options = $('#w2ui-popup').data('options');
+ if (typeof options == 'undefined') options = {};
+ options = $.extend({}, w2popup.defaults, options);
+ $('#w2ui-lock').css({
+ '-webkit-transition': options.speed + 's opacity',
+ '-moz-transition': options.speed + 's opacity',
+ '-ms-transition': options.speed + 's opacity',
+ '-o-transition': options.speed + 's opacity',
+ 'opacity': 0
+ });
+ setTimeout(function () {
+ $('#w2ui-lock').remove();
+ }, options.speed * 1000);
+ return true;
+ },
+
+ resize: function (width, height, callBack) {
+ var options = $('#w2ui-popup').data('options');
+ // calculate new position
+ if (parseInt($(window).width()) - 10 < parseInt(width)) width = parseInt($(window).width()) - 10;
+ if (parseInt($(window).height()) - 10 < parseInt(height)) height = parseInt($(window).height()) - 10;
+ var top = ((parseInt($(window).height()) - parseInt(height)) / 2) * 0.8;
+ var left = (parseInt($(window).width()) - parseInt(width)) / 2;
+ // resize there
+ $('#w2ui-popup').css({
+ '-webkit-transition': options.speed + 's width, ' + options.speed + 's height, ' + options.speed + 's left, ' + options.speed + 's top',
+ '-moz-transition': options.speed + 's width, ' + options.speed + 's height, ' + options.speed + 's left, ' + options.speed + 's top',
+ '-ms-transition': options.speed + 's width, ' + options.speed + 's height, ' + options.speed + 's left, ' + options.speed + 's top',
+ '-o-transition': options.speed + 's width, ' + options.speed + 's height, ' + options.speed + 's left, ' + options.speed + 's top',
+ 'top': top,
+ 'left': left,
+ 'width': width,
+ 'height': height
+ });
+ setTimeout(function () {
+ options.width = width;
+ options.height = height;
+ if (typeof callBack == 'function') callBack();
+ }, (options.speed * 1000) + 50); // give extra 50 ms
+ }
+ }
+
+ // merge in event handling
+ $.extend(w2popup, w2utils.event);
+
+})();
+
+// ============================================
+// --- Common dialogs
+
+var w2alert = function (msg, title, callBack) {
+ if (title == null) title = w2utils.lang('Notification');
+ if ($('#w2ui-popup').length > 0 && w2popup.status != 'closing') {
+ w2popup.message({
+ width : 400,
+ height : 170,
+ html : '<div style="position: absolute; top: 0px; left: 0px; right: 0px; bottom: 45px; overflow: auto">' +
+ ' <div class="w2ui-centered" style="font-size: 13px;">' + msg + '</div>' +
+ '</div>' +
+ '<div style="position: absolute; bottom: 7px; left: 0px; right: 0px; text-align: center; padding: 5px">' +
+ ' <button onclick="w2popup.message();" class="w2ui-popup-btn btn">' + w2utils.lang('Ok') + '</button>' +
+ '</div>',
+ onClose : function () {
+ if (typeof callBack == 'function') callBack();
+ }
+ });
+ } else {
+ w2popup.open({
+ width : 450,
+ height : 220,
+ showMax : false,
+ showClose : false,
+ title : title,
+ body : '<div class="w2ui-centered" style="font-size: 13px;">' + msg + '</div>',
+ buttons : '<button onclick="w2popup.close();" class="w2ui-popup-btn btn">' + w2utils.lang('Ok') + '</button>',
+ onClose : function () {
+ if (typeof callBack == 'function') callBack();
+ }
+ });
+ }
+};
+
+var w2confirm = function (msg, title, callBack) {
+ var options = {};
+ var defaults = {
+ msg : '',
+ title : w2utils.lang('Confirmation'),
+ width : ($('#w2ui-popup').length > 0 ? 400 : 450),
+ height : ($('#w2ui-popup').length > 0 ? 170 : 220),
+ yes_text : 'Yes',
+ yes_class : '',
+ yes_style : '',
+ yes_callBack: null,
+ no_text : 'No',
+ no_class : '',
+ no_style : '',
+ no_callBack : null,
+ callBack : null
+ };
+ if (arguments.length == 1 && typeof msg == 'object') {
+ $.extend(options, defaults, msg);
+ } else {
+ if (typeof title == 'function') {
+ $.extend(options, defaults, {
+ msg : msg,
+ callBack: title
+ })
+ } else {
+ $.extend(options, defaults, {
+ msg : msg,
+ title : title,
+ callBack: callBack
+ })
+ }
+ }
+ if ($('#w2ui-popup').length > 0 && w2popup.status != 'closing') {
+ if (options.width > w2popup.get().width) options.width = w2popup.get().width;
+ if (options.height > (w2popup.get().height - 50)) options.height = w2popup.get().height - 50;
+ w2popup.message({
+ width : options.width,
+ height : options.height,
+ html : '<div style="position: absolute; top: 0px; left: 0px; right: 0px; bottom: 40px; overflow: auto">' +
+ ' <div class="w2ui-centered" style="font-size: 13px;">' + options.msg + '</div>' +
+ '</div>' +
+ '<div style="position: absolute; bottom: 7px; left: 0px; right: 0px; text-align: center; padding: 5px">' +
+ ' <button id="Yes" class="w2ui-popup-btn btn '+ options.yes_class +'" style="'+ options.yes_style +'">' + w2utils.lang(options.yes_text) + '</button>' +
+ ' <button id="No" class="w2ui-popup-btn btn '+ options.no_class +'" style="'+ options.no_style +'">' + w2utils.lang(options.no_text) + '</button>' +
+ '</div>',
+ onOpen: function () {
+ $('#w2ui-popup .w2ui-popup-message .btn').on('click', function (event) {
+ w2popup.message();
+ if (typeof options.callBack == 'function') options.callBack(event.target.id);
+ if (event.target.id == 'Yes' && typeof options.yes_callBack == 'function') options.yes_callBack();
+ if (event.target.id == 'No' && typeof options.no_callBack == 'function') options.no_callBack();
+ });
+ },
+ onKeydown: function (event) {
+ switch (event.originalEvent.keyCode) {
+ case 13: // enter
+ if (typeof options.callBack == 'function') options.callBack('Yes');
+ if (typeof options.yes_callBack == 'function') options.yes_callBack();
+ w2popup.message();
+ break
+ case 27: // esc
+ if (typeof options.callBack == 'function') options.callBack('No');
+ if (typeof options.no_callBack == 'function') options.no_callBack();
+ w2popup.message();
+ break
+ }
+ }
+ });
+
+ } else {
+
+ if (!w2utils.isInt(options.height)) options.height = options.height + 50;
+ w2popup.open({
+ width : options.width,
+ height : options.height,
+ title : options.title,
+ modal : true,
+ showClose : false,
+ body : '<div class="w2ui-centered" style="font-size: 13px;">' + options.msg + '</div>',
+ buttons : '<button id="Yes" class="w2ui-popup-btn btn '+ options.yes_class +'" style="'+ options.yes_style +'">'+ w2utils.lang(options.yes_text) +'</button>'+
+ '<button id="No" class="w2ui-popup-btn btn '+ options.no_class +'" style="'+ options.no_style +'">'+ w2utils.lang(options.no_text) +'</button>',
+ onOpen: function (event) {
+ event.onComplete = function () {
+ $('#w2ui-popup .w2ui-popup-btn').on('click', function (event) {
+ w2popup.close();
+ if (typeof options.callBack == 'function') options.callBack(event.target.id);
+ if (event.target.id == 'Yes' && typeof options.yes_callBack == 'function') options.yes_callBack();
+ if (event.target.id == 'No' && typeof options.no_callBack == 'function') options.no_callBack();
+ });
+ }
+ },
+ onKeydown: function (event) {
+ switch (event.originalEvent.keyCode) {
+ case 13: // enter
+ if (typeof options.callBack == 'function') options.callBack('Yes');
+ if (typeof options.yes_callBack == 'function') options.yes_callBack();
+ w2popup.close();
+ break
+ case 27: // esc
+ if (typeof options.callBack == 'function') options.callBack('No');
+ if (typeof options.no_callBack == 'function') options.no_callBack();
+ w2popup.close();
+ break
+ }
+ }
+ });
+ }
+
+ return {
+ yes: function (fun) {
+ options.yes_callBack = fun;
+ return this;
+ },
+ no: function (fun) {
+ options.no_callBack = fun;
+ return this;
+ }
+ };
+};
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2tabs - tabs widget
+* - $().w2tabs - jQuery wrapper
+* - Dependencies: jQuery, w2utils
+*
+* == NICE TO HAVE ==
+* - on overflow display << >>
+*
+************************************************************************/
+
+(function () {
+ var w2tabs = function (options) {
+ this.box = null; // DOM Element that holds the element
+ this.name = null; // unique name for w2ui
+ this.active = null;
+ this.tabs = [];
+ this.routeData = {}; // data for dynamic routes
+ this.right = '';
+ this.style = '';
+ this.onClick = null;
+ this.onClose = null;
+ this.onRender = null;
+ this.onRefresh = null;
+ this.onResize = null;
+ this.onDestroy = null;
+
+ $.extend(this, { handlers: [] });
+ $.extend(true, this, w2obj.tabs, options);
+ };
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2tabs = function(method) {
+ if (typeof method === 'object' || !method ) {
+ // check name parameter
+ if (!w2utils.checkName(method, 'w2tabs')) return;
+ // extend tabs
+ var tabs = method.tabs || [];
+ var object = new w2tabs(method);
+ for (var i = 0; i < tabs.length; i++) {
+ object.tabs[i] = $.extend({}, w2tabs.prototype.tab, tabs[i]);
+ }
+ if ($(this).length !== 0) {
+ object.render($(this)[0]);
+ }
+ // register new object
+ w2ui[object.name] = object;
+ return object;
+ } else if (w2ui[$(this).attr('name')]) {
+ var obj = w2ui[$(this).attr('name')];
+ obj[method].apply(obj, Array.prototype.slice.call(arguments, 1));
+ return this;
+ } else {
+ console.log('ERROR: Method ' + method + ' does not exist on jQuery.w2tabs' );
+ return undefined;
+ }
+ };
+
+ // ====================================================
+ // -- Implementation of core functionality
+
+ w2tabs.prototype = {
+ tab : {
+ id : null, // command to be sent to all event handlers
+ text : '',
+ route : null,
+ hidden : false,
+ disabled : false,
+ closable : false,
+ hint : '',
+ onClick : null,
+ onRefresh : null,
+ onClose : null
+ },
+
+ add: function (tab) {
+ return this.insert(null, tab);
+ },
+
+ insert: function (id, tab) {
+ if (!$.isArray(tab)) tab = [tab];
+ // assume it is array
+ for (var i = 0; i < tab.length; i++) {
+ // checks
+ if (typeof tab[i].id === 'undefined') {
+ console.log('ERROR: The parameter "id" is required but not supplied. (obj: '+ this.name +')');
+ return;
+ }
+ if (!w2utils.checkUniqueId(tab[i].id, this.tabs, 'tabs', this.name)) return;
+ // add tab
+ var newTab = $.extend({}, w2tabs.prototype.tab, tab[i]);
+ if (id === null || typeof id === 'undefined') {
+ this.tabs.push(newTab);
+ } else {
+ var middle = this.get(id, true);
+ this.tabs = this.tabs.slice(0, middle).concat([newTab], this.tabs.slice(middle));
+ }
+ this.refresh(tab[i].id);
+ }
+ },
+
+ remove: function () {
+ var removed = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ var tab = this.get(arguments[a]);
+ if (!tab) return false;
+ removed++;
+ // remove from array
+ this.tabs.splice(this.get(tab.id, true), 1);
+ // remove from screen
+ $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(tab.id)).remove();
+ }
+ return removed;
+ },
+
+ select: function (id) {
+ if (this.active == id || this.get(id) === null) return false;
+ this.active = id;
+ this.refresh();
+ return true;
+ },
+
+ set: function (id, tab) {
+ var index = this.get(id, true);
+ if (index === null) return false;
+ $.extend(this.tabs[index], tab);
+ this.refresh(id);
+ return true;
+ },
+
+ get: function (id, returnIndex) {
+ if (arguments.length === 0) {
+ var all = [];
+ for (var i1 = 0; i1 < this.tabs.length; i1++) {
+ if (this.tabs[i1].id != null) {
+ all.push(this.tabs[i1].id);
+ }
+ }
+ return all;
+ } else {
+ for (var i2 = 0; i2 < this.tabs.length; i2++) {
+ if (this.tabs[i2].id == id) { // need to be == since id can be numeric
+ return (returnIndex === true ? i2 : this.tabs[i2]);
+ }
+ }
+ }
+ return null;
+ },
+
+ show: function () {
+ var obj = this;
+ var shown = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var tab = this.get(arguments[a]);
+ if (!tab || tab.hidden === false) continue;
+ shown++;
+ tab.hidden = false;
+ tmp.push(tab.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return shown;
+ },
+
+ hide: function () {
+ var obj = this;
+ var hidden= 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var tab = this.get(arguments[a]);
+ if (!tab || tab.hidden === true) continue;
+ hidden++;
+ tab.hidden = true;
+ tmp.push(tab.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return hidden;
+ },
+
+ enable: function () {
+ var obj = this;
+ var enabled = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var tab = this.get(arguments[a]);
+ if (!tab || tab.disabled === false) continue;
+ enabled++;
+ tab.disabled = false;
+ tmp.push(tab.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return enabled;
+ },
+
+ disable: function () {
+ var obj = this;
+ var disabled = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var tab = this.get(arguments[a]);
+ if (!tab || tab.disabled === true) continue;
+ disabled++;
+ tab.disabled = true;
+ tmp.push(tab.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return disabled;
+ },
+
+ refresh: function (id) {
+ var time = (new Date()).getTime();
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'refresh', target: (typeof id !== 'undefined' ? id : this.name), object: this.get(id) });
+ if (eventData.isCancelled === true) return;
+ if (typeof id === 'undefined') {
+ // refresh all
+ for (var i = 0; i < this.tabs.length; i++) this.refresh(this.tabs[i].id);
+ } else {
+ // create or refresh only one item
+ var tab = this.get(id);
+ if (tab === null) return false;
+ if (typeof tab.caption !== 'undefined') tab.text = tab.caption;
+
+ var jq_el = $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(tab.id));
+ var tabHTML = (tab.closable ? '<div class="w2ui-tab-close" onclick="w2ui[\''+ this.name +'\'].animateClose(\''+ tab.id +'\', event);"></div>' : '') +
+ ' <div class="w2ui-tab'+ (this.active === tab.id ? ' active' : '') + (tab.closable ? ' closable' : '') +'" '+
+ ' title="'+ (typeof tab.hint !== 'undefined' ? tab.hint : '') +'"'+
+ ' onclick="w2ui[\''+ this.name +'\'].click(\''+ tab.id +'\', event);">' + tab.text + '</div>';
+ if (jq_el.length === 0) {
+ // does not exist - create it
+ var addStyle = '';
+ if (tab.hidden) { addStyle += 'display: none;'; }
+ if (tab.disabled) { addStyle += 'opacity: 0.2; -moz-opacity: 0.2; -webkit-opacity: 0.2; -o-opacity: 0.2; filter:alpha(opacity=20);'; }
+ var html = '<td id="tabs_'+ this.name + '_tab_'+ tab.id +'" style="'+ addStyle +'" valign="middle">'+ tabHTML + '</td>';
+ if (this.get(id, true) !== this.tabs.length-1 && $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(this.tabs[parseInt(this.get(id, true))+1].id)).length > 0) {
+ $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(this.tabs[parseInt(this.get(id, true))+1].id)).before(html);
+ } else {
+ $(this.box).find('#tabs_'+ this.name +'_right').before(html);
+ }
+ } else {
+ // refresh
+ jq_el.html(tabHTML);
+ if (tab.hidden) { jq_el.css('display', 'none'); }
+ else { jq_el.css('display', ''); }
+ if (tab.disabled) { jq_el.css({ 'opacity': '0.2', '-moz-opacity': '0.2', '-webkit-opacity': '0.2', '-o-opacity': '0.2', 'filter': 'alpha(opacity=20)' }); }
+ else { jq_el.css({ 'opacity': '1', '-moz-opacity': '1', '-webkit-opacity': '1', '-o-opacity': '1', 'filter': 'alpha(opacity=100)' }); }
+ }
+ }
+ // right html
+ $('#tabs_'+ this.name +'_right').html(this.right);
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ render: function (box) {
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'render', target: this.name, box: box });
+ if (eventData.isCancelled === true) return;
+ // default action
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ if (typeof box !== 'undefined' && box !== null) {
+ if ($(this.box).find('> table #tabs_'+ this.name + '_right').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-tabs')
+ .html('');
+ }
+ this.box = box;
+ }
+ if (!this.box) return false;
+ // render all buttons
+ var html = '<table cellspacing="0" cellpadding="1" width="100%">'+
+ ' <tr><td width="100%" id="tabs_'+ this.name +'_right" align="right">'+ this.right +'</td></tr>'+
+ '</table>';
+ $(this.box)
+ .attr('name', this.name)
+ .addClass('w2ui-reset w2ui-tabs')
+ .html(html);
+ if ($(this.box).length > 0) $(this.box)[0].style.cssText += this.style;
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ this.refresh();
+ return (new Date()).getTime() - time;
+ },
+
+ resize: function () {
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'resize', target: this.name });
+ if (eventData.isCancelled === true) return;
+
+ // intentionaly blank
+
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ destroy: function () {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'destroy', target: this.name });
+ if (eventData.isCancelled === true) return;
+ // clean up
+ if ($(this.box).find('> table #tabs_'+ this.name + '_right').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-tabs')
+ .html('');
+ }
+ delete w2ui[this.name];
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ // ===================================================
+ // -- Internal Event Handlers
+
+ click: function (id, event) {
+ var tab = this.get(id);
+ if (tab === null || tab.disabled) return false;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'click', target: id, tab: tab, object: tab, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(this.active) +' .w2ui-tab').removeClass('active');
+ this.active = tab.id;
+ // route processing
+ if (tab.route) {
+ var route = String('/'+ tab.route).replace(/\/{2,}/g, '/');
+ var info = w2utils.parseRoute(route);
+ if (info.keys.length > 0) {
+ for (var k = 0; k < info.keys.length; k++) {
+ if (this.routeData[info.keys[k].name] == null) continue;
+ route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name]);
+ }
+ }
+ setTimeout(function () { window.location.hash = route; }, 1);
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ this.refresh(id);
+ },
+
+ animateClose: function(id, event) {
+ var tab = this.get(id);
+ if (tab === null || tab.disabled) return false;
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'close', target: id, object: this.get(id), originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default action
+ var obj = this;
+ $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(tab.id)).css({
+ '-webkit-transition': '.2s',
+ '-moz-transition': '2s',
+ '-ms-transition': '.2s',
+ '-o-transition': '.2s',
+ opacity: '0' });
+ setTimeout(function () {
+ var width = $(obj.box).find('#tabs_'+ obj.name +'_tab_'+ w2utils.escapeId(tab.id)).width();
+ $(obj.box).find('#tabs_'+ obj.name +'_tab_'+ w2utils.escapeId(tab.id))
+ .html('<div style="width: '+ width +'px; -webkit-transition: .2s; -moz-transition: .2s; -ms-transition: .2s; -o-transition: .2s"></div>');
+ setTimeout(function () {
+ $(obj.box).find('#tabs_'+ obj.name +'_tab_'+ w2utils.escapeId(tab.id)).find(':first-child').css({ 'width': '0px' });
+ }, 50);
+ }, 200);
+ setTimeout(function () {
+ obj.remove(id);
+ }, 450);
+ // event before
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ this.refresh();
+ },
+
+ animateInsert: function(id, tab) {
+ if (this.get(id) === null) return;
+ if (!$.isPlainObject(tab)) return;
+ // check for unique
+ if (!w2utils.checkUniqueId(tab.id, this.tabs, 'tabs', this.name)) return;
+ // insert simple div
+ var jq_el = $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(tab.id));
+ if (jq_el.length !== 0) return; // already exists
+ // measure width
+ if (typeof tab.caption !== 'undefined') tab.text = tab.caption;
+ var tmp = '<div id="_tmp_tabs" class="w2ui-reset w2ui-tabs" style="position: absolute; top: -1000px;">'+
+ '<table cellspacing="0" cellpadding="1" width="100%"><tr>'+
+ '<td id="_tmp_simple_tab" style="" valign="middle">'+
+ (tab.closable ? '<div class="w2ui-tab-close"></div>' : '') +
+ ' <div class="w2ui-tab '+ (this.active === tab.id ? 'active' : '') +'">'+ tab.text +'</div>'+
+ '</td></tr></table>'+
+ '</div>';
+ $('body').append(tmp);
+ // create dummy element
+ var tabHTML = '<div style="width: 1px; -webkit-transition: 0.2s; -moz-transition: 0.2s; -ms-transition: 0.2s; -o-transition: 0.2s;">&nbsp;</div>';
+ var addStyle = '';
+ if (tab.hidden) { addStyle += 'display: none;'; }
+ if (tab.disabled) { addStyle += 'opacity: 0.2; -moz-opacity: 0.2; -webkit-opacity: 0.2; -o-opacity: 0.2; filter:alpha(opacity=20);'; }
+ var html = '<td id="tabs_'+ this.name +'_tab_'+ tab.id +'" style="'+ addStyle +'" valign="middle">'+ tabHTML +'</td>';
+ if (this.get(id, true) !== this.tabs.length && $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(this.tabs[parseInt(this.get(id, true))].id)).length > 0) {
+ $(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(this.tabs[parseInt(this.get(id, true))].id)).before(html);
+ } else {
+ $(this.box).find('#tabs_'+ this.name +'_right').before(html);
+ }
+ // -- move
+ var obj = this;
+ setTimeout(function () {
+ var width = $('#_tmp_simple_tab').width();
+ $('#_tmp_tabs').remove();
+ $('#tabs_'+ obj.name +'_tab_'+ w2utils.escapeId(tab.id) +' > div').css('width', width+'px');
+ }, 1);
+ setTimeout(function () {
+ // insert for real
+ obj.insert(id, tab);
+ }, 200);
+ }
+ };
+
+ $.extend(w2tabs.prototype, w2utils.event);
+ w2obj.tabs = w2tabs;
+})();
+
+
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2toolbar - toolbar widget
+* - $().w2toolbar - jQuery wrapper
+* - Dependencies: jQuery, w2utils
+*
+* == NICE TO HAVE ==
+* - on overflow display << >>
+* - verticle toolbar
+*
+************************************************************************/
+
+(function () {
+ var w2toolbar = function (options) {
+ this.box = null; // DOM Element that holds the element
+ this.name = null; // unique name for w2ui
+ this.routeData = {}; // data for dynamic routes
+ this.items = [];
+ this.right = ''; // HTML text on the right of toolbar
+ this.onClick = null;
+ this.onRender = null;
+ this.onRefresh = null;
+ this.onResize = null;
+ this.onDestroy = null;
+
+ $.extend(true, this, w2obj.toolbar, options);
+ };
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2toolbar = function(method) {
+ if (typeof method === 'object' || !method ) {
+ // check name parameter
+ if (!w2utils.checkName(method, 'w2toolbar')) return;
+ // extend items
+ var items = method.items || [];
+ var object = new w2toolbar(method);
+ $.extend(object, { items: [], handlers: [] });
+ for (var i = 0; i < items.length; i++) {
+ object.items[i] = $.extend({}, w2toolbar.prototype.item, items[i]);
+ }
+ if ($(this).length !== 0) {
+ object.render($(this)[0]);
+ }
+ // register new object
+ w2ui[object.name] = object;
+ return object;
+
+ } else if (w2ui[$(this).attr('name')]) {
+ var obj = w2ui[$(this).attr('name')];
+ obj[method].apply(obj, Array.prototype.slice.call(arguments, 1));
+ return this;
+ } else {
+ console.log('ERROR: Method ' + method + ' does not exist on jQuery.w2toolbar' );
+ }
+ };
+
+ // ====================================================
+ // -- Implementation of core functionality
+
+ w2toolbar.prototype = {
+ item: {
+ id : null, // command to be sent to all event handlers
+ type : 'button', // button, check, radio, drop, menu, break, html, spacer
+ text : '',
+ route : null, // if not null, it is route to go
+ html : '',
+ img : null,
+ icon : null,
+ count : null,
+ hidden : false,
+ disabled : false,
+ checked : false, // used for radio buttons
+ arrow : true, // arrow down for drop/menu types
+ hint : '',
+ group : null, // used for radio buttons
+ items : null, // for type menu it is an array of items in the menu
+ overlay : {},
+ onClick : null
+ },
+
+ add: function (items) {
+ this.insert(null, items);
+ },
+
+ insert: function (id, items) {
+ if (!$.isArray(items)) items = [items];
+ for (var o = 0; o < items.length; o++) {
+ // checks
+ if (typeof items[o].type === 'undefined') {
+ console.log('ERROR: The parameter "type" is required but not supplied in w2toolbar.add() method.');
+ return;
+ }
+ if ($.inArray(String(items[o].type), ['button', 'check', 'radio', 'drop', 'menu', 'break', 'html', 'spacer']) === -1) {
+ console.log('ERROR: The parameter "type" should be one of the following [button, check, radio, drop, menu, break, html, spacer] '+
+ 'in w2toolbar.add() method.');
+ return;
+ }
+ if (typeof items[o].id === 'undefined') {
+ console.log('ERROR: The parameter "id" is required but not supplied in w2toolbar.add() method.');
+ return;
+ }
+ if (!w2utils.checkUniqueId(items[o].id, this.items, 'toolbar items', this.name)) return;
+ // add item
+ var it = $.extend({}, w2toolbar.prototype.item, items[o]);
+ if (id == null) {
+ this.items.push(it);
+ } else {
+ var middle = this.get(id, true);
+ this.items = this.items.slice(0, middle).concat([it], this.items.slice(middle));
+ }
+ this.refresh(it.id);
+ }
+ },
+
+ remove: function () {
+ var removed = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ var it = this.get(arguments[a]);
+ if (!it) continue;
+ removed++;
+ // remove from screen
+ $(this.box).find('#tb_'+ this.name +'_item_'+ w2utils.escapeId(it.id)).remove();
+ // remove from array
+ var ind = this.get(it.id, true);
+ if (ind) this.items.splice(ind, 1);
+ }
+ return removed;
+ },
+
+ set: function (id, item) {
+ var index = this.get(id, true);
+ if (index === null) return false;
+ $.extend(this.items[index], item);
+ this.refresh(id);
+ return true;
+ },
+
+ get: function (id, returnIndex) {
+ if (arguments.length === 0) {
+ var all = [];
+ for (var i1 = 0; i1 < this.items.length; i1++) if (this.items[i1].id !== null) all.push(this.items[i1].id);
+ return all;
+ }
+ for (var i2 = 0; i2 < this.items.length; i2++) {
+ if (this.items[i2].id === id) {
+ if (returnIndex === true) return i2; else return this.items[i2];
+ }
+ }
+ return null;
+ },
+
+ show: function () {
+ var obj = this;
+ var items = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var it = this.get(arguments[a]);
+ if (!it) continue;
+ items++;
+ it.hidden = false;
+ tmp.push(it.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return items;
+ },
+
+ hide: function () {
+ var obj = this;
+ var items = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var it = this.get(arguments[a]);
+ if (!it) continue;
+ items++;
+ it.hidden = true;
+ tmp.push(it.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return items;
+ },
+
+ enable: function () {
+ var obj = this;
+ var items = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var it = this.get(arguments[a]);
+ if (!it) continue;
+ items++;
+ it.disabled = false;
+ tmp.push(it.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return items;
+ },
+
+ disable: function () {
+ var obj = this;
+ var items = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var it = this.get(arguments[a]);
+ if (!it) continue;
+ items++;
+ it.disabled = true;
+ tmp.push(it.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return items;
+ },
+
+ check: function () {
+ var obj = this;
+ var items = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var it = this.get(arguments[a]);
+ if (!it) continue;
+ items++;
+ it.checked = true;
+ tmp.push(it.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return items;
+ },
+
+ uncheck: function () {
+ var obj = this;
+ var items = 0;
+ var tmp = [];
+ for (var a = 0; a < arguments.length; a++) {
+ var it = this.get(arguments[a]);
+ if (!it) continue;
+ items++;
+ it.checked = false;
+ tmp.push(it.id);
+ }
+ setTimeout(function () { for (var t in tmp) obj.refresh(tmp[t]); }, 15); // needs timeout
+ return items;
+ },
+
+ render: function (box) {
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'render', target: this.name, box: box });
+ if (eventData.isCancelled === true) return;
+
+ if (box != null) {
+ if ($(this.box).find('> table #tb_'+ this.name + '_right').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-toolbar')
+ .html('');
+ }
+ this.box = box;
+ }
+ if (!this.box) return;
+ // render all buttons
+ var html = '<table cellspacing="0" cellpadding="0" width="100%">'+
+ '<tr>';
+ for (var i = 0; i < this.items.length; i++) {
+ var it = this.items[i];
+ if (it.id == null) it.id = "item_" + i;
+ if (it === null) continue;
+ if (it.type === 'spacer') {
+ html += '<td width="100%" id="tb_'+ this.name +'_item_'+ it.id +'" align="right"></td>';
+ } else {
+ html += '<td id="tb_'+ this.name + '_item_'+ it.id +'" style="'+ (it.hidden ? 'display: none' : '') +'" '+
+ ' class="'+ (it.disabled ? 'disabled' : '') +'" valign="middle">'+ this.getItemHTML(it) +
+ '</td>';
+ }
+ }
+ html += '<td width="100%" id="tb_'+ this.name +'_right" align="right">'+ this.right +'</td>';
+ html += '</tr>'+
+ '</table>';
+ $(this.box)
+ .attr('name', this.name)
+ .addClass('w2ui-reset w2ui-toolbar')
+ .html(html);
+ if ($(this.box).length > 0) $(this.box)[0].style.cssText += this.style;
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ refresh: function (id) {
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'refresh', target: (typeof id !== 'undefined' ? id : this.name), item: this.get(id) });
+ if (eventData.isCancelled === true) return;
+
+ if (id == null) {
+ // refresh all
+ for (var i = 0; i < this.items.length; i++) {
+ var it1 = this.items[i];
+ if (it1.id == null) it1.id = "item_" + i;
+ this.refresh(it1.id);
+ }
+ }
+ // create or refresh only one item
+ var it = this.get(id);
+ if (it === null) return false;
+
+ var el = $(this.box).find('#tb_'+ this.name +'_item_'+ w2utils.escapeId(it.id));
+ var html = this.getItemHTML(it);
+ if (el.length === 0) {
+ // does not exist - create it
+ if (it.type === 'spacer') {
+ html = '<td width="100%" id="tb_'+ this.name +'_item_'+ it.id +'" align="right"></td>';
+ } else {
+ html = '<td id="tb_'+ this.name + '_item_'+ it.id +'" style="'+ (it.hidden ? 'display: none' : '') +'" '+
+ ' class="'+ (it.disabled ? 'disabled' : '') +'" valign="middle">'+ html +
+ '</td>';
+ }
+ if (this.get(id, true) === this.items.length-1) {
+ $(this.box).find('#tb_'+ this.name +'_right').before(html);
+ } else {
+ $(this.box).find('#tb_'+ this.name +'_item_'+ w2utils.escapeId(this.items[parseInt(this.get(id, true))+1].id)).before(html);
+ }
+ } else {
+ // refresh
+ el.html(html);
+ if (it.hidden) { el.css('display', 'none'); } else { el.css('display', ''); }
+ if (it.disabled) { el.addClass('disabled'); } else { el.removeClass('disabled'); }
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ resize: function () {
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'resize', target: this.name });
+ if (eventData.isCancelled === true) return;
+
+ // intentionaly blank
+
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ destroy: function () {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'destroy', target: this.name });
+ if (eventData.isCancelled === true) return;
+ // clean up
+ if ($(this.box).find('> table #tb_'+ this.name + '_right').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-toolbar')
+ .html('');
+ }
+ $(this.box).html('');
+ delete w2ui[this.name];
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ // ========================================
+ // --- Internal Functions
+
+ getItemHTML: function (item) {
+ var html = '';
+
+ if (typeof item.caption !== 'undefined') item.text = item.caption;
+ if (typeof item.hint === 'undefined') item.hint = '';
+ if (typeof item.text === 'undefined') item.text = '';
+
+ switch (item.type) {
+ case 'menu':
+ case 'button':
+ case 'check':
+ case 'radio':
+ case 'drop':
+ var img = '<td>&nbsp;</td>';
+ if (item.img) img = '<td><div class="w2ui-tb-image w2ui-icon '+ item.img +'"></div></td>';
+ if (item.icon) img = '<td><div class="w2ui-tb-image"><span class="'+ item.icon +'"></span></div></td>';
+ html += '<table cellpadding="0" cellspacing="0" title="'+ item.hint +'" class="w2ui-button '+ (item.checked ? 'checked' : '') +'" '+
+ ' onclick = "var el=w2ui[\''+ this.name + '\']; if (el) el.click(\''+ item.id +'\', event);" '+
+ ' onmouseover = "' + (!item.disabled ? "$(this).addClass('over');" : "") + '"'+
+ ' onmouseout = "' + (!item.disabled ? "$(this).removeClass('over').removeClass('down');" : "") + '"'+
+ ' onmousedown = "' + (!item.disabled ? "$(this).addClass('down');" : "") + '"'+
+ ' onmouseup = "' + (!item.disabled ? "$(this).removeClass('down');" : "") + '"'+
+ '>'+
+ '<tr><td>'+
+ ' <table cellpadding="1" cellspacing="0">'+
+ ' <tr>' +
+ img +
+ (item.text !== '' ? '<td class="w2ui-tb-caption" nowrap>'+ item.text +'</td>' : '') +
+ (item.count != null ? '<td class="w2ui-tb-count" nowrap><span>'+ item.count +'</span></td>' : '') +
+ (((item.type === 'drop' || item.type === 'menu') && item.arrow !== false) ?
+ '<td class="w2ui-tb-down" nowrap><div></div></td>' : '') +
+ ' </tr></table>'+
+ '</td></tr></table>';
+ break;
+
+ case 'break':
+ html += '<table cellpadding="0" cellspacing="0"><tr>'+
+ ' <td><div class="w2ui-break">&nbsp;</div></td>'+
+ '</tr></table>';
+ break;
+
+ case 'html':
+ html += '<table cellpadding="0" cellspacing="0"><tr>'+
+ ' <td nowrap>' + item.html + '</td>'+
+ '</tr></table>';
+ break;
+ }
+
+ var newHTML = '';
+ if (typeof item.onRender === 'function') newHTML = item.onRender.call(this, item.id, html);
+ if (typeof this.onRender === 'function') newHTML = this.onRender(item.id, html);
+ if (newHTML !== '' && newHTML != null) html = newHTML;
+ return html;
+ },
+
+ menuClick: function (event) {
+ var obj = this;
+ if (event.item && !event.item.disabled) {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'click', target: event.item.id + ':' + event.subItem.id, item: event.item,
+ subItem: event.subItem, originalEvent: event.originalEvent });
+ if (eventData.isCancelled === true) return;
+
+ // route processing
+ var it = event.subItem;
+ if (it.route) {
+ var route = String('/'+ it.route).replace(/\/{2,}/g, '/');
+ var info = w2utils.parseRoute(route);
+ if (info.keys.length > 0) {
+ for (var k = 0; k < info.keys.length; k++) {
+ if (obj.routeData[info.keys[k].name] == null) continue;
+ route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name]);
+ }
+ }
+ setTimeout(function () { window.location.hash = route; }, 1);
+ }
+
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ },
+
+ click: function (id, event) {
+ var obj = this;
+ var it = this.get(id);
+ if (it && !it.disabled) {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'click', target: (typeof id !== 'undefined' ? id : this.name),
+ item: it, object: it, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+
+ var btn = $('#tb_'+ this.name +'_item_'+ w2utils.escapeId(it.id) +' table.w2ui-button');
+ btn.removeClass('down');
+
+ if (it.type === 'radio') {
+ for (var i = 0; i < this.items.length; i++) {
+ var itt = this.items[i];
+ if (itt == null || itt.id === it.id || itt.type !== 'radio') continue;
+ if (itt.group === it.group && itt.checked) {
+ itt.checked = false;
+ this.refresh(itt.id);
+ }
+ }
+ it.checked = true;
+ btn.addClass('checked');
+ }
+
+ if (it.type === 'drop' || it.type === 'menu') {
+ if (it.checked) {
+ // if it was already checked, second click will hide it
+ it.checked = false;
+ } else {
+ // show overlay
+ setTimeout(function () {
+ var el = $('#tb_'+ obj.name +'_item_'+ w2utils.escapeId(it.id));
+ if (!$.isPlainObject(it.overlay)) it.overlay = {};
+ var left = (el.width() - 50) / 2;
+ if (left > 19) left = 19;
+ if (it.type === 'drop') {
+ el.w2overlay(it.html, $.extend({ left: left, top: 3 }, it.overlay));
+ }
+ if (it.type === 'menu') {
+ el.w2menu(it.items, $.extend({ left: left, top: 3 }, it.overlay, {
+ select: function (event) {
+ obj.menuClick({ item: it, subItem: event.item, originalEvent: event.originalEvent });
+ hideDrop();
+ }
+ }));
+ }
+ // window.click to hide it
+ $(document).on('click', hideDrop);
+ function hideDrop() {
+ $(document).off('click', hideDrop);
+ it.checked = false;
+ btn.removeClass('checked');
+ }
+ }, 1);
+ }
+ }
+
+ if (it.type === 'check' || it.type === 'drop' || it.type === 'menu') {
+ it.checked = !it.checked;
+ if (it.checked) {
+ btn.addClass('checked');
+ } else {
+ btn.removeClass('checked');
+ }
+ }
+ // route processing
+ if (it.route) {
+ var route = String('/'+ it.route).replace(/\/{2,}/g, '/');
+ var info = w2utils.parseRoute(route);
+ if (info.keys.length > 0) {
+ for (var k = 0; k < info.keys.length; k++) {
+ route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name]);
+ }
+ }
+ setTimeout(function () { window.location.hash = route; }, 1);
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ }
+ };
+
+ $.extend(w2toolbar.prototype, w2utils.event);
+ w2obj.toolbar = w2toolbar;
+})();
+
+
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2sidebar - sidebar widget
+* - $().w2sidebar - jQuery wrapper
+* - Dependencies: jQuery, w2utils
+*
+* == NICE TO HAVE ==
+* - return ids of all subitems
+* - add find() method to find nodes by a specific criteria (I want all nodes for exampe)
+* - dbl click should be like it is in grid (with timer not HTML dbl click event)
+* - reorder with grag and drop
+* - add route property that would navigate to a #route
+* - node.style is missleading - should be there to apply color for example
+*
+************************************************************************/
+
+(function () {
+ var w2sidebar = function (options) {
+ this.name = null;
+ this.box = null;
+ this.sidebar = null;
+ this.parent = null;
+ this.nodes = []; // Sidebar child nodes
+ this.menu = [];
+ this.routeData = {}; // data for dynamic routes
+ this.selected = null; // current selected node (readonly)
+ this.img = null;
+ this.icon = null;
+ this.style = '';
+ this.topHTML = '';
+ this.bottomHTML = '';
+ this.keyboard = true;
+ this.onClick = null; // Fire when user click on Node Text
+ this.onDblClick = null; // Fire when user dbl clicks
+ this.onContextMenu = null;
+ this.onMenuClick = null; // when context menu item selected
+ this.onExpand = null; // Fire when node Expands
+ this.onCollapse = null; // Fire when node Colapses
+ this.onKeydown = null;
+ this.onRender = null;
+ this.onRefresh = null;
+ this.onResize = null;
+ this.onDestroy = null;
+
+ $.extend(true, this, w2obj.sidebar, options);
+ };
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2sidebar = function(method) {
+ if (typeof method === 'object' || !method ) {
+ // check name parameter
+ if (!w2utils.checkName(method, 'w2sidebar')) return;
+ // extend items
+ var nodes = method.nodes;
+ var object = new w2sidebar(method);
+ $.extend(object, { handlers: [], nodes: [] });
+ if (typeof nodes != 'undefined') {
+ object.add(object, nodes);
+ }
+ if ($(this).length !== 0) {
+ object.render($(this)[0]);
+ }
+ object.sidebar = object;
+ // register new object
+ w2ui[object.name] = object;
+ return object;
+
+ } else if (w2ui[$(this).attr('name')]) {
+ var obj = w2ui[$(this).attr('name')];
+ obj[method].apply(obj, Array.prototype.slice.call(arguments, 1));
+ return this;
+ } else {
+ console.log('ERROR: Method ' + method + ' does not exist on jQuery.w2sidebar' );
+ }
+ };
+
+ // ====================================================
+ // -- Implementation of core functionality
+
+ w2sidebar.prototype = {
+
+ node: {
+ id : null,
+ text : '',
+ count : null,
+ img : null,
+ icon : null,
+ nodes : [],
+ style : '', // additional style for subitems
+ route : null,
+ selected : false,
+ expanded : false,
+ hidden : false,
+ disabled : false,
+ group : false, // if true, it will build as a group
+ groupShowHide : true,
+ plus : false, // if true, plus will be shown even if there is no sub nodes
+ // events
+ onClick : null,
+ onDblClick : null,
+ onContextMenu : null,
+ onExpand : null,
+ onCollapse : null,
+ // internal
+ parent : null, // node object
+ sidebar : null
+ },
+
+ add: function (parent, nodes) {
+ if (arguments.length == 1) {
+ // need to be in reverse order
+ nodes = arguments[0];
+ parent = this;
+ }
+ if (typeof parent == 'string') parent = this.get(parent);
+ return this.insert(parent, null, nodes);
+ },
+
+ insert: function (parent, before, nodes) {
+ var txt, ind, tmp, node, nd;
+ if (arguments.length == 2) {
+ // need to be in reverse order
+ nodes = arguments[1];
+ before = arguments[0];
+ ind = this.get(before);
+ if (ind === null) {
+ if (!$.isArray(nodes)) nodes = [nodes];
+ txt = (nodes[0].caption != null ? nodes[0].caption : nodes[0].text);
+ console.log('ERROR: Cannot insert node "'+ txt +'" because cannot find node "'+ before +'" to insert before.');
+ return null;
+ }
+ parent = this.get(before).parent;
+ }
+ if (typeof parent == 'string') parent = this.get(parent);
+ if (!$.isArray(nodes)) nodes = [nodes];
+ for (var o in nodes) {
+ node = nodes[o];
+ if (typeof node.id == null) {
+ txt = (node.caption != null ? node.caption : node.text);
+ console.log('ERROR: Cannot insert node "'+ txt +'" because it has no id.');
+ continue;
+ }
+ if (this.get(this, node.id) !== null) {
+ txt = (node.caption != null ? node.caption : node.text);
+ console.log('ERROR: Cannot insert node with id='+ node.id +' (text: '+ txt + ') because another node with the same id already exists.');
+ continue;
+ }
+ tmp = $.extend({}, w2sidebar.prototype.node, node);
+ tmp.sidebar = this;
+ tmp.parent = parent;
+ nd = tmp.nodes || [];
+ tmp.nodes = []; // very important to re-init empty nodes array
+ if (before === null) { // append to the end
+ parent.nodes.push(tmp);
+ } else {
+ ind = this.get(parent, before, true);
+ if (ind === null) {
+ txt = (node.caption != null ? node.caption : node.text);
+ console.log('ERROR: Cannot insert node "'+ txt +'" because cannot find node "'+ before +'" to insert before.');
+ return null;
+ }
+ parent.nodes.splice(ind, 0, tmp);
+ }
+ if (nd.length > 0) {
+ this.insert(tmp, null, nd);
+ }
+ }
+ this.refresh(parent.id);
+ return tmp;
+ },
+
+ remove: function () { // multiple arguments
+ var deleted = 0;
+ var tmp;
+ for (var a = 0; a < arguments.length; a++) {
+ tmp = this.get(arguments[a]);
+ if (tmp === null) continue;
+ if (this.selected !== null && this.selected === tmp.id) {
+ this.selected = null;
+ }
+ var ind = this.get(tmp.parent, arguments[a], true);
+ if (ind === null) continue;
+ if (tmp.parent.nodes[ind].selected) tmp.sidebar.unselect(tmp.id);
+ tmp.parent.nodes.splice(ind, 1);
+ deleted++;
+ }
+ if (deleted > 0 && arguments.length == 1) this.refresh(tmp.parent.id); else this.refresh();
+ return deleted;
+ },
+
+ set: function (parent, id, node) {
+ if (arguments.length == 2) {
+ // need to be in reverse order
+ node = id;
+ id = parent;
+ parent = this;
+ }
+ // searches all nested nodes
+ if (typeof parent == 'string') parent = this.get(parent);
+ if (parent.nodes == null) return null;
+ for (var i = 0; i < parent.nodes.length; i++) {
+ if (parent.nodes[i].id === id) {
+ // make sure nodes inserted correctly
+ var nodes = node.nodes;
+ $.extend(parent.nodes[i], node, { nodes: [] });
+ if (nodes != null) {
+ this.add(parent.nodes[i], nodes);
+ }
+ this.refresh(id);
+ return true;
+ } else {
+ var rv = this.set(parent.nodes[i], id, node);
+ if (rv) return true;
+ }
+ }
+ return false;
+ },
+
+ get: function (parent, id, returnIndex) { // can be just called get(id) or get(id, true)
+ if (arguments.length === 0) {
+ var all = [];
+ var tmp = this.find({});
+ for (var t = 0; t < tmp.length; t++) {
+ if (tmp[t].id != null) all.push(tmp[t].id);
+ }
+ return all;
+ } else {
+ if (arguments.length == 1 || (arguments.length == 2 && id === true) ) {
+ // need to be in reverse order
+ returnIndex = id;
+ id = parent;
+ parent = this;
+ }
+ // searches all nested nodes
+ if (typeof parent == 'string') parent = this.get(parent);
+ if (parent.nodes == null) return null;
+ for (var i = 0; i < parent.nodes.length; i++) {
+ if (parent.nodes[i].id == id) {
+ if (returnIndex === true) return i; else return parent.nodes[i];
+ } else {
+ var rv = this.get(parent.nodes[i], id, returnIndex);
+ if (rv || rv === 0) return rv;
+ }
+ }
+ return null;
+ }
+ },
+
+ find: function (parent, params, results) { // can be just called find({ selected: true })
+ if (arguments.length == 1) {
+ // need to be in reverse order
+ params = parent;
+ parent = this;
+ }
+ if (!results) results = [];
+ // searches all nested nodes
+ if (typeof parent == 'string') parent = this.get(parent);
+ if (parent.nodes == null) return results;
+ for (var i = 0; i < parent.nodes.length; i++) {
+ var match = true;
+ for (var prop in params) {
+ if (parent.nodes[i][prop] != params[prop]) match = false;
+ }
+ if (match) results.push(parent.nodes[i]);
+ if (parent.nodes[i].nodes.length > 0) results = this.find(parent.nodes[i], params, results);
+ }
+ return results;
+ },
+
+ hide: function () { // multiple arguments
+ var hidden = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ var tmp = this.get(arguments[a]);
+ if (tmp === null) continue;
+ tmp.hidden = true;
+ hidden++;
+ }
+ if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh();
+ return hidden;
+ },
+
+ show: function () { // multiple arguments
+ var shown = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ var tmp = this.get(arguments[a]);
+ if (tmp === null) continue;
+ tmp.hidden = false;
+ shown++;
+ }
+ if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh();
+ return shown;
+ },
+
+ disable: function () { // multiple arguments
+ var disabled = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ var tmp = this.get(arguments[a]);
+ if (tmp === null) continue;
+ tmp.disabled = true;
+ if (tmp.selected) this.unselect(tmp.id);
+ disabled++;
+ }
+ if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh();
+ return disabled;
+ },
+
+ enable: function () { // multiple arguments
+ var enabled = 0;
+ for (var a = 0; a < arguments.length; a++) {
+ var tmp = this.get(arguments[a]);
+ if (tmp === null) continue;
+ tmp.disabled = false;
+ enabled++;
+ }
+ if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh();
+ return enabled;
+ },
+
+ select: function (id) {
+ var new_node = this.get(id);
+ if (!new_node) return false;
+ if (this.selected == id && new_node.selected) return false;
+ this.unselect(this.selected);
+ $(this.box).find('#node_'+ w2utils.escapeId(id))
+ .addClass('w2ui-selected')
+ .find('.w2ui-icon').addClass('w2ui-icon-selected');
+ new_node.selected = true;
+ this.selected = id;
+ return true;
+ },
+
+ unselect: function (id) {
+ var current = this.get(id);
+ if (!current) return false;
+ current.selected = false;
+ $(this.box).find('#node_'+ w2utils.escapeId(id))
+ .removeClass('w2ui-selected')
+ .find('.w2ui-icon').removeClass('w2ui-icon-selected');
+ if (this.selected == id) this.selected = null;
+ return true;
+ },
+
+ toggle: function(id) {
+ var nd = this.get(id);
+ if (nd === null) return false;
+ if (nd.plus) {
+ this.set(id, { plus: false });
+ this.expand(id);
+ this.refresh(id);
+ return;
+ }
+ if (nd.nodes.length === 0) return false;
+ if (this.get(id).expanded) return this.collapse(id); else return this.expand(id);
+ },
+
+ collapse: function (id) {
+ var obj = this;
+ var nd = this.get(id);
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'collapse', target: id, object: nd });
+ if (eventData.isCancelled === true) return;
+ // default action
+ $(this.box).find('#node_'+ w2utils.escapeId(id) +'_sub').slideUp(200);
+ $(this.box).find('#node_'+ w2utils.escapeId(id) +' .w2ui-node-dots:first-child').html('<div class="w2ui-expand">+</div>');
+ nd.expanded = false;
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ setTimeout(function () { obj.refresh(id); }, 200);
+ return true;
+ },
+
+ collapseAll: function (parent) {
+ if (typeof parent == 'undefined') parent = this;
+ if (typeof parent == 'string') parent = this.get(parent);
+ if (parent.nodes == null) return false;
+ for (var i = 0; i < parent.nodes.length; i++) {
+ if (parent.nodes[i].expanded === true) parent.nodes[i].expanded = false;
+ if (parent.nodes[i].nodes && parent.nodes[i].nodes.length > 0) this.collapseAll(parent.nodes[i]);
+ }
+ this.refresh(parent.id);
+ return true;
+ },
+
+ expand: function (id) {
+ var obj = this;
+ var nd = this.get(id);
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'expand', target: id, object: nd });
+ if (eventData.isCancelled === true) return;
+ // default action
+ $(this.box).find('#node_'+ w2utils.escapeId(id) +'_sub').slideDown(200);
+ $(this.box).find('#node_'+ w2utils.escapeId(id) +' .w2ui-node-dots:first-child').html('<div class="w2ui-expand">-</div>');
+ nd.expanded = true;
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ setTimeout(function () { obj.refresh(id); }, 200);
+ return true;
+ },
+
+ expandAll: function (parent) {
+ if (typeof parent == 'undefined') parent = this;
+ if (typeof parent == 'string') parent = this.get(parent);
+ if (parent.nodes == null) return false;
+ for (var i = 0; i < parent.nodes.length; i++) {
+ if (parent.nodes[i].expanded === false) parent.nodes[i].expanded = true;
+ if (parent.nodes[i].nodes && parent.nodes[i].nodes.length > 0) this.collapseAll(parent.nodes[i]);
+ }
+ this.refresh(parent.id);
+ },
+
+ expandParents: function (id) {
+ var node = this.get(id);
+ if (node === null) return false;
+ if (node.parent) {
+ node.parent.expanded = true;
+ this.expandParents(node.parent.id);
+ }
+ this.refresh(id);
+ return true;
+ },
+
+ click: function (id, event) {
+ var obj = this;
+ var nd = this.get(id);
+ if (nd === null) return;
+ if (nd.disabled || nd.group) return; // should click event if already selected
+ // unselect all previsously
+ $(obj.box).find('.w2ui-node.w2ui-selected').each(function (index, el) {
+ var oldID = $(el).attr('id').replace('node_', '');
+ var oldNode = obj.get(oldID);
+ if (oldNode != null) oldNode.selected = false;
+ $(el).removeClass('w2ui-selected').find('.w2ui-icon').removeClass('w2ui-icon-selected');
+ });
+ // select new one
+ var newNode = $(obj.box).find('#node_'+ w2utils.escapeId(id));
+ var oldNode = $(obj.box).find('#node_'+ w2utils.escapeId(obj.selected));
+ newNode.addClass('w2ui-selected').find('.w2ui-icon').addClass('w2ui-icon-selected');
+ // need timeout to allow rendering
+ setTimeout(function () {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'click', target: id, originalEvent: event, node: nd, object: nd });
+ if (eventData.isCancelled === true) {
+ // restore selection
+ newNode.removeClass('w2ui-selected').find('.w2ui-icon').removeClass('w2ui-icon-selected');
+ oldNode.addClass('w2ui-selected').find('.w2ui-icon').addClass('w2ui-icon-selected');
+ return;
+ }
+ // default action
+ if (oldNode !== null) oldNode.selected = false;
+ obj.get(id).selected = true;
+ obj.selected = id;
+ // route processing
+ if (nd.route) {
+ var route = String('/'+ nd.route).replace(/\/{2,}/g, '/');
+ var info = w2utils.parseRoute(route);
+ if (info.keys.length > 0) {
+ for (var k = 0; k < info.keys.length; k++) {
+ if (obj.routeData[info.keys[k].name] == null) continue;
+ route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), obj.routeData[info.keys[k].name]);
+ }
+ }
+ setTimeout(function () { window.location.hash = route; }, 1);
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, 1);
+ },
+
+ keydown: function (event) {
+ var obj = this;
+ var nd = obj.get(obj.selected);
+ if (!nd || obj.keyboard !== true) return;
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'keydown', target: obj.name, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default behaviour
+ if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
+ if (nd.nodes.length > 0) obj.toggle(obj.selected);
+ }
+ if (event.keyCode == 37) { // left
+ if (nd.nodes.length > 0 && nd.expanded) {
+ obj.collapse(obj.selected);
+ } else {
+ selectNode(nd.parent);
+ if (!nd.parent.group) obj.collapse(nd.parent.id);
+ }
+ }
+ if (event.keyCode == 39) { // right
+ if ((nd.nodes.length > 0 || nd.plus) && !nd.expanded) obj.expand(obj.selected);
+ }
+ if (event.keyCode == 38) { // up
+ selectNode(neighbor(nd, prev));
+ }
+ if (event.keyCode == 40) { // down
+ selectNode(neighbor(nd, next));
+ }
+ // cancel event if needed
+ if ($.inArray(event.keyCode, [13, 32, 37, 38, 39, 40]) != -1) {
+ if (event.preventDefault) event.preventDefault();
+ if (event.stopPropagation) event.stopPropagation();
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+
+ function selectNode (node, event) {
+ if (node !== null && !node.hidden && !node.disabled && !node.group) {
+ obj.click(node.id, event);
+ setTimeout(function () { obj.scrollIntoView(); }, 50);
+ }
+ }
+
+ function neighbor (node, neighborFunc) {
+ node = neighborFunc(node);
+ while (node !== null && (node.hidden || node.disabled)) {
+ if (node.group) break; else node = neighborFunc(node);
+ }
+ return node;
+ }
+
+ function next (node, noSubs) {
+ if (node === null) return null;
+ var parent = node.parent;
+ var ind = obj.get(node.id, true);
+ var nextNode = null;
+ // jump inside
+ if (node.expanded && node.nodes.length > 0 && noSubs !== true) {
+ var t = node.nodes[0];
+ if (t.hidden || t.disabled || t.group) nextNode = next(t); else nextNode = t;
+ } else {
+ if (parent && ind + 1 < parent.nodes.length) {
+ nextNode = parent.nodes[ind + 1];
+ } else {
+ nextNode = next(parent, true); // jump to the parent
+ }
+ }
+ if (nextNode !== null && (nextNode.hidden || nextNode.disabled || nextNode.group)) nextNode = next(nextNode);
+ return nextNode;
+ }
+
+ function prev (node) {
+ if (node === null) return null;
+ var parent = node.parent;
+ var ind = obj.get(node.id, true);
+ var prevNode = (ind > 0) ? lastChild(parent.nodes[ind - 1]) : parent;
+ if (prevNode !== null && (prevNode.hidden || prevNode.disabled || prevNode.group)) prevNode = prev(prevNode);
+ return prevNode;
+ }
+
+ function lastChild (node) {
+ if (node.expanded && node.nodes.length > 0) {
+ var t = node.nodes[node.nodes.length - 1];
+ if (t.hidden || t.disabled || t.group) return prev(t); else return lastChild(t);
+ }
+ return node;
+ }
+ },
+
+ scrollIntoView: function (id) {
+ if (typeof id == 'undefined') id = this.selected;
+ var nd = this.get(id);
+ if (nd === null) return;
+ var body = $(this.box).find('.w2ui-sidebar-div');
+ var item = $(this.box).find('#node_'+ w2utils.escapeId(id));
+ var offset = item.offset().top - body.offset().top;
+ if (offset + item.height() > body.height()) {
+ body.animate({ 'scrollTop': body.scrollTop() + body.height() / 1.3 }, 250, 'linear');
+ }
+ if (offset <= 0) {
+ body.animate({ 'scrollTop': body.scrollTop() - body.height() / 1.3 }, 250, 'linear');
+ }
+ },
+
+ dblClick: function (id, event) {
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ var nd = this.get(id);
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'dblClick', target: id, originalEvent: event, object: nd });
+ if (eventData.isCancelled === true) return;
+ // default action
+ this.toggle(id);
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ contextMenu: function (id, event) {
+ var obj = this;
+ var nd = obj.get(id);
+ if (id != obj.selected) obj.click(id);
+ // need timeout to allow click to finish first
+ setTimeout(function () {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'contextMenu', target: id, originalEvent: event, object: nd });
+ if (eventData.isCancelled === true) return;
+ // default action
+ if (nd.group || nd.disabled) return;
+ if (obj.menu.length > 0) {
+ $(obj.box).find('#node_'+ w2utils.escapeId(id))
+ .w2menu(obj.menu, {
+ left : (event ? event.offsetX || event.pageX : 50) - 25,
+ onSelect: function (event) {
+ obj.menuClick(id, parseInt(event.index), event.originalEvent);
+ }
+ }
+ );
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, 150); // need timer 150 for FF
+ },
+
+ menuClick: function (itemId, index, event) {
+ var obj = this;
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'menuClick', target: itemId, originalEvent: event, menuIndex: index, menuItem: obj.menu[index] });
+ if (eventData.isCancelled === true) return;
+ // default action
+ // -- empty
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ render: function (box) {
+ var time = (new Date()).getTime();
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'render', target: this.name, box: box });
+ if (eventData.isCancelled === true) return;
+ // default action
+ if (typeof box != 'undefined' && box !== null) {
+ if ($(this.box).find('> div > div.w2ui-sidebar-div').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-sidebar')
+ .html('');
+ }
+ this.box = box;
+ }
+ if (!this.box) return;
+ $(this.box)
+ .attr('name', this.name)
+ .addClass('w2ui-reset w2ui-sidebar')
+ .html('<div>'+
+ '<div class="w2ui-sidebar-top"></div>' +
+ '<div class="w2ui-sidebar-div"></div>'+
+ '<div class="w2ui-sidebar-bottom"></div>'+
+ '</div>'
+ );
+ $(this.box).find('> div').css({
+ width : $(this.box).width() + 'px',
+ height: $(this.box).height() + 'px'
+ });
+ if ($(this.box).length > 0) $(this.box)[0].style.cssText += this.style;
+ // adjust top and bottom
+ if (this.topHTML !== '') {
+ $(this.box).find('.w2ui-sidebar-top').html(this.topHTML);
+ $(this.box).find('.w2ui-sidebar-div')
+ .css('top', $(this.box).find('.w2ui-sidebar-top').height() + 'px');
+ }
+ if (this.bottomHTML !== '') {
+ $(this.box).find('.w2ui-sidebar-bottom').html(this.bottomHTML);
+ $(this.box).find('.w2ui-sidebar-div')
+ .css('bottom', $(this.box).find('.w2ui-sidebar-bottom').height() + 'px');
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ // ---
+ this.refresh();
+ return (new Date()).getTime() - time;
+ },
+
+ refresh: function (id) {
+ var time = (new Date()).getTime();
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'refresh', target: (typeof id != 'undefined' ? id : this.name) });
+ if (eventData.isCancelled === true) return;
+ // adjust top and bottom
+ if (this.topHTML !== '') {
+ $(this.box).find('.w2ui-sidebar-top').html(this.topHTML);
+ $(this.box).find('.w2ui-sidebar-div')
+ .css('top', $(this.box).find('.w2ui-sidebar-top').height() + 'px');
+ }
+ if (this.bottomHTML !== '') {
+ $(this.box).find('.w2ui-sidebar-bottom').html(this.bottomHTML);
+ $(this.box).find('.w2ui-sidebar-div')
+ .css('bottom', $(this.box).find('.w2ui-sidebar-bottom').height() + 'px');
+ }
+ // default action
+ $(this.box).find('> div').css({
+ width : $(this.box).width() + 'px',
+ height: $(this.box).height() + 'px'
+ });
+ var obj = this;
+ var node, nd;
+ var nm;
+ if (typeof id == 'undefined') {
+ node = this;
+ nm = '.w2ui-sidebar-div';
+ } else {
+ node = this.get(id);
+ if (node === null) return;
+ nm = '#node_'+ w2utils.escapeId(node.id) + '_sub';
+ }
+ var nodeHTML;
+ if (node !== this) {
+ var tmp = '#node_'+ w2utils.escapeId(node.id);
+ nodeHTML = getNodeHTML(node);
+ $(this.box).find(tmp).before('<div id="sidebar_'+ this.name + '_tmp"></div>');
+ $(this.box).find(tmp).remove();
+ $(this.box).find(nm).remove();
+ $('#sidebar_'+ this.name + '_tmp').before(nodeHTML);
+ $('#sidebar_'+ this.name + '_tmp').remove();
+ }
+ // refresh sub nodes
+ $(this.box).find(nm).html('');
+ for (var i = 0; i < node.nodes.length; i++) {
+ nd = node.nodes[i];
+ nodeHTML = getNodeHTML(nd);
+ $(this.box).find(nm).append(nodeHTML);
+ if (nd.nodes.length !== 0) { this.refresh(nd.id); }
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+
+ function getNodeHTML(nd) {
+ var html = '';
+ var img = nd.img;
+ if (img === null) img = this.img;
+ var icon = nd.icon;
+ if (icon === null) icon = this.icon;
+ // -- find out level
+ var tmp = nd.parent;
+ var level = 0;
+ while (tmp && tmp.parent !== null) {
+ if (tmp.group) level--;
+ tmp = tmp.parent;
+ level++;
+ }
+ if (typeof nd.caption != 'undefined') nd.text = nd.caption;
+ if (nd.group) {
+ html =
+ '<div class="w2ui-node-group" id="node_'+ nd.id +'"'+
+ ' onclick="w2ui[\''+ obj.name +'\'].toggle(\''+ nd.id +'\')"'+
+ ' onmouseout="$(this).find(\'span:nth-child(1)\').css(\'color\', \'transparent\')" '+
+ ' onmouseover="$(this).find(\'span:nth-child(1)\').css(\'color\', \'inherit\')">'+
+ (nd.groupShowHide ? '<span>'+ (!nd.hidden && nd.expanded ? w2utils.lang('Hide') : w2utils.lang('Show')) +'</span>' : '<span></span>') +
+ ' <span>'+ nd.text +'</span>'+
+ '</div>'+
+ '<div class="w2ui-node-sub" id="node_'+ nd.id +'_sub" style="'+ nd.style +';'+ (!nd.hidden && nd.expanded ? '' : 'display: none;') +'"></div>';
+ } else {
+ if (nd.selected && !nd.disabled) obj.selected = nd.id;
+ tmp = '';
+ if (img) tmp = '<div class="w2ui-node-image w2ui-icon '+ img + (nd.selected && !nd.disabled ? " w2ui-icon-selected" : "") +'"></div>';
+ if (icon) tmp = '<div class="w2ui-node-image"><span class="'+ icon +'"></span></div>';
+ html =
+ '<div class="w2ui-node '+ (nd.selected ? 'w2ui-selected' : '') +' '+ (nd.disabled ? 'w2ui-disabled' : '') +'" id="node_'+ nd.id +'" style="'+ (nd.hidden ? 'display: none;' : '') +'"'+
+ ' ondblclick="w2ui[\''+ obj.name +'\'].dblClick(\''+ nd.id +'\', event);"'+
+ ' oncontextmenu="w2ui[\''+ obj.name +'\'].contextMenu(\''+ nd.id +'\', event); '+
+ ' if (event.preventDefault) event.preventDefault();"'+
+ ' onClick="w2ui[\''+ obj.name +'\'].click(\''+ nd.id +'\', event); ">'+
+ '<table cellpadding="0" cellspacing="0" style="margin-left:'+ (level*18) +'px; padding-right:'+ (level*18) +'px"><tr>'+
+ '<td class="w2ui-node-dots" nowrap onclick="w2ui[\''+ obj.name +'\'].toggle(\''+ nd.id +'\'); '+
+ ' if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;">'+
+ ' <div class="w2ui-expand">' + (nd.nodes.length > 0 ? (nd.expanded ? '-' : '+') : (nd.plus ? '+' : '')) + '</div>' +
+ '</td>'+
+ '<td class="w2ui-node-data" nowrap>'+
+ tmp +
+ (nd.count || nd.count === 0 ? '<div class="w2ui-node-count">'+ nd.count +'</div>' : '') +
+ '<div class="w2ui-node-caption">'+ nd.text +'</div>'+
+ '</td>'+
+ '</tr></table>'+
+ '</div>'+
+ '<div class="w2ui-node-sub" id="node_'+ nd.id +'_sub" style="'+ nd.style +';'+ (!nd.hidden && nd.expanded ? '' : 'display: none;') +'"></div>';
+ }
+ return html;
+ }
+ },
+
+ resize: function () {
+ var time = (new Date()).getTime();
+ // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'resize', target: this.name });
+ if (eventData.isCancelled === true) return;
+ // default action
+ $(this.box).css('overflow', 'hidden'); // container should have no overflow
+ //$(this.box).find('.w2ui-sidebar-div').css('overflow', 'hidden');
+ $(this.box).find('> div').css({
+ width : $(this.box).width() + 'px',
+ height : $(this.box).height() + 'px'
+ });
+ //$(this.box).find('.w2ui-sidebar-div').css('overflow', 'auto');
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return (new Date()).getTime() - time;
+ },
+
+ destroy: function () {
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'destroy', target: this.name });
+ if (eventData.isCancelled === true) return;
+ // clean up
+ if ($(this.box).find('> div > div.w2ui-sidebar-div').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-sidebar')
+ .html('');
+ }
+ delete w2ui[this.name];
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ lock: function (msg, showSpinner) {
+ var box = $(this.box).find('> div:first-child');
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift(box);
+ w2utils.lock.apply(window, args);
+ },
+
+ unlock: function () {
+ w2utils.unlock(this.box);
+ }
+ };
+
+ $.extend(w2sidebar.prototype, w2utils.event);
+ w2obj.sidebar = w2sidebar;
+})();
+
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2field - various field controls
+* - $().w2field - jQuery wrapper
+* - Dependencies: jQuery, w2utils
+*
+* == NICE TO HAVE ==
+* - upload (regular files)
+* - BUG with prefix/postfix and arrows (test in different contexts)
+* - prefix and suffix are slow (100ms or so)
+* - multiple date selection
+* - month selection, year selections
+* - arrows no longer work (for int)
+* - form to support custom types
+* - bug: if input is hidden and then enum is applied, then when it becomes visible, it will be 110px
+*
+************************************************************************/
+
+(function ($) {
+
+ var w2field = function (options) {
+ // public properties
+ this.el = null
+ this.helpers = {}; // object or helper elements
+ this.type = options.type || 'text';
+ this.options = $.extend(true, {}, options);
+ this.onSearch = options.onSearch || null;
+ this.onRequest = options.onRequest || null;
+ this.onLoad = options.onLoad || null;
+ this.onError = options.onError || null;
+ this.onClick = options.onClick || null;
+ this.onAdd = options.onAdd || null;
+ this.onNew = options.onNew || null;
+ this.onRemove = options.onRemove || null;
+ this.onMouseOver = options.onMouseOver || null;
+ this.onMouseOut = options.onMouseOut || null;
+ this.onIconClick = options.onIconClick || null;
+ this.tmp = {}; // temp object
+ // clean up some options
+ delete this.options.type;
+ delete this.options.onSearch;
+ delete this.options.onRequest;
+ delete this.options.onLoad;
+ delete this.options.onError;
+ delete this.options.onClick;
+ delete this.options.onMouseOver;
+ delete this.options.onMouseOut;
+ delete this.options.onIconClick;
+ // extend with defaults
+ $.extend(true, this, w2obj.field);
+ };
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2field = function (method, options) {
+ // call direct
+ if (this.length == 0) {
+ var pr = w2field.prototype;
+ if (pr[method]) {
+ return pr[method].apply(pr, Array.prototype.slice.call(arguments, 1));
+ }
+ } else {
+ if (typeof method == 'string' && typeof options == 'object') {
+ method = $.extend(true, {}, options, { type: method });
+ }
+ if (typeof method == 'string' && typeof options == 'undefined') {
+ method = { type: method };
+ }
+ method.type = String(method.type).toLowerCase();
+ return this.each(function (index, el) {
+ var obj = $(el).data('w2field');
+ // if object is not defined, define it
+ if (typeof obj == 'undefined') {
+ var obj = new w2field(method);
+ $.extend(obj, { handlers: [] });
+ if (el) obj.el = $(el)[0];
+ obj.init();
+ $(el).data('w2field', obj);
+ return obj;
+ } else { // fully re-init
+ obj.clear();
+ if (method.type == 'clear') return;
+ var obj = new w2field(method);
+ $.extend(obj, { handlers: [] });
+ if (el) obj.el = $(el)[0];
+ obj.init();
+ $(el).data('w2field', obj);
+ return obj;
+ }
+ return null;
+ });
+ }
+ }
+
+ // ====================================================
+ // -- Implementation of core functionality
+
+ /* To add custom types
+ $().w2field('addType', 'myType', function (options) {
+ $(this.el).on('keypress', function (event) {
+ if (event.metaKey || event.ctrlKey || event.altKey
+ || (event.charCode != event.keyCode && event.keyCode > 0)) return;
+ var ch = String.fromCharCode(event.charCode);
+ if (ch != 'a' && ch != 'b' && ch != 'c') {
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ return false;
+ }
+ });
+ $(this.el).on('blur', function (event) { // keyCode & charCode differ in FireFox
+ var ch = this.value;
+ if (ch != 'a' && ch != 'b' && ch != 'c') {
+ $(this).w2tag(w2utils.lang("Not a single charecter from the set of 'abc'"));
+ }
+ });
+ });
+ */
+
+ w2field.prototype = {
+
+ custom: {}, // map of custom types
+
+ pallete: [
+ ['000000', '444444', '666666', '999999', 'CCCCCC', 'EEEEEE', 'F3F3F3', 'FFFFFF'],
+ ['FF011B', 'FF9838', 'FFFD59', '01FD55', '00FFFE', '0424F3', '9B24F4', 'FF21F5'],
+ ['F4CCCC', 'FCE5CD', 'FFF2CC', 'D9EAD3', 'D0E0E3', 'CFE2F3', 'D9D1E9', 'EAD1DC'],
+ ['EA9899', 'F9CB9C', 'FEE599', 'B6D7A8', 'A2C4C9', '9FC5E8', 'B4A7D6', 'D5A6BD'],
+ ['E06666', 'F6B26B', 'FED966', '93C47D', '76A5AF', '6FA8DC', '8E7CC3', 'C27BA0'],
+ ['CC0814', 'E69138', 'F1C232', '6AA84F', '45818E', '3D85C6', '674EA7', 'A54D79'],
+ ['99050C', 'B45F17', 'BF901F', '37761D', '124F5C', '0A5394', '351C75', '741B47'],
+ ['660205', '783F0B', '7F6011', '274E12', '0C343D', '063762', '20124D', '4C1030']
+ ],
+
+ addType: function (type, handler) {
+ type = String(type).toLowerCase();
+ this.custom[type] = handler;
+ return true;
+ },
+
+ removeType: function (type) {
+ type = String(type).toLowerCase();
+ if (!this.custom[type]) return false;
+ delete this.custom[type];
+ return true
+ },
+
+ init: function () {
+ var obj = this;
+ var options = this.options;
+ var defaults;
+
+ // Custom Types
+ if (typeof this.custom[this.type] == 'function') {
+ this.custom[this.type].call(this, options);
+ return;
+ }
+ // only for INPUT or TEXTAREA
+ if (['INPUT', 'TEXTAREA'].indexOf(this.el.tagName) == -1) {
+ console.log('ERROR: w2field could only be applied to INPUT or TEXTAREA.', this.el);
+ return;
+ }
+
+ switch (this.type) {
+ case 'text':
+ case 'int':
+ case 'float':
+ case 'money':
+ case 'currency':
+ case 'percent':
+ case 'alphanumeric':
+ case 'hex':
+ defaults = {
+ min : null,
+ max : null,
+ step : 1,
+ placeholder : '',
+ autoFormat : true,
+ currencyPrefix : w2utils.settings.currencyPrefix,
+ currencySuffix : w2utils.settings.currencySuffix,
+ currencyPrecision : w2utils.settings.currencyPrecision,
+ groupSymbol : w2utils.settings.groupSymbol,
+ arrows : false,
+ keyboard : true,
+ precision : null,
+ silent : true,
+ prefix : '',
+ suffix : ''
+ };
+ this.options = $.extend(true, {}, defaults, options);
+ options = this.options; // since object is re-created, need to re-assign
+ options.numberRE = new RegExp('['+ options.groupSymbol + ']', 'g');
+ options.moneyRE = new RegExp('['+ options.currencyPrefix + options.currencySuffix + options.groupSymbol + ']', 'g');
+ options.percentRE = new RegExp('['+ options.groupSymbol + '%]', 'g');
+ // no keyboard support needed
+ if (['text', 'alphanumeric', 'hex'].indexOf(this.type) != -1) {
+ options.arrows = false;
+ options.keyboard = false;
+ }
+ this.addPrefix(); // only will add if needed
+ this.addSuffix();
+ $(this.el).attr('placeholder', options.placeholder);
+ break;
+
+ case 'color':
+ defaults = {
+ prefix : '#',
+ suffix : '<div style="width: '+ (parseInt($(this.el).css('font-size')) || 12) +'px">&nbsp;</div>',
+ placeholder : '',
+ arrows : false,
+ keyboard : false
+ };
+ $.extend(options, defaults);
+ this.addPrefix(); // only will add if needed
+ this.addSuffix(); // only will add if needed
+ // additional checks
+ $(this.el).attr('maxlength', 6);
+ if ($(this.el).val() != '') setTimeout(function () { $(obj.el).change(); }, 1);
+ $(this.el).attr('placeholder', options.placeholder);
+ break;
+
+ case 'date':
+ defaults = {
+ format : w2utils.settings.date_format, // date format
+ placeholder : '',
+ keyboard : true,
+ silent : true,
+ start : '', // string or jquery object
+ end : '', // string or jquery object
+ blocked : {}, // { '4/11/2011': 'yes' }
+ colored : {} // { '4/11/2011': 'red:white' }
+ };
+ this.options = $.extend(true, {}, defaults, options);
+ options = this.options; // since object is re-created, need to re-assign
+ $(this.el).attr('placeholder', options.placeholder ? options.placeholder : options.format);
+ break;
+
+ case 'time':
+ defaults = {
+ format : w2utils.settings.time_format,
+ placeholder : '',
+ keyboard : true,
+ silent : true,
+ start : '',
+ end : ''
+ };
+ this.options = $.extend(true, {}, defaults, options);
+ options = this.options; // since object is re-created, need to re-assign
+ $(this.el).attr('placeholder', options.placeholder ? options.placeholder : (options.format == 'h12' ? 'hh:mi pm' : 'hh:mi'));
+ break;
+
+ case 'datetime':
+ break;
+
+ case 'list':
+ case 'combo':
+ defaults = {
+ items : [],
+ selected : {},
+ placeholder : '',
+ url : null, // url to pull data from
+ postData : {},
+ minLength : 1,
+ cacheMax : 250,
+ maxDropHeight : 350, // max height for drop down menu
+ match : 'begins', // ['contains', 'is', 'begins', 'ends']
+ silent : true,
+ icon : null,
+ iconStyle : '',
+ onSearch : null, // when search needs to be performed
+ onRequest : null, // when request is submitted
+ onLoad : null, // when data is received
+ onError : null, // when data fails to load due to server error or other failure modes
+ onIconClick : null,
+ renderDrop : null, // render function for drop down item
+ prefix : '',
+ suffix : '',
+ openOnFocus : false, // if to show overlay onclick or when typing
+ markSearch : false
+ };
+ options.items = this.normMenu(options.items); // need to be first
+ if (this.type == 'list') {
+ // defaults.search = (options.items && options.items.length >= 10 ? true : false);
+ defaults.openOnFocus = true;
+ defaults.suffix = '<div class="arrow-down" style="margin-top: '+ ((parseInt($(this.el).height()) - 6) / 2) +'px;"></div>';
+ $(this.el).addClass('w2ui-select');
+ // if simple value - look it up
+ if (!$.isPlainObject(options.selected)) {
+ for (var i in options.items) {
+ var item = options.items[i];
+ if (item && item.id == options.selected) {
+ options.selected = $.extend(true, {}, item);
+ break;
+ }
+ }
+ }
+ }
+ options = $.extend({}, defaults, options, {
+ align : 'both', // same width as control
+ altRows : true // alternate row color
+ });
+ this.options = options;
+ if (!$.isPlainObject(options.selected)) options.selected = {};
+ $(this.el).data('selected', options.selected);
+ if (options.url) this.request(0);
+ if (this.type == 'list') this.addFocus();
+ this.addPrefix();
+ this.addSuffix();
+ setTimeout(function () { obj.refresh(); }, 10); // need this for icon refresh
+ $(this.el).attr('placeholder', options.placeholder).attr('autocomplete', 'off');
+ if (typeof options.selected.text != 'undefined') $(this.el).val(options.selected.text);
+ break;
+
+ case 'enum':
+ defaults = {
+ items : [],
+ selected : [],
+ placeholder : '',
+ max : 0, // max number of selected items, 0 - unlim
+ url : null, // not implemented
+ postData : {},
+ minLength : 1,
+ cacheMax : 250,
+ maxWidth : 250, // max width for a single item
+ maxHeight : 350, // max height for input control to grow
+ maxDropHeight : 350, // max height for drop down menu
+ match : 'contains', // ['contains', 'is', 'begins', 'ends']
+ silent : true,
+ openOnFocus : false, // if to show overlay onclick or when typing
+ markSearch : true,
+ renderDrop : null, // render function for drop down item
+ renderItem : null, // render selected item
+ style : '', // style for container div
+ onSearch : null, // when search needs to be performed
+ onRequest : null, // when request is submitted
+ onLoad : null, // when data is received
+ onError : null, // when data fails to load due to server error or other failure modes
+ onClick : null, // when an item is clicked
+ onAdd : null, // when an item is added
+ onNew : null, // when new item should be added
+ onRemove : null, // when an item is removed
+ onMouseOver : null, // when an item is mouse over
+ onMouseOut : null // when an item is mouse out
+ };
+ options = $.extend({}, defaults, options, {
+ align : 'both', // same width as control
+ suffix : '',
+ altRows : true // alternate row color
+ });
+ options.items = this.normMenu(options.items);
+ options.selected = this.normMenu(options.selected);
+ this.options = options;
+ if (!$.isArray(options.selected)) options.selected = [];
+ $(this.el).data('selected', options.selected);
+ if (options.url) this.request(0);
+ this.addSuffix();
+ this.addMulti();
+ break;
+
+ case 'file':
+ defaults = {
+ selected : [],
+ placeholder : w2utils.lang('Attach files by dragging and dropping or Click to Select'),
+ max : 0,
+ maxSize : 0, // max size of all files, 0 - unlim
+ maxFileSize : 0, // max size of a single file, 0 -unlim
+ maxWidth : 250, // max width for a single item
+ maxHeight : 350, // max height for input control to grow
+ maxDropHeight : 350, // max height for drop down menu
+ silent : true,
+ renderItem : null, // render selected item
+ style : '', // style for container div
+ onClick : null, // when an item is clicked
+ onAdd : null, // when an item is added
+ onRemove : null, // when an item is removed
+ onMouseOver : null, // when an item is mouse over
+ onMouseOut : null // when an item is mouse out
+ };
+ options = $.extend({}, defaults, options, {
+ align : 'both', // same width as control
+ altRows : true // alternate row color
+ });
+ this.options = options;
+ if (!$.isArray(options.selected)) options.selected = [];
+ $(this.el).data('selected', options.selected);
+ this.addMulti();
+ break;
+ }
+ // attach events
+ this.tmp = {
+ onChange : function (event) { obj.change.call(obj, event) },
+ onClick : function (event) { obj.click.call(obj, event) },
+ onFocus : function (event) { obj.focus.call(obj, event) },
+ onBlur : function (event) { obj.blur.call(obj, event) },
+ onKeydown : function (event) { obj.keyDown.call(obj, event) },
+ onKeyup : function (event) { obj.keyUp.call(obj, event) },
+ onKeypress : function (event) { obj.keyPress.call(obj, event) }
+ }
+ $(this.el)
+ .addClass('w2field')
+ .data('w2field', this)
+ .on('change', this.tmp.onChange)
+ .on('click', this.tmp.onClick) // ignore click because it messes overlays
+ .on('focus', this.tmp.onFocus)
+ .on('blur', this.tmp.onBlur)
+ .on('keydown', this.tmp.onKeydown)
+ .on('keyup', this.tmp.onKeyup)
+ .on('keypress', this.tmp.onKeypress)
+ .css({
+ 'box-sizing' : 'border-box',
+ '-webkit-box-sizing' : 'border-box',
+ '-moz-box-sizing' : 'border-box',
+ '-ms-box-sizing' : 'border-box',
+ '-o-box-sizing' : 'border-box'
+ });
+ // format initial value
+ this.change($.Event('change'));
+ },
+
+ clear: function () {
+ var obj = this;
+ var options = this.options;
+ // if money then clear value
+ if (['money', 'currency'].indexOf(this.type) != -1) {
+ $(this.el).val($(this.el).val().replace(options.moneyRE, ''));
+ }
+ if (this.type == 'percent') {
+ $(this.el).val($(this.el).val().replace(/%/g, ''));
+ }
+ if (this.type == 'color') {
+ $(this.el).removeAttr('maxlength');
+ }
+ if (this.type == 'list') {
+ $(this.el).removeClass('w2ui-select');
+ }
+ if (['date', 'time'].indexOf(this.type) != -1) {
+ if ($(this.el).attr('placeholder') == options.format) $(this.el).attr('placeholder', '');
+ }
+ this.type = 'clear';
+ var tmp = $(this.el).data('tmp');
+ if (!this.tmp) return;
+ // restore paddings
+ if (typeof tmp != 'undefined') {
+ if (tmp && tmp['old-padding-left']) $(this.el).css('padding-left', tmp['old-padding-left']);
+ if (tmp && tmp['old-padding-right']) $(this.el).css('padding-right', tmp['old-padding-right']);
+ }
+ // remove events and data
+ $(this.el)
+ .val(this.clean($(this.el).val()))
+ .removeClass('w2field')
+ .removeData() // removes all attached data
+ .off('change', this.tmp.onChange)
+ .off('click', this.tmp.onClick)
+ .off('focus', this.tmp.onFocus)
+ .off('blur', this.tmp.onBlur)
+ .off('keydown', this.tmp.onKeydown)
+ .off('keyup', this.tmp.onKeyup)
+ .off('keypress', this.tmp.onKeypress);
+ // remove helpers
+ for (var h in this.helpers) $(this.helpers[h]).remove();
+ this.helpers = {};
+ },
+
+ refresh: function () {
+ var obj = this;
+ var options = this.options;
+ var selected = $(this.el).data('selected');
+ var time = (new Date()).getTime();
+ // enum
+ if (['list'].indexOf(this.type) != -1) {
+ $(obj.el).parent().css('white-space', 'nowrap'); // needs this for arrow alway to appear on the right side
+ // hide focus and show text
+ if (obj.helpers.prefix) obj.helpers.prefix.hide();
+ setTimeout(function () {
+ if (!obj.helpers.focus) return;
+ // if empty show no icon
+ if (!$.isEmptyObject(selected) && options.icon) {
+ options.prefix = '<span class="w2ui-icon '+ options.icon +'"style="cursor: pointer; font-size: 14px;' +
+ ' display: inline-block; margin-top: -1px; color: #7F98AD;'+ options.iconStyle +'">'+
+ '</span>';
+ obj.addPrefix();
+ } else {
+ options.prefix = '';
+ obj.addPrefix();
+ }
+ // focus helpder
+ var focus = obj.helpers.focus.find('input');
+ if ($(focus).val() == '') {
+ $(focus).css('opacity', 0).prev().css('opacity', 0);
+ $(obj.el).val(selected && selected.text != null ? selected.text : '');
+ $(obj.el).attr('placeholder', $(obj.el).attr('_placeholder'));
+ } else {
+ $(focus).css('opacity', 1).prev().css('opacity', 1);
+ $(obj.el).val('');
+ $(obj.el).attr('_placeholder', $(obj.el).attr('placeholder')).removeAttr('placeholder');
+ setTimeout(function () {
+ if (obj.helpers.prefix) obj.helpers.prefix.hide();
+ var tmp = 'position: absolute; opacity: 0; margin: 4px 0px 0px 2px; background-position: left !important;';
+ if (options.icon) {
+ $(focus).css('margin-left', '17px');
+ $(obj.helpers.focus).find('.icon-search').attr('style', tmp + 'width: 11px !important; opacity: 1');
+ } else {
+ $(focus).css('margin-left', '0px');
+ $(obj.helpers.focus).find('.icon-search').attr('style', tmp + 'width: 0px !important; opacity: 0');
+ }
+ }, 1);
+ }
+ }, 1);
+ }
+ if (['enum', 'file'].indexOf(this.type) != -1) {
+ var html = '';
+ for (var s in selected) {
+ var it = selected[s];
+ var ren = '';
+ if (typeof options.renderItem == 'function') {
+ ren = options.renderItem(it, s, '<div class="w2ui-list-remove" title="'+ w2utils.lang('Remove') +'" index="'+ s +'">&nbsp;&nbsp;</div>');
+ } else {
+ ren = '<div class="w2ui-list-remove" title="'+ w2utils.lang('Remove') +'" index="'+ s +'">&nbsp;&nbsp;</div>'+
+ (obj.type == 'enum' ? it.text : it.name + '<span class="file-size"> - '+ w2utils.size(it.size) +'</span>');
+ }
+ html += '<li index="'+ s +'" style="max-width: '+ parseInt(options.maxWidth) + 'px; '+ (it.style ? it.style : '') +'">'+
+ ren +'</li>';
+ }
+ var div = obj.helpers.multi;
+ var ul = div.find('ul');
+ div.attr('style', div.attr('style') + ';' + options.style);
+ if ($(obj.el).attr('readonly')) div.addClass('w2ui-readonly'); else div.removeClass('w2ui-readonly');
+ // celan
+ div.find('.w2ui-enum-placeholder').remove();
+ ul.find('li').not('li.nomouse').remove();
+ // add new list
+ if (html != '') {
+ ul.prepend(html);
+ } else if (typeof options.placeholder != 'undefined') {
+ var style =
+ 'padding-top: ' + $(this.el).css('padding-top') + ';'+
+ 'padding-left: ' + $(this.el).css('padding-left') + '; ' +
+ 'box-sizing: ' + $(this.el).css('box-sizing') + '; ' +
+ 'line-height: ' + $(this.el).css('line-height') + '; ' +
+ 'font-size: ' + $(this.el).css('font-size') + '; ' +
+ 'font-family: ' + $(this.el).css('font-family') + '; ';
+ div.prepend('<div class="w2ui-enum-placeholder" style="'+ style +'">'+ options.placeholder + '</div>');
+ }
+ // ITEMS events
+ div.find('li')
+ .data('mouse', 'out')
+ .on('click', function (event) {
+ var item = selected[$(event.target).attr('index')];
+ if ($(event.target).hasClass('nomouse')) return;
+ event.stopPropagation();
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'click', target: obj.el, originalEvent: event.originalEvent, item: item });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ if ($(event.target).hasClass('w2ui-list-remove')) {
+ if ($(obj.el).attr('readonly')) return;
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'remove', target: obj.el, originalEvent: event.originalEvent, item: item });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ $().w2overlay();
+ selected.splice($(event.target).attr('index'), 1);
+ $(obj.el).trigger('change');
+ $(event.target).parent().fadeOut('fast');
+ setTimeout(function () {
+ obj.refresh();
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, 300);
+ }
+ if (obj.type == 'file' && !$(event.target).hasClass('w2ui-list-remove')) {
+ var preview = '';
+ if ((/image/i).test(item.type)) { // image
+ preview = '<div style="padding: 3px;">'+
+ ' <img src="'+ (item.content ? 'data:'+ item.type +';base64,'+ item.content : '') +'" style="max-width: 300px;" '+
+ ' onload="var w = $(this).width(); var h = $(this).height(); '+
+ ' if (w < 300 & h < 300) return; '+
+ ' if (w >= h && w > 300) $(this).width(300);'+
+ ' if (w < h && h > 300) $(this).height(300);"'+
+ ' onerror="this.style.display = \'none\'"'+
+ ' >'+
+ '</div>';
+ }
+ var td1 = 'style="padding: 3px; text-align: right; color: #777;"';
+ var td2 = 'style="padding: 3px"';
+ preview += '<div style="padding: 8px;">'+
+ ' <table cellpadding="2">'+
+ ' <tr><td '+ td1 +'>Name:</td><td '+ td2 +'>'+ item.name +'</td></tr>'+
+ ' <tr><td '+ td1 +'>Size:</td><td '+ td2 +'>'+ w2utils.size(item.size) +'</td></tr>'+
+ ' <tr><td '+ td1 +'>Type:</td><td '+ td2 +'>' +
+ ' <span style="width: 200px; display: block-inline; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">'+ item.type +'</span>'+
+ ' </td></tr>'+
+ ' <tr><td '+ td1 +'>Modified:</td><td '+ td2 +'>'+ w2utils.date(item.modified) +'</td></tr>'+
+ ' </table>'+
+ '</div>';
+ $(event.target).w2overlay(preview);
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ })
+ .on('mouseover', function (event) {
+ var tmp = event.target;
+ if (tmp.tagName != 'LI') tmp = tmp.parentNode;
+ if ($(tmp).hasClass('nomouse')) return;
+ if ($(tmp).data('mouse') == 'out') {
+ var item = selected[$(tmp).attr('index')];
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'mouseOver', target: obj.el, originalEvent: event.originalEvent, item: item });
+ if (eventData.isCancelled === true) return;
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ $(tmp).data('mouse', 'over');
+ })
+ .on('mouseout', function (event) {
+ var tmp = event.target;
+ if (tmp.tagName != 'LI') tmp = tmp.parentNode;
+ if ($(tmp).hasClass('nomouse')) return;
+ $(tmp).data('mouse', 'leaving');
+ setTimeout(function () {
+ if ($(tmp).data('mouse') == 'leaving') {
+ $(tmp).data('mouse', 'out');
+ var item = selected[$(tmp).attr('index')];
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'f', target: obj.el, originalEvent: event.originalEvent, item: item });
+ if (eventData.isCancelled === true) return;
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ }, 0);
+ });
+ // adjust height
+ $(this.el).height('auto');
+ var cntHeight = $(div).find('> div').height() + w2utils.getSize(div, '+height') * 2;
+ if (cntHeight < 26) cntHeight = 26;
+ if (cntHeight > options.maxHeight) cntHeight = options.maxHeight;
+ if (div.length > 0) div[0].scrollTop = 1000;
+ var inpHeight = w2utils.getSize($(this.el), 'height') - 2;
+ if (inpHeight > cntHeight) cntHeight = inpHeight
+ $(div).css({ 'height': cntHeight + 'px', overflow: (cntHeight == options.maxHeight ? 'auto' : 'hidden') });
+ if (cntHeight < options.maxHeight) $(div).prop('scrollTop', 0);
+ $(this.el).css({ 'height' : (cntHeight + 2) + 'px' });
+ }
+ return (new Date()).getTime() - time;
+ },
+
+ reset: function () {
+ var obj = this;
+ var type = this.type;
+ this.clear();
+ this.type = type;
+ this.init();
+ },
+
+ clean: function (val) {
+ var options = this.options;
+ val = String(val).trim();
+ // clean
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(this.type) != -1) {
+ if (options.autoFormat && ['money', 'currency'].indexOf(this.type) != -1) val = String(val).replace(options.moneyRE, '');
+ if (options.autoFormat && this.type == 'percent') val = String(val).replace(options.percentRE, '');
+ if (options.autoFormat && ['int', 'float'].indexOf(this.type) != -1) val = String(val).replace(options.numberRE, '');
+ if (parseFloat(val) == val) {
+ if (options.min !== null && val < options.min) { val = options.min; $(this.el).val(options.min); }
+ if (options.max !== null && val > options.max) { val = options.max; $(this.el).val(options.max); }
+ }
+ if (val !== '' && w2utils.isFloat(val)) val = Number(val); else val = '';
+ }
+ return val;
+ },
+
+ format: function (val) {
+ var options = this.options;
+ // autoformat numbers or money
+ if (options.autoFormat && val != '') {
+ switch (this.type) {
+ case 'money':
+ case 'currency':
+ val = w2utils.formatNumber(Number(val).toFixed(options.currencyPrecision), options.groupSymbol);
+ if (val != '') val = options.currencyPrefix + val + options.currencySuffix;
+ break;
+ case 'percent':
+ val = w2utils.formatNumber(options.precision ? Number(val).toFixed(options.precision) : val, options.groupSymbol);
+ if (val != '') val += '%';
+ break;
+ case 'float':
+ val = w2utils.formatNumber(options.precision ? Number(val).toFixed(options.precision) : val, options.groupSymbol);
+ break;
+ case 'int':
+ val = w2utils.formatNumber(val, options.groupSymbol);
+ break;
+ }
+ }
+ return val;
+ },
+
+ change: function (event) {
+ var obj = this;
+ var options = obj.options;
+ // numeric
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(this.type) != -1) {
+ // check max/min
+ var val = $(this.el).val();
+ var new_val = this.format(this.clean($(this.el).val()));
+ // if was modified
+ if (val != '' && val != new_val) {
+ $(this.el).val(new_val).change();
+ // cancel event
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+ }
+ // color
+ if (this.type == 'color') {
+ var color = '#' + $(this.el).val();
+ if ($(this.el).val().length != 6 && $(this.el).val().length != 3) color = '';
+ $(this.el).next().find('div').css('background-color', color);
+ if ($(obj.el).is(':focus')) this.updateOverlay();
+ }
+ },
+
+ click: function (event) {
+ event.stopPropagation();
+ // lists
+ if (['list', 'combo', 'enum'].indexOf(this.type) != -1) {
+ if (!$(this.el).is(':focus')) this.focus(event);
+ }
+ // other fields with drops
+ if (['date', 'time', 'color'].indexOf(this.type) != -1) {
+ this.updateOverlay();
+ }
+ },
+
+ focus: function (event) {
+ var obj = this;
+ var options = this.options;
+ // color, date, time
+ if (['color', 'date', 'time'].indexOf(obj.type) !== -1) {
+ if ($(obj.el).attr('readonly')) return;
+ if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay')[0].hide();
+ setTimeout(function () { obj.updateOverlay(); }, 150);
+ }
+ // menu
+ if (['list', 'combo', 'enum'].indexOf(obj.type) != -1) {
+ if ($(obj.el).attr('readonly')) return;
+ if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay')[0].hide();
+ setTimeout(function () {
+ if (obj.type == 'list' && $(obj.el).is(':focus')) {
+ $(obj.helpers.focus).find('input').focus();
+ return;
+ }
+ obj.search();
+ setTimeout(function () { obj.updateOverlay(); }, 1);
+ }, 1);
+ }
+ // file
+ if (obj.type == 'file') {
+ $(obj.helpers.multi).css({ 'outline': 'auto 5px #7DB4F3', 'outline-offset': '-2px' });
+ }
+ },
+
+ blur: function (event) {
+ var obj = this;
+ var options = obj.options;
+ var val = $(obj.el).val().trim();
+ // hide overlay
+ if (['color', 'date', 'time', 'list', 'combo', 'enum'].indexOf(obj.type) != -1) {
+ if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay')[0].hide();
+ }
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(obj.type) != -1) {
+ if (val !== '' && !obj.checkType(val)) {
+ $(obj.el).val('').change();
+ if (options.silent === false) {
+ $(obj.el).w2tag('Not a valid number');
+ setTimeout(function () { $(obj.el).w2tag(''); }, 3000);
+ }
+ }
+ }
+ // date or time
+ if (['date', 'time'].indexOf(obj.type) != -1) {
+ if (w2utils.isInt(obj.el.value)) {
+ $(obj.el).val(w2utils.formatDate(new Date(parseInt(obj.el.value)), options.format)).change();
+ }
+ // check if in range
+ if (val !== '' && !obj.inRange(obj.el.value)) {
+ $(obj.el).val('').removeData('selected').change();
+ if (options.silent === false) {
+ $(obj.el).w2tag('Not in range');
+ setTimeout(function () { $(obj.el).w2tag(''); }, 3000);
+ }
+ } else {
+ if (obj.type == 'date' && val !== '' && !w2utils.isDate(obj.el.value, options.format)) {
+ $(obj.el).val('').removeData('selected').change();
+ if (options.silent === false) {
+ $(obj.el).w2tag('Not a valid date');
+ setTimeout(function () { $(obj.el).w2tag(''); }, 3000);
+ }
+ }
+ if (obj.type == 'time' && val !== '' && !w2utils.isTime(obj.el.value)) {
+ $(obj.el).val('').removeData('selected').change();
+ if (options.silent === false) {
+ $(obj.el).w2tag('Not a valid time');
+ setTimeout(function () { $(obj.el).w2tag(''); }, 3000);
+ }
+ }
+ }
+ }
+ // clear search input
+ if (obj.type == 'enum') {
+ $(obj.helpers.multi).find('input').val('').width(20);
+ }
+ // file
+ if (obj.type == 'file') {
+ $(obj.helpers.multi).css({ 'outline': 'none' });
+ }
+ },
+
+ keyPress: function (event) {
+ var obj = this;
+ var options = obj.options;
+ // ignore wrong pressed key
+ if (['int', 'float', 'money', 'currency', 'percent', 'hex', 'color', 'alphanumeric'].indexOf(obj.type) != -1) {
+ // keyCode & charCode differ in FireFox
+ if (event.metaKey || event.ctrlKey || event.altKey || (event.charCode != event.keyCode && event.keyCode > 0)) return;
+ var ch = String.fromCharCode(event.charCode);
+ if (!obj.checkType(ch, true) && event.keyCode != 13) {
+ event.preventDefault();
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ return false;
+ }
+ }
+ // update date popup
+ if (['date', 'time'].indexOf(obj.type) != -1) {
+ setTimeout(function () { obj.updateOverlay(); }, 1);
+ }
+ },
+
+ keyDown: function (event, extra) {
+ var obj = this;
+ var options = obj.options;
+ var key = event.keyCode || (extra && extra.keyCode);
+ // numeric
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(obj.type) != -1) {
+ if (!options.keyboard || $(obj.el).attr('readonly')) return;
+ var cancel = false;
+ var val = parseFloat($(obj.el).val().replace(options.moneyRE, '')) || 0;
+ var inc = options.step;
+ if (event.ctrlKey || event.metaKey) inc = 10;
+ switch (key) {
+ case 38: // up
+ if (event.shiftKey) break; // no action if shift key is pressed
+ $(obj.el).val((val + inc <= options.max || options.max === null ? Number((val + inc).toFixed(12)) : options.max)).change();
+ cancel = true;
+ break;
+ case 40: // down
+ if (event.shiftKey) break; // no action if shift key is pressed
+ $(obj.el).val((val - inc >= options.min || options.min === null ? Number((val - inc).toFixed(12)) : options.min)).change();
+ cancel = true;
+ break;
+ }
+ if (cancel) {
+ event.preventDefault();
+ setTimeout(function () {
+ // set cursor to the end
+ obj.el.setSelectionRange(obj.el.value.length, obj.el.value.length);
+ }, 0);
+ }
+ }
+ // date
+ if (obj.type == 'date') {
+ if (!options.keyboard || $(obj.el).attr('readonly')) return;
+ var cancel = false;
+ var daymil = 24*60*60*1000;
+ var inc = 1;
+ if (event.ctrlKey || event.metaKey) inc = 10;
+ var dt = w2utils.isDate($(obj.el).val(), options.format, true);
+ if (!dt) { dt = new Date(); daymil = 0; }
+ switch (key) {
+ case 38: // up
+ if (event.shiftKey) break; // no action if shift key is pressed
+ var newDT = w2utils.formatDate(dt.getTime() + daymil, options.format);
+ if (inc == 10) newDT = w2utils.formatDate(new Date(dt.getFullYear(), dt.getMonth()+1, dt.getDate()), options.format);
+ $(obj.el).val(newDT).change();
+ cancel = true;
+ break;
+ case 40: // down
+ if (event.shiftKey) break; // no action if shift key is pressed
+ var newDT = w2utils.formatDate(dt.getTime() - daymil, options.format);
+ if (inc == 10) newDT = w2utils.formatDate(new Date(dt.getFullYear(), dt.getMonth()-1, dt.getDate()), options.format);
+ $(obj.el).val(newDT).change();
+ cancel = true;
+ break;
+ }
+ if (cancel) {
+ event.preventDefault();
+ setTimeout(function () {
+ // set cursor to the end
+ obj.el.setSelectionRange(obj.el.value.length, obj.el.value.length);
+ obj.updateOverlay();
+ }, 0);
+ }
+ }
+ // time
+ if (obj.type == 'time') {
+ if (!options.keyboard || $(obj.el).attr('readonly')) return;
+ var cancel = false;
+ var inc = 1;
+ if (event.ctrlKey || event.metaKey) inc = 60;
+ if (w2utils.isInt(obj.el.value)) {
+ $(obj.el).val(w2utils.formatTime(new Date(parseInt(obj.el.value)), options.format)).change();
+ }
+ var val = $(obj.el).val();
+ var time = obj.toMin(val) || obj.toMin((new Date()).getHours() + ':' + ((new Date()).getMinutes() - 1));
+ switch (key) {
+ case 38: // up
+ if (event.shiftKey) break; // no action if shift key is pressed
+ time += inc;
+ cancel = true;
+ break;
+ case 40: // down
+ if (event.shiftKey) break; // no action if shift key is pressed
+ time -= inc;
+ cancel = true;
+ break;
+ }
+ if (cancel) {
+ $(obj.el).val(obj.fromMin(time)).change();
+ event.preventDefault();
+ setTimeout(function () {
+ // set cursor to the end
+ obj.el.setSelectionRange(obj.el.value.length, obj.el.value.length);
+ }, 0);
+ }
+ }
+ // color
+ if (obj.type == 'color') {
+ if ($(obj.el).attr('readonly')) return;
+ // paste
+ if (event.keyCode == 86 && (event.ctrlKey || event.metaKey)) {
+ $(obj.el).prop('maxlength', 7);
+ setTimeout(function () {
+ var val = $(obj).val();
+ if (val.substr(0, 1) == '#') val = val.substr(1);
+ if (!w2utils.isHex(val)) val = '';
+ $(obj).val(val).prop('maxlength', 6).change();
+ }, 20);
+ }
+ if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
+ if (typeof obj.tmp.cind1 == 'undefined') {
+ obj.tmp.cind1 = -1;
+ obj.tmp.cind2 = -1;
+ } else {
+ switch (key) {
+ case 38: // up
+ obj.tmp.cind1--;
+ break;
+ case 40: // down
+ obj.tmp.cind1++;
+ break;
+ case 39: // right
+ obj.tmp.cind2++;
+ break;
+ case 37: // left
+ obj.tmp.cind2--;
+ break;
+ }
+ if (obj.tmp.cind1 < 0) obj.tmp.cind1 = 0;
+ if (obj.tmp.cind1 > this.pallete.length - 1) obj.tmp.cind1 = this.pallete.length - 1;
+ if (obj.tmp.cind2 < 0) obj.tmp.cind2 = 0;
+ if (obj.tmp.cind2 > this.pallete[0].length - 1) obj.tmp.cind2 = this.pallete[0].length - 1;
+ }
+ if ([37, 38, 39, 40].indexOf(key) != -1) {
+ $(obj.el).val(this.pallete[obj.tmp.cind1][obj.tmp.cind2]).change();
+ event.preventDefault();
+ }
+ }
+ }
+ // list/select/combo
+ if (['list', 'combo', 'enum'].indexOf(obj.type) != -1) {
+ if ($(obj.el).attr('readonly')) return;
+ var cancel = false;
+ var selected = $(obj.el).data('selected');
+ var focus = $(obj.helpers.focus).find('input');
+ if (obj.type == 'list') {
+ if ([37, 38, 39, 40].indexOf(key) == -1) obj.refresh(); // arrows
+ }
+ // apply arrows
+ switch (key) {
+ case 27: // escape
+ if (obj.type == 'list') {
+ if ($(focus).val() != '') $(focus).val('');
+ event.stopPropagation(); // escape in field should not close popup
+ }
+ break;
+ case 37: // left
+ case 39: // right
+ // cancel = true;
+ break;
+ case 13: // enter
+ if ($('#w2ui-overlay').length == 0) break; // no action if overlay not open
+ var item = options.items[options.index];
+ var multi = $(obj.helpers.multi).find('input');
+ if (obj.type == 'enum') {
+ if (item != null) {
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'add', target: obj.el, originalEvent: event.originalEvent, item: item });
+ if (eventData.isCancelled === true) return;
+ item = eventData.item; // need to reassign because it could be recreated by user
+ // default behavior
+ if (selected.length >= options.max && options.max > 0) selected.pop();
+ delete item.hidden;
+ delete obj.tmp.force_open;
+ selected.push(item);
+ $(obj.el).change();
+ multi.val('').width(20);
+ obj.refresh();
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ } else {
+ // trigger event
+ item = { id: multi.val(), text: multi.val() }
+ var eventData = obj.trigger({ phase: 'before', type: 'new', target: obj.el, originalEvent: event.originalEvent, item: item });
+ if (eventData.isCancelled === true) return;
+ item = eventData.item; // need to reassign because it could be recreated by user
+ // default behavior
+ if (typeof obj.onNew == 'function') {
+ if (selected.length >= options.max && options.max > 0) selected.pop();
+ delete obj.tmp.force_open;
+ selected.push(item);
+ $(obj.el).change();
+ multi.val('').width(20);
+ obj.refresh();
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ } else {
+ if (item) $(obj.el).data('selected', item).val(item.text).change();
+ if ($(obj.el).val() == '' && $(obj.el).data('selected')) $(obj.el).removeData('selected').val('').change();
+ if (obj.type == 'list') {
+ focus.val('');
+ obj.refresh();
+ }
+ // hide overlay
+ obj.tmp.force_hide = true;
+ }
+ break;
+ case 8: // backspace
+ case 46: // delete
+ if (obj.type == 'enum' && key == 8) {
+ if ($(obj.helpers.multi).find('input').val() == '' && selected.length > 0) {
+ var item = selected[selected.length - 1];
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'remove', target: obj.el, originalEvent: event.originalEvent, item: item });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ selected.pop();
+ $(obj.el).trigger('change');
+ obj.refresh();
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ }
+ if (obj.type == 'list' && $(focus).val() == '') {
+ $(obj.el).data('selected', {}).change();
+ obj.refresh();
+ }
+ break;
+ case 38: // up
+ options.index = w2utils.isInt(options.index) ? parseInt(options.index) : 0;
+ options.index--;
+ while (options.index > 0 && options.items[options.index].hidden) options.index--;
+ if (options.index == 0 && options.items[options.index].hidden) {
+ while (options.items[options.index] && options.items[options.index].hidden) options.index++;
+ }
+ cancel = true;
+ break;
+ case 40: // down
+ options.index = w2utils.isInt(options.index) ? parseInt(options.index) : -1;
+ options.index++;
+ while (options.index < options.items.length-1 && options.items[options.index].hidden) options.index++;
+ if (options.index == options.items.length-1 && options.items[options.index].hidden) {
+ while (options.items[options.index] && options.items[options.index].hidden) options.index--;
+ }
+ // show overlay if not shown
+ var input = obj.el;
+ if (['enum'].indexOf(obj.type) != -1) input = obj.helpers.multi.find('input');
+ if ($(input).val() == '' && $('#w2ui-overlay').length == 0) {
+ obj.tmp.force_open = true;
+ } else {
+ cancel = true;
+ }
+ break;
+ }
+ if (cancel) {
+ if (options.index < 0) options.index = 0;
+ if (options.index >= options.items.length) options.index = options.items.length -1;
+ obj.updateOverlay();
+ // cancel event
+ event.preventDefault();
+ setTimeout(function () {
+ // set cursor to the end
+ if (obj.type == 'enum') {
+ var tmp = obj.helpers.multi.find('input').get(0);
+ tmp.setSelectionRange(tmp.value.length, tmp.value.length);
+ } else if (obj.type == 'list') {
+ var tmp = obj.helpers.focus.find('input').get(0);
+ tmp.setSelectionRange(tmp.value.length, tmp.value.length);
+ } else {
+ obj.el.setSelectionRange(obj.el.value.length, obj.el.value.length);
+ }
+ }, 0);
+ return;
+ }
+ // expand input
+ if (obj.type == 'enum') {
+ var input = obj.helpers.multi.find('input');
+ var search = input.val();
+ input.width(((search.length + 2) * 8) + 'px');
+ }
+ // run search
+ if ([16, 17, 18, 20, 37, 39, 91].indexOf(key) == -1) { // no refreah on crtl, shift, left/right arrows, etc
+ setTimeout(function () {
+ if (!obj.tmp.force_hide) obj.request();
+ obj.search();
+ }, 1);
+ }
+ }
+ },
+
+ keyUp: function (event) {
+ if (this.type == 'color') {
+ if (event.keyCode == 86 && (event.ctrlKey || event.metaKey)) $(this).prop('maxlength', 6);
+ }
+ },
+
+ clearCache: function () {
+ var options = this.options;
+ options.items = [];
+ this.tmp.xhr_loading = false;
+ this.tmp.xhr_search = '';
+ this.tmp.xhr_total = -1;
+ this.search();
+ },
+
+ request: function (interval) {
+ var obj = this;
+ var options = this.options;
+ var search = $(obj.el).val() || '';
+ // if no url - do nothing
+ if (!options.url) return;
+ // --
+ if (obj.type == 'enum') {
+ var tmp = $(obj.helpers.multi).find('input');
+ if (tmp.length == 0) search = ''; else search = tmp.val();
+ }
+ if (obj.type == 'list') {
+ var tmp = $(obj.helpers.focus).find('input');
+ if (tmp.length == 0) search = ''; else search = tmp.val();
+ }
+ if (options.minLength != 0 && search.length < options.minLength) {
+ options.items = []; // need to empty the list
+ this.updateOverlay();
+ return;
+ }
+ if (typeof interval == 'undefined') interval = 350;
+ if (typeof obj.tmp.xhr_search == 'undefined') obj.tmp.xhr_search = '';
+ if (typeof obj.tmp.xhr_total == 'undefined') obj.tmp.xhr_total = -1;
+ // check if need to search
+ if (options.url && $(obj.el).prop('readonly') != true && (
+ (options.items.length === 0 && obj.tmp.xhr_total !== 0) ||
+ (obj.tmp.xhr_total == options.cacheMax && search.length > obj.tmp.xhr_search.length) ||
+ (search.length >= obj.tmp.xhr_search.length && search.substr(0, obj.tmp.xhr_search.length) != obj.tmp.xhr_search) ||
+ (search.length < obj.tmp.xhr_search.length)
+ )) {
+ // empty list
+ obj.tmp.xhr_loading = true;
+ obj.search();
+ // timeout
+ clearTimeout(obj.tmp.timeout);
+ obj.tmp.timeout = setTimeout(function () {
+ // trigger event
+ var url = options.url;
+ var postData = {
+ search : search,
+ max : options.cacheMax
+ };
+ $.extend(postData, options.postData);
+ var eventData = obj.trigger({ phase: 'before', type: 'request', target: obj.el, url: url, postData: postData });
+ if (eventData.isCancelled === true) return;
+ url = eventData.url;
+ postData = eventData.postData;
+ // console.log('REMOTE SEARCH:', search);
+ if (obj.tmp.xhr) obj.tmp.xhr.abort();
+ var ajaxOptions = {
+ type : 'GET',
+ url : url,
+ data : postData,
+ dataType : 'JSON' // expected from server
+ };
+ if (w2utils.settings.dataType == 'JSON') {
+ ajaxOptions.type = 'POST';
+ ajaxOptions.data = JSON.stringify(ajaxOptions.data);
+ ajaxOptions.contentType = 'application/json';
+ }
+ obj.tmp.xhr = $.ajax(ajaxOptions)
+ .done(function (data, status, xhr) {
+ // trigger event
+ var eventData2 = obj.trigger({ phase: 'before', type: 'load', target: obj.el, search: postData.search, data: data, xhr: xhr });
+ if (eventData2.isCancelled === true) return;
+ // default behavior
+ data = eventData2.data;
+ if (typeof data == 'string') data = JSON.parse(data);
+ if (data.status != 'success') {
+ console.log('ERROR: server did not return proper structure. It should return', { status: 'success', items: [{ id: 1, text: 'item' }] });
+ return;
+ }
+ // remove all extra items if more then needed for cache
+ if (data.items.length > options.cacheMax) data.items.splice(options.cacheMax, 100000);
+ // remember stats
+ obj.tmp.xhr_loading = false;
+ obj.tmp.xhr_search = search;
+ obj.tmp.xhr_total = data.items.length;
+ options.items = data.items;
+ if (search == '' && data.items.length == 0) obj.tmp.emptySet = true; else obj.tmp.emptySet = false;
+ obj.search();
+ // console.log('-->', 'retrieved:', obj.tmp.xhr_total);
+ // event after
+ obj.trigger($.extend(eventData2, { phase: 'after' }));
+ })
+ .fail(function (xhr, status, error) {
+ // trigger event
+ var errorObj = { status: status, error: error, rawResponseText: xhr.responseText };
+ var eventData2 = obj.trigger({ phase: 'before', type: 'error', target: obj.el, search: search, error: errorObj, xhr: xhr });
+ if (eventData2.isCancelled === true) return;
+ // default behavior
+ if (status != 'abort') {
+ var data;
+ try { data = $.parseJSON(xhr.responseText) } catch (e) {}
+ console.log('ERROR: Server communication failed.',
+ '\n EXPECTED:', { status: 'success', items: [{ id: 1, text: 'item' }] },
+ '\n OR:', { status: 'error', message: 'error message' },
+ '\n RECEIVED:', typeof data == 'object' ? data : xhr.responseText);
+ }
+ // reset stats
+ obj.clearCache();
+ // event after
+ obj.trigger($.extend(eventData2, { phase: 'after' }));
+ });
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, interval);
+ }
+ },
+
+ search: function () {
+ var obj = this;
+ var options = this.options;
+ var search = $(obj.el).val();
+ var target = obj.el;
+ var ids = [];
+ var selected= $(obj.el).data('selected');
+ if (obj.type == 'enum') {
+ target = $(obj.helpers.multi).find('input');
+ search = target.val();
+ for (var s in selected) { if (selected[s]) ids.push(selected[s].id); }
+ }
+ if (obj.type == 'list') {
+ target = $(obj.helpers.focus).find('input');
+ search = target.val();
+ for (var s in selected) { if (selected[s]) ids.push(selected[s].id); }
+ }
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'search', target: target, search: search });
+ if (eventData.isCancelled === true) return;
+ if (obj.tmp.xhr_loading !== true) {
+ var shown = 0;
+ for (var i in options.items) {
+ var item = options.items[i];
+ var prefix = '';
+ var suffix = '';
+ if (['is', 'begins'].indexOf(options.match) != -1) prefix = '^';
+ if (['is', 'ends'].indexOf(options.match) != -1) suffix = '$';
+ try {
+ var re = new RegExp(prefix + search + suffix, 'i');
+ if (re.test(item.text) || item.text == '...') item.hidden = false; else item.hidden = true;
+ } catch (e) {}
+ // do not show selected items
+ if (obj.type == 'enum' && $.inArray(item.id, ids) != -1) item.hidden = true;
+ if (item.hidden !== true) shown++;
+ }
+ if (obj.type != 'combo') { // don't preselect first for combo
+ options.index = 0;
+ while (options.items[options.index] && options.items[options.index].hidden) options.index++;
+ } else {
+ options.index = -1;
+ }
+ if (shown <= 0) options.index = -1;
+ options.spinner = false;
+ obj.updateOverlay();
+ setTimeout(function () {
+ var html = $('#w2ui-overlay').html() || '';
+ if (options.markSearch && html.indexOf('$.fn.w2menuHandler') != -1) { // do not highlight when no items
+ $('#w2ui-overlay').w2marker(search);
+ }
+ }, 1);
+ } else {
+ options.items.splice(0, options.cacheMax);
+ options.spinner = true;
+ obj.updateOverlay();
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ updateOverlay: function () {
+ var obj = this;
+ var options = this.options;
+ // color
+ if (this.type == 'color') {
+ if ($(obj.el).attr('readonly')) return;
+ if ($('#w2ui-overlay').length == 0) {
+ $(obj.el).w2overlay(obj.getColorHTML());
+ } else {
+ $('#w2ui-overlay').html(obj.getColorHTML());
+ }
+ // bind events
+ $('#w2ui-overlay .color')
+ .on('mousedown', function (event) {
+ var color = $(event.originalEvent.target).attr('name');
+ var index = $(event.originalEvent.target).attr('index').split(':');
+ obj.tmp.cind1 = index[0];
+ obj.tmp.cind2 = index[1];
+ $(obj.el).val(color).change();
+ $(this).html('&#149;');
+ })
+ .on('mouseup', function () {
+ setTimeout(function () {
+ if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay').removeData('keepOpen')[0].hide();
+ }, 10);
+ });
+ }
+ // date
+ if (this.type == 'date') {
+ if ($(obj.el).attr('readonly')) return;
+ if ($('#w2ui-overlay').length == 0) {
+ $(obj.el).w2overlay('<div class="w2ui-reset w2ui-calendar" onclick="event.stopPropagation();"></div>', {
+ css: { "background-color": "#f5f5f5" }
+ });
+ }
+ var month, year;
+ var dt = w2utils.isDate($(obj.el).val(), obj.options.format, true);
+ if (dt) { month = dt.getMonth() + 1; year = dt.getFullYear(); }
+ (function refreshCalendar(month, year) {
+ $('#w2ui-overlay > div > div').html(obj.getMonthHTML(month, year));
+ $('#w2ui-overlay .w2ui-calendar-title')
+ .on('mousedown', function () {
+ if ($(this).next().hasClass('w2ui-calendar-jump')) {
+ $(this).next().remove();
+ } else {
+ var selYear, selMonth;
+ $(this).after('<div class="w2ui-calendar-jump" style=""></div>');
+ $(this).next().hide().html(obj.getYearHTML()).fadeIn(200);
+ setTimeout(function () {
+ $('#w2ui-overlay .w2ui-calendar-jump')
+ .find('.w2ui-jump-month, .w2ui-jump-year')
+ .on('click', function () {
+ if ($(this).hasClass('w2ui-jump-month')) {
+ $(this).parent().find('.w2ui-jump-month').removeClass('selected');
+ $(this).addClass('selected');
+ selMonth = $(this).attr('name');
+ }
+ if ($(this).hasClass('w2ui-jump-year')) {
+ $(this).parent().find('.w2ui-jump-year').removeClass('selected');
+ $(this).addClass('selected');
+ selYear = $(this).attr('name');
+ }
+ if (selYear != null && selMonth != null) {
+ $('#w2ui-overlay .w2ui-calendar-jump').fadeOut(100);
+ setTimeout(function () { refreshCalendar(parseInt(selMonth)+1, selYear); }, 100);
+ }
+ });
+ $('#w2ui-overlay .w2ui-calendar-jump >:last-child').prop('scrollTop', 2000);
+ }, 1);
+ }
+ });
+ $('#w2ui-overlay .w2ui-date')
+ .on('mousedown', function () {
+ var day = $(this).attr('date');
+ $(obj.el).val(day).change();
+ $(this).css({ 'background-color': '#B6D5FB', 'border-color': '#aaa' });
+ })
+ .on('mouseup', function () {
+ setTimeout(function () {
+ if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay').removeData('keepOpen')[0].hide();
+ }, 10);
+ });
+ $('#w2ui-overlay .previous').on('mousedown', function () {
+ var tmp = obj.options.current.split('/');
+ tmp[0] = parseInt(tmp[0]) - 1;
+ refreshCalendar(tmp[0], tmp[1]);
+ });
+ $('#w2ui-overlay .next').on('mousedown', function () {
+ var tmp = obj.options.current.split('/');
+ tmp[0] = parseInt(tmp[0]) + 1;
+ refreshCalendar(tmp[0], tmp[1]);
+ });
+ }) (month, year);
+ }
+ // date
+ if (this.type == 'time') {
+ if ($(obj.el).attr('readonly')) return;
+ if ($('#w2ui-overlay').length == 0) {
+ $(obj.el).w2overlay('<div class="w2ui-reset w2ui-calendar-time" onclick="event.stopPropagation();"></div>', {
+ css: { "background-color": "#fff" }
+ });
+ }
+ var h24 = (this.options.format == 'h24' ? true : false);
+ $('#w2ui-overlay > div').html(obj.getHourHTML());
+ $('#w2ui-overlay .w2ui-time')
+ .on('mousedown', function (event) {
+ $(this).css({ 'background-color': '#B6D5FB', 'border-color': '#aaa' });
+ var hour = $(this).attr('hour');
+ $(obj.el).val((hour > 12 && !h24 ? hour - 12 : hour) + ':00' + (!h24 ? (hour < 12 ? ' am' : ' pm') : '')).change();
+ })
+ .on('mouseup', function () {
+ var hour = $(this).attr('hour');
+ if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay')[0].hide();
+ $(obj.el).w2overlay('<div class="w2ui-reset w2ui-calendar-time"></div>', { css: { "background-color": "#fff" } });
+ $('#w2ui-overlay > div').html(obj.getMinHTML(hour));
+ $('#w2ui-overlay .w2ui-time')
+ .on('mousedown', function () {
+ $(this).css({ 'background-color': '#B6D5FB', 'border-color': '#aaa' });
+ var min = $(this).attr('min');
+ $(obj.el).val((hour > 12 && !h24 ? hour - 12 : hour) + ':' + (min < 10 ? 0 : '') + min + (!h24 ? (hour < 12 ? ' am' : ' pm') : '')).change();
+ })
+ .on('mouseup', function () {
+ setTimeout(function () { if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay').removeData('keepOpen')[0].hide(); }, 10);
+ });
+ });
+ }
+ // list
+ if (['list', 'combo', 'enum'].indexOf(this.type) != -1) {
+ var el = this.el;
+ var input = this.el;
+ if (this.type == 'enum') {
+ el = $(this.helpers.multi);
+ input = $(el).find('input');
+ }
+ if (this.type == 'list') {
+ input = $(this.helpers.focus).find('input');
+ }
+ if ($(input).is(':focus')) {
+ if (options.openOnFocus === false && $(input).val() == '' && obj.tmp.force_open !== true) {
+ $().w2overlay();
+ return;
+ }
+ if (obj.tmp.force_hide) {
+ $().w2overlay();
+ setTimeout(function () {
+ delete obj.tmp.force_hide;
+ }, 1);
+ return;
+ }
+ if ($(input).val() != '') delete obj.tmp.force_open;
+ if ($('#w2ui-overlay').length == 0) options.index = 0;
+ var msgNoItems = w2utils.lang('No matches');
+ if (options.url != null && $(input).val().length < options.minLength && obj.tmp.emptySet !== true) msgNoItems = options.minLength + ' ' + w2utils.lang('letters or more...');
+ if (options.url != null && $(input).val() == '' && obj.tmp.emptySet !== true) msgNoItems = w2utils.lang('Type to search....');
+ $(el).w2menu('refresh', $.extend(true, {}, options, {
+ search : false,
+ render : options.renderDrop,
+ maxHeight : options.maxDropHeight,
+ msgNoItems : msgNoItems,
+ // selected with mouse
+ onSelect: function (event) {
+ if (obj.type == 'enum') {
+ var selected = $(obj.el).data('selected');
+ if (event.item) {
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'add', target: obj.el, originalEvent: event.originalEvent, item: event.item });
+ if (eventData.isCancelled === true) return;
+ // default behavior
+ if (selected.length >= options.max && options.max > 0) selected.pop();
+ delete event.item.hidden;
+ selected.push(event.item);
+ $(obj.el).data('selected', selected).change();
+ $(obj.helpers.multi).find('input').val('').width(20);
+ obj.refresh();
+ if ($("#w2ui-overlay").length > 0) $('#w2ui-overlay')[0].hide();
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }
+ } else {
+ $(obj.el).data('selected', event.item).val(event.item.text).change();
+ if (obj.helpers.focus) {
+ obj.helpers.focus.find('input').val('');
+ obj.refresh();
+ }
+ }
+ }
+ }));
+ }
+ }
+ },
+
+ inRange: function (str) {
+ var inRange = false;
+ if (this.type == 'date') {
+ var dt = w2utils.isDate(str, this.options.format, true);
+ if (dt) {
+ // enable range
+ if (this.options.start || this.options.end) {
+ var st = (typeof this.options.start == 'string' ? this.options.start : $(this.options.start).val());
+ var en = (typeof this.options.end == 'string' ? this.options.end : $(this.options.end).val());
+ var start = w2utils.isDate(st, this.options.format, true);
+ var end = w2utils.isDate(en, this.options.format, true);
+ var current = new Date(dt);
+ if (!start) start = current;
+ if (!end) end = current;
+ if (current >= start && current <= end) inRange = true;
+ } else {
+ inRange = true;
+ }
+ // block predefined dates
+ if (this.options.blocked && $.inArray(str, this.options.blocked) != -1) inRange = false;
+ }
+ }
+ if (this.type == 'time') {
+ if (this.options.start || this.options.end) {
+ var tm = this.toMin(str);
+ var tm1 = this.toMin(this.options.start);
+ var tm2 = this.toMin(this.options.end);
+ if (!tm1) tm1 = tm;
+ if (!tm2) tm2 = tm;
+ if (tm >= tm1 && tm <= tm2) inRange = true;
+ } else {
+ inRange = true;
+ }
+ }
+ return inRange;
+ },
+
+ /*
+ * INTERNAL FUNCTIONS
+ */
+
+ checkType: function (ch, loose) {
+ var obj = this;
+ switch (obj.type) {
+ case 'int':
+ if (loose && ['-'].indexOf(ch) != -1) return true;
+ return w2utils.isInt(ch.replace(obj.options.numberRE, ''));
+ case 'percent':
+ ch = ch.replace(/%/g, '');
+ case 'float':
+ if (loose && ['-','.'].indexOf(ch) != -1) return true;
+ return w2utils.isFloat(ch.replace(obj.options.numberRE, ''));
+ case 'money':
+ case 'currency':
+ if (loose && ['-', '.', obj.options.groupSymbol, obj.options.currencyPrefix, obj.options.currencySuffix].indexOf(ch) != -1) return true;
+ return w2utils.isFloat(ch.replace(obj.options.moneyRE, ''));
+ case 'hex':
+ case 'color':
+ return w2utils.isHex(ch);
+ case 'alphanumeric':
+ return w2utils.isAlphaNumeric(ch);
+ }
+ return true;
+ },
+
+ addPrefix: function () {
+ var obj = this;
+ setTimeout(function () {
+ if (obj.type === 'clear') return;
+ var helper;
+ var tmp = $(obj.el).data('tmp') || {};
+ if (tmp['old-padding-left']) $(obj.el).css('padding-left', tmp['old-padding-left']);
+ tmp['old-padding-left'] = $(obj.el).css('padding-left');
+ $(obj.el).data('tmp', tmp);
+ // remove if already displaed
+ if (obj.helpers.prefix) $(obj.helpers.prefix).remove();
+ if (obj.options.prefix !== '') {
+ // add fresh
+ $(obj.el).before(
+ '<div class="w2ui-field-helper">'+
+ obj.options.prefix +
+ '</div>'
+ );
+ helper = $(obj.el).prev();
+ helper
+ .css({
+ 'color' : $(obj.el).css('color'),
+ 'font-family' : $(obj.el).css('font-family'),
+ 'font-size' : $(obj.el).css('font-size'),
+ 'padding-top' : $(obj.el).css('padding-top'),
+ 'padding-bottom' : $(obj.el).css('padding-bottom'),
+ 'padding-left' : $(obj.el).css('padding-left'),
+ 'padding-right' : 0,
+ 'margin-top' : (parseInt($(obj.el).css('margin-top'), 10) + 2) + 'px',
+ 'margin-bottom' : (parseInt($(obj.el).css('margin-bottom'), 10) + 1) + 'px',
+ 'margin-left' : $(obj.el).css('margin-left'),
+ 'margin-right' : 0
+ })
+ .on('click', function (event) {
+ if (obj.options.icon && typeof obj.onIconClick == 'function') {
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'iconClick', target: obj.el, el: $(this).find('span.w2ui-icon')[0] });
+ if (eventData.isCancelled === true) return;
+
+ // intentionally empty
+
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ } else {
+ if (obj.type == 'list') {
+ $(obj.helpers.focus).find('input').focus();
+ } else {
+ $(obj.el).focus();
+ }
+ }
+ });
+ $(obj.el).css('padding-left', (helper.width() + parseInt($(obj.el).css('padding-left'), 10)) + 'px');
+ // remember helper
+ obj.helpers.prefix = helper;
+ }
+ }, 1);
+ },
+
+ addSuffix: function () {
+ var obj = this;
+ var helper, pr;
+ setTimeout(function () {
+ if (obj.type === 'clear') return;
+ var tmp = $(obj.el).data('tmp') || {};
+ if (tmp['old-padding-right']) $(obj.el).css('padding-right', tmp['old-padding-right']);
+ tmp['old-padding-right'] = $(obj.el).css('padding-right');
+ $(obj.el).data('tmp', tmp);
+ pr = parseInt($(obj.el).css('padding-right'), 10);
+ if (obj.options.arrows) {
+ // remove if already displaed
+ if (obj.helpers.arrows) $(obj.helpers.arrows).remove();
+ // add fresh
+ $(obj.el).after(
+ '<div class="w2ui-field-helper" style="border: 1px solid transparent">&nbsp;'+
+ ' <div class="w2ui-field-up" type="up">'+
+ ' <div class="arrow-up" type="up"></div>'+
+ ' </div>'+
+ ' <div class="w2ui-field-down" type="down">'+
+ ' <div class="arrow-down" type="down"></div>'+
+ ' </div>'+
+ '</div>');
+ var height = w2utils.getSize(obj.el, 'height');
+ helper = $(obj.el).next();
+ helper.css({
+ 'color' : $(obj.el).css('color'),
+ 'font-family' : $(obj.el).css('font-family'),
+ 'font-size' : $(obj.el).css('font-size'),
+ 'height' : ($(obj.el).height() + parseInt($(obj.el).css('padding-top'), 10) + parseInt($(obj.el).css('padding-bottom'), 10) ) + 'px',
+ 'padding' : 0,
+ 'margin-top' : (parseInt($(obj.el).css('margin-top'), 10) + 1) + 'px',
+ 'margin-bottom' : 0,
+ 'border-left' : '1px solid silver'
+ })
+ .css('margin-left', '-'+ (helper.width() + parseInt($(obj.el).css('margin-right'), 10) + 12) + 'px')
+ .on('mousedown', function (event) {
+ $('body').on('mouseup', tmp);
+ $('body').data('_field_update_timer', setTimeout(update, 700));
+ update(false);
+ // timer function
+ function tmp() {
+ clearTimeout($('body').data('_field_update_timer'));
+ $('body').off('mouseup', tmp);
+ }
+ // update function
+ function update(notimer) {
+ $(obj.el).focus();
+ obj.keyDown($.Event("keydown"), {
+ keyCode : ($(event.target).attr('type') == 'up' ? 38 : 40)
+ });
+ if (notimer !== false) $('body').data('_field_update_timer', setTimeout(update, 60));
+ }
+ });
+ pr += helper.width() + 12;
+ $(obj.el).css('padding-right', pr + 'px');
+ // remember helper
+ obj.helpers.arrows = helper;
+ }
+ if (obj.options.suffix !== '') {
+ // remove if already displaed
+ if (obj.helpers.suffix) $(obj.helpers.suffix).remove();
+ // add fresh
+ $(obj.el).after(
+ '<div class="w2ui-field-helper">'+
+ obj.options.suffix +
+ '</div>');
+ helper = $(obj.el).next();
+ helper
+ .css({
+ 'color' : $(obj.el).css('color'),
+ 'font-family' : $(obj.el).css('font-family'),
+ 'font-size' : $(obj.el).css('font-size'),
+ 'padding-top' : $(obj.el).css('padding-top'),
+ 'padding-bottom' : $(obj.el).css('padding-bottom'),
+ 'padding-left' : '3px',
+ 'padding-right' : $(obj.el).css('padding-right'),
+ 'margin-top' : (parseInt($(obj.el).css('margin-top'), 10) + 2) + 'px',
+ 'margin-bottom' : (parseInt($(obj.el).css('margin-bottom'), 10) + 1) + 'px'
+ })
+ .on('click', function (event) {
+ if (obj.type == 'list') {
+ $(obj.helpers.focus).find('input').focus();
+ } else {
+ $(obj.el).focus();
+ }
+ });
+
+ helper.css('margin-left', '-'+ (w2utils.getSize(helper, 'width') + parseInt($(obj.el).css('margin-right'), 10) + 2) + 'px');
+ pr += helper.width() + 3;
+ $(obj.el).css('padding-right', pr + 'px');
+ // remember helper
+ obj.helpers.suffix = helper;
+ }
+ }, 1);
+ },
+
+ addFocus: function () {
+ var obj = this;
+ var options = this.options;
+ var width = 0; // 11 - show search icon, 0 do not show
+ // clean up & init
+ $(obj.helpers.focus).remove();
+ // build helper
+ var html =
+ '<div class="w2ui-field-helper">'+
+ ' <div class="w2ui-icon icon-search"></div>'+
+ ' <input type="text" autocomplete="off">'+
+ '<div>';
+ $(obj.el).attr('tabindex', -1).before(html);
+ var helper = $(obj.el).prev();
+ obj.helpers.focus = helper;
+ helper.css({
+ width : $(obj.el).width(),
+ "margin-top" : $(obj.el).css('margin-top'),
+ "margin-left" : (parseInt($(obj.el).css('margin-left')) + parseInt($(obj.el).css('padding-left'))) + 'px',
+ "margin-bottom" : $(obj.el).css('margin-bottom'),
+ "margin-right" : $(obj.el).css('margin-right')
+ })
+ .find('input')
+ .css({
+ cursor : 'default',
+ width : '100%',
+ outline : 'none',
+ opacity : 1,
+ margin : 0,
+ border : '1px solid transparent',
+ padding : $(obj.el).css('padding-top'),
+ "padding-left" : 0,
+ "margin-left" : (width > 0 ? width + 6 : 0),
+ "background-color" : 'transparent'
+ });
+ // INPUT events
+ helper.find('input')
+ .on('click', function (event) {
+ if ($('#w2ui-overlay').length == 0) obj.focus(event);
+ event.stopPropagation();
+ })
+ .on('focus', function (event) {
+ $(obj.el).css({ 'outline': 'auto 5px #7DB4F3', 'outline-offset': '-2px' });
+ $(this).val('');
+ $(obj.el).triggerHandler('focus');
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ })
+ .on('blur', function (event) {
+ $(obj.el).css('outline', 'none');
+ $(this).val('');
+ obj.refresh();
+ $(obj.el).triggerHandler('blur');
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ })
+ .on('keyup', function (event) { obj.keyUp(event) })
+ .on('keydown', function (event) { obj.keyDown(event) })
+ .on('keypress', function (event) { obj.keyPress(event); });
+ // MAIN div
+ helper.on('click', function (event) { $(this).find('input').focus(); });
+ obj.refresh();
+ },
+
+ addMulti: function () {
+ var obj = this;
+ var options = this.options;
+ // clean up & init
+ $(obj.helpers.multi).remove();
+ // build helper
+ var html = '';
+ var margin =
+ 'margin-top : 0px; ' +
+ 'margin-bottom : 0px; ' +
+ 'margin-left : ' + $(obj.el).css('margin-left') + '; ' +
+ 'margin-right : ' + $(obj.el).css('margin-right') + '; '+
+ 'width : ' + (w2utils.getSize(obj.el, 'width')
+ - parseInt($(obj.el).css('margin-left'), 10)
+ - parseInt($(obj.el).css('margin-right'), 10))
+ + 'px;';
+ if (obj.type == 'enum') {
+ html = '<div class="w2ui-field-helper w2ui-list" style="'+ margin + '; box-sizing: border-box">'+
+ ' <div style="padding: 0px; margin: 0px; margin-right: 20px; display: inline-block">'+
+ ' <ul>'+
+ ' <li style="padding-left: 0px; padding-right: 0px" class="nomouse">'+
+ ' <input type="text" style="width: 20px" autocomplete="off" '+ ($(obj.el).attr('readonly') ? 'readonly': '') + '>'+
+ ' </li>'
+ ' </ul>'+
+ ' </div>'+
+ '</div>';
+ }
+ if (obj.type == 'file') {
+ html = '<div class="w2ui-field-helper w2ui-list" style="'+ margin + '; box-sizing: border-box">'+
+ ' <div style="padding: 0px; margin: 0px; margin-right: 20px; display: inline-block">'+
+ ' <ul><li style="padding-left: 0px; padding-right: 0px" class="nomouse"></li></ul>'+
+ ' <input class="file-input" type="file" name="attachment" multiple style="display: none" tabindex="-1">'
+ ' </div>'+
+ '</div>';
+ }
+ $(obj.el)
+ .before(html)
+ .css({
+ 'background-color' : 'transparent',
+ 'border-color' : 'transparent'
+ });
+
+ var div = $(obj.el).prev();
+ obj.helpers.multi = div;
+ if (obj.type == 'enum') {
+ $(obj.el).attr('tabindex', -1);
+ // INPUT events
+ div.find('input')
+ .on('click', function (event) {
+ if ($('#w2ui-overlay').length == 0) obj.focus(event);
+ $(obj.el).triggerHandler('click');
+ })
+ .on('focus', function (event) {
+ $(div).css({ 'outline': 'auto 5px #7DB4F3', 'outline-offset': '-2px' });
+ $(obj.el).triggerHandler('focus');
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ })
+ .on('blur', function (event) {
+ $(div).css('outline', 'none');
+ $(obj.el).triggerHandler('blur');
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
+ })
+ .on('keyup', function (event) { obj.keyUp(event) })
+ .on('keydown', function (event) { obj.keyDown(event) })
+ .on('keypress', function (event) { div.find('.w2ui-enum-placeholder').remove(); obj.keyPress(event); });
+ // MAIN div
+ div.on('click', function (event) { $(this).find('input').focus(); });
+ }
+ if (obj.type == 'file') {
+ $(obj.el).css('outline', 'none');
+ div.on('click', function (event) {
+ $(obj.el).focus();
+ if ($(obj.el).attr('readonly')) return;
+ obj.blur(event);
+ div.find('input').click();
+ })
+ .on('dragenter', function (event) {
+ if ($(obj.el).attr('readonly')) return;
+ $(div).addClass('w2ui-file-dragover');
+ })
+ .on('dragleave', function (event) {
+ if ($(obj.el).attr('readonly')) return;
+ var tmp = $(event.target).parents('.w2ui-field-helper');
+ if (tmp.length == 0) $(div).removeClass('w2ui-file-dragover');
+ })
+ .on('drop', function (event) {
+ if ($(obj.el).attr('readonly')) return;
+ $(div).removeClass('w2ui-file-dragover');
+ var files = event.originalEvent.dataTransfer.files;
+ for (var i=0, l=files.length; i<l; i++) obj.addFile.call(obj, files[i]);
+ // cancel to stop browser behaviour
+ event.preventDefault();
+ event.stopPropagation();
+ })
+ .on('dragover', function (event) {
+ // cancel to stop browser behaviour
+ event.preventDefault();
+ event.stopPropagation();
+ });
+ div.find('input')
+ .on('click', function (event) {
+ event.stopPropagation();
+ })
+ .on('change', function () {
+ if (typeof this.files !== "undefined") {
+ for (var i = 0, l = this.files.length; i < l; i++) {
+ obj.addFile.call(obj, this.files[i]);
+ }
+ }
+ });
+ }
+ obj.refresh();
+ },
+
+ addFile: function (file) {
+ var obj = this;
+ var options = this.options;
+ var selected = $(obj.el).data('selected');
+ var newItem = {
+ name : file.name,
+ type : file.type,
+ modified : file.lastModifiedDate,
+ size : file.size,
+ content : null
+ };
+ var size = 0;
+ var cnt = 0;
+ var err;
+ for (var s in selected) { size += selected[s].size; cnt++; }
+ // trigger event
+ var eventData = obj.trigger({ phase: 'before', type: 'add', target: obj.el, file: newItem, total: cnt, totalSize: size });
+ if (eventData.isCancelled === true) return;
+ // check params
+ if (options.maxFileSize !== 0 && newItem.size > options.maxFileSize) {
+ err = 'Maximum file size is '+ w2utils.size(options.maxFileSize);
+ if (options.silent === false) $(obj.el).w2tag(err);
+ console.log('ERROR: '+ err);
+ return;
+ }
+ if (options.maxSize !== 0 && size + newItem.size > options.maxSize) {
+ err = 'Maximum total size is '+ w2utils.size(options.maxSize);
+ if (options.silent === false) $(obj.el).w2tag(err);
+ console.log('ERROR: '+ err);
+ return;
+ }
+ if (options.max !== 0 && cnt >= options.max) {
+ err = 'Maximum number of files is '+ options.max;
+ if (options.silent === false) $(obj.el).w2tag(err);
+ console.log('ERROR: '+ err);
+ return;
+ }
+ selected.push(newItem);
+ // read file as base64
+ if (typeof FileReader !== "undefined") {
+ var reader = new FileReader();
+ // need a closure
+ reader.onload = (function () {
+ return function (event) {
+ var fl = event.target.result;
+ var ind = fl.indexOf(',');
+ newItem.content = fl.substr(ind+1);
+ obj.refresh();
+ $(obj.el).trigger('change');
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ };
+ })();
+ reader.readAsDataURL(file);
+ } else {
+ obj.refresh();
+ $(obj.el).trigger('change');
+ }
+ },
+
+ normMenu: function (menu) {
+ if ($.isArray(menu)) {
+ for (var m = 0; m < menu.length; m++) {
+ if (typeof menu[m] == 'string') {
+ menu[m] = { id: menu[m], text: menu[m] };
+ } else {
+ if (typeof menu[m].text != 'undefined' && typeof menu[m].id == 'undefined') menu[m].id = menu[m].text;
+ if (typeof menu[m].text == 'undefined' && typeof menu[m].id != 'undefined') menu[m].text = menu[m].id;
+ if (typeof menu[m].caption != 'undefined') menu[m].text = menu[m].caption;
+ }
+ }
+ return menu;
+ } else if (typeof menu == 'object') {
+ var tmp = []
+ for (var m in menu) tmp.push({ id: m, text: menu[m] });
+ return tmp;
+ }
+ },
+
+ getColorHTML: function () {
+ var html = '<div class="w2ui-color">'+
+ '<table cellspacing="5">';
+ for (var i = 0; i < 8; i++) {
+ html += '<tr>';
+ for (var j = 0; j < 8; j++) {
+ html += '<td>'+
+ ' <div class="color" style="background-color: #'+ this.pallete[i][j] +';" name="'+ this.pallete[i][j] +'" index="'+ i + ':' + j +'">'+
+ ' '+ ($(this.el).val() == this.pallete[i][j] ? '&#149;' : '&nbsp;')+
+ ' </div>'+
+ '</td>';
+ }
+ html += '</tr>';
+ if (i < 2) html += '<tr><td style="height: 8px" colspan="8"></td></tr>';
+ }
+ html += '</table></div>';
+ return html;
+ },
+
+ getMonthHTML: function (month, year) {
+ var td = new Date();
+ var months = w2utils.settings.fullmonths;
+ var days = w2utils.settings.fulldays;
+ var daysCount = ['31', '28', '31', '30', '31', '30', '31', '31', '30', '31', '30', '31'];
+ var today = td.getFullYear() + '/' + (Number(td.getMonth()) + 1) + '/' + td.getDate();
+ // normalize date
+ year = w2utils.isInt(year) ? parseInt(year) : td.getFullYear();
+ month = w2utils.isInt(month) ? parseInt(month) : td.getMonth() + 1;
+ if (month > 12) { month -= 12; year++; }
+ if (month < 1 || month === 0) { month += 12; year--; }
+ if (year/4 == Math.floor(year/4)) { daysCount[1] = '29'; } else { daysCount[1] = '28'; }
+ this.options.current = month + '/' + year;
+
+ // start with the required date
+ td = new Date(year, month-1, 1);
+ var weekDay = td.getDay();
+ var tabDays = w2utils.settings.shortdays;
+ var dayTitle = '';
+ for ( var i = 0, len = tabDays.length; i < len; i++) {
+ dayTitle += '<td>' + tabDays[i] + '</td>';
+ }
+ var html =
+ '<div class="w2ui-calendar-title title">'+
+ ' <div class="w2ui-calendar-previous previous"> <div></div> </div>'+
+ ' <div class="w2ui-calendar-next next"> <div></div> </div> '+
+ months[month-1] +', '+ year +
+ '</div>'+
+ '<table class="w2ui-calendar-days" cellspacing="0">'+
+ ' <tr class="w2ui-day-title">' + dayTitle + '</tr>'+
+ ' <tr>';
+
+ var day = 1;
+ for (var ci=1; ci<43; ci++) {
+ if (weekDay === 0 && ci == 1) {
+ for (var ti=0; ti<6; ti++) html += '<td class="w2ui-day-empty">&nbsp;</td>';
+ ci += 6;
+ } else {
+ if (ci < weekDay || day > daysCount[month-1]) {
+ html += '<td class="w2ui-day-empty">&nbsp;</td>';
+ if ((ci) % 7 === 0) html += '</tr><tr>';
+ continue;
+ }
+ }
+ var dt = year + '/' + month + '/' + day;
+
+ var className = '';
+ if (ci % 7 == 6) className = ' w2ui-saturday';
+ if (ci % 7 === 0) className = ' w2ui-sunday';
+ if (dt == today) className += ' w2ui-today';
+
+ var dspDay = day;
+ var col = '';
+ var bgcol = '';
+ var tmp_dt = w2utils.formatDate(dt, this.options.format);
+ if (this.options.colored && this.options.colored[tmp_dt] !== undefined) { // if there is predefined colors for dates
+ tmp = this.options.colored[tmp_dt].split(':');
+ bgcol = 'background-color: ' + tmp[0] + ';';
+ col = 'color: ' + tmp[1] + ';';
+ }
+ html += '<td class="'+ (this.inRange(tmp_dt) ? 'w2ui-date ' : 'w2ui-blocked') + className + '" style="'+ col + bgcol + '" date="'+ tmp_dt +'">'+
+ dspDay +
+ '</td>';
+ if (ci % 7 === 0 || (weekDay === 0 && ci == 1)) html += '</tr><tr>';
+ day++;
+ }
+ html += '</tr></table>';
+ return html;
+ },
+
+ getYearHTML: function () {
+ var months = w2utils.settings.shortmonths;
+ var mhtml = '';
+ var yhtml = '';
+ for (var m in months) {
+ mhtml += '<div class="w2ui-jump-month" name="'+ m +'">'+ months[m] + '</div>';
+ }
+ for (var y = 1950; y <= 2020; y++) {
+ yhtml += '<div class="w2ui-jump-year" name="'+ y +'">'+ y + '</div>'
+ }
+ return '<div>'+ mhtml +'</div><div>'+ yhtml +'</div>';
+ },
+
+ getHourHTML: function () {
+ var tmp = [];
+ var h24 = (this.options.format == 'h24' ? true : false);
+ for (var a=0; a<24; a++) {
+ var time = (a >= 12 && !h24 ? a - 12 : a) + ':00' + (!h24 ? (a < 12 ? ' am' : ' pm') : '');
+ if (a == 12 && !h24) time = '12:00 pm';
+ if (!tmp[Math.floor(a/8)]) tmp[Math.floor(a/8)] = '';
+ var tm1 = this.fromMin(this.toMin(time));
+ var tm2 = this.fromMin(this.toMin(time) + 59);
+ tmp[Math.floor(a/8)] += '<div class="'+ (this.inRange(tm1) || this.inRange(tm2) ? 'w2ui-time ' : 'w2ui-blocked') + '" hour="'+ a +'">'+ time +'</div>';
+ }
+ var html =
+ '<div class="w2ui-calendar-time"><table><tr>'+
+ ' <td>'+ tmp[0] +'</td>' +
+ ' <td>'+ tmp[1] +'</td>' +
+ ' <td>'+ tmp[2] +'</td>' +
+ '</tr></table></div>';
+ return html;
+ },
+
+ getMinHTML: function (hour) {
+ if (typeof hour == 'undefined') hour = 0;
+ var h24 = (this.options.format == 'h24' ? true : false);
+ var tmp = [];
+ for (var a=0; a<60; a+=5) {
+ var time = (hour > 12 && !h24 ? hour - 12 : hour) + ':' + (a < 10 ? 0 : '') + a + ' ' + (!h24 ? (hour < 12 ? 'am' : 'pm') : '');
+ var ind = a < 20 ? 0 : (a < 40 ? 1 : 2);
+ if (!tmp[ind]) tmp[ind] = '';
+ tmp[ind] += '<div class="'+ (this.inRange(time) ? 'w2ui-time ' : 'w2ui-blocked') + '" min="'+ a +'">'+ time +'</div>';
+ }
+ var html =
+ '<div class="w2ui-calendar-time"><table><tr>'+
+ ' <td>'+ tmp[0] +'</td>' +
+ ' <td>'+ tmp[1] +'</td>' +
+ ' <td>'+ tmp[2] +'</td>' +
+ '</tr></table></div>';
+ return html;
+ },
+
+ toMin: function (str) {
+ if (typeof str != 'string') return null;
+ var tmp = str.split(':');
+ if (tmp.length == 2) {
+ tmp[0] = parseInt(tmp[0]);
+ tmp[1] = parseInt(tmp[1]);
+ if (str.indexOf('pm') != -1 && tmp[0] != 12) tmp[0] += 12;
+ } else {
+ return null;
+ }
+ return tmp[0] * 60 + tmp[1];
+ },
+
+ fromMin: function (time) {
+ var ret = '';
+ if (time >= 24 * 60) time = time % (24 * 60);
+ if (time < 0) time = 24 * 60 + time;
+ var hour = Math.floor(time/60);
+ var min = ((time % 60) < 10 ? '0' : '') + (time % 60);
+ if (this.options.format.indexOf('h24') != -1) {
+ ret = hour + ':' + min;
+ } else {
+ ret = (hour <= 12 ? hour : hour - 12) + ':' + min + ' ' + (hour >= 12 ? 'pm' : 'am');
+ }
+ return ret;
+ }
+ }
+
+ $.extend(w2field.prototype, w2utils.event);
+ w2obj.field = w2field;
+
+}) (jQuery);
+
+/************************************************************************
+* Library: Web 2.0 UI for jQuery (using prototypical inheritance)
+* - Following objects defined
+* - w2form - form widget
+* - $().w2form - jQuery wrapper
+* - Dependencies: jQuery, w2utils, w2fields, w2tabs, w2toolbar, w2alert
+*
+* == NICE TO HAVE ==
+* - refresh(field) - would refresh only one field
+* - include delta on save
+* - create an example how to do cascadic dropdown
+* - form should read <select> <options> into items
+* - two way data bindings
+* - verify validation of fields
+* - when field is blank, set record.field = null
+* - show/hide a field
+* - added getChanges() - not complete
+*
+************************************************************************/
+
+
+(function () {
+ var w2form = function(options) {
+ // public properties
+ this.name = null;
+ this.header = '';
+ this.box = null; // HTML element that hold this element
+ this.url = '';
+ this.routeData = {}; // data for dynamic routes
+ this.formURL = ''; // url where to get form HTML
+ this.formHTML = ''; // form HTML (might be loaded from the url)
+ this.page = 0; // current page
+ this.recid = 0; // can be null or 0
+ this.fields = [];
+ this.actions = {};
+ this.record = {};
+ this.original = {};
+ this.postData = {};
+ this.toolbar = {}; // if not empty, then it is toolbar
+ this.tabs = {}; // if not empty, then it is tabs object
+
+ this.style = '';
+ this.focus = 0; // focus first or other element
+ this.msgNotJSON = w2utils.lang('Return data is not in JSON format.');
+ this.msgAJAXerror = w2utils.lang('AJAX error. See console for more details.');
+ this.msgRefresh = w2utils.lang('Refreshing...');
+ this.msgSaving = w2utils.lang('Saving...');
+
+ // events
+ this.onRequest = null;
+ this.onLoad = null;
+ this.onValidate = null;
+ this.onSubmit = null;
+ this.onSave = null;
+ this.onChange = null;
+ this.onRender = null;
+ this.onRefresh = null;
+ this.onResize = null;
+ this.onDestroy = null;
+ this.onAction = null;
+ this.onToolbar = null;
+ this.onError = null;
+
+ // internal
+ this.isGenerated = false;
+ this.last = {
+ xhr: null // jquery xhr requests
+ }
+
+ $.extend(true, this, w2obj.form, options);
+ };
+
+ // ====================================================
+ // -- Registers as a jQuery plugin
+
+ $.fn.w2form = function(method) {
+ if (typeof method === 'object' || !method ) {
+ var obj = this;
+ // check name parameter
+ if (!w2utils.checkName(method, 'w2form')) return;
+ // remember items
+ var record = method.record;
+ var original = method.original;
+ var fields = method.fields;
+ var toolbar = method.toolbar;
+ var tabs = method.tabs;
+ // extend items
+ var object = new w2form(method);
+ $.extend(object, { record: {}, original: {}, fields: [], tabs: {}, toolbar: {}, handlers: [] });
+ if ($.isArray(tabs)) {
+ $.extend(true, object.tabs, { tabs: [] });
+ for (var t in tabs) {
+ var tmp = tabs[t];
+ if (typeof tmp === 'object') object.tabs.tabs.push(tmp); else object.tabs.tabs.push({ id: tmp, caption: tmp });
+ }
+ } else {
+ $.extend(true, object.tabs, tabs);
+ }
+ $.extend(true, object.toolbar, toolbar);
+ // reassign variables
+ for (var p in fields) {
+ var field = $.extend(true, {}, fields[p]);
+ if (typeof field.name == 'undefined' && typeof field.field != 'undefined') field.name = field.field;
+ if (typeof field.field == 'undefined' && typeof field.name != 'undefined') field.field = field.name;
+ object.fields[p] = field;
+ }
+ for (var p in record) {
+ if ($.isPlainObject(record[p])) {
+ object.record[p] = $.extend(true, {}, record[p]);
+ } else {
+ object.record[p] = record[p];
+ }
+ }
+ for (var p in original) {
+ if ($.isPlainObject(original[p])) {
+ object.original[p] = $.extend(true, {}, original[p]);
+ } else {
+ object.original[p] = original[p];
+ }
+ }
+ if (obj.length > 0) object.box = obj[0];
+ // render if necessary
+ if (object.formURL != '') {
+ $.get(object.formURL, function (data) { // should always be $.get as it is template
+ object.formHTML = data;
+ object.isGenerated = true;
+ if ($(object.box).length != 0 || data.length != 0) {
+ $(object.box).html(data);
+ object.render(object.box);
+ }
+ });
+ } else if (object.formHTML != '') {
+ // it is already loaded into formHTML
+ } else if ($(this).length != 0 && $.trim($(this).html()) != '') {
+ object.formHTML = $(this).html();
+ } else { // try to generate it
+ object.formHTML = object.generateHTML();
+ }
+ // register new object
+ w2ui[object.name] = object;
+ // render if not loaded from url
+ if (object.formURL == '') {
+ if (String(object.formHTML).indexOf('w2ui-page') == -1) {
+ object.formHTML = '<div class="w2ui-page page-0">'+ object.formHTML +'</div>';
+ }
+ $(object.box).html(object.formHTML);
+ object.isGenerated = true;
+ object.render(object.box);
+ }
+ return object;
+
+ } else if (w2ui[$(this).attr('name')]) {
+ var obj = w2ui[$(this).attr('name')];
+ obj[method].apply(obj, Array.prototype.slice.call(arguments, 1));
+ return this;
+ } else {
+ console.log('ERROR: Method ' + method + ' does not exist on jQuery.w2form');
+ }
+ };
+
+ // ====================================================
+ // -- Implementation of core functionality
+
+ w2form.prototype = {
+
+ get: function (field, returnIndex) {
+ if (arguments.length === 0) {
+ var all = [];
+ for (var f1 in this.fields) {
+ if (this.fields[f1].name != null) all.push(this.fields[f1].name);
+ }
+ return all;
+ } else {
+ for (var f2 in this.fields) {
+ if (this.fields[f2].name == field) {
+ if (returnIndex === true) return f2; else return this.fields[f2];
+ }
+ }
+ return null;
+ }
+ },
+
+ set: function (field, obj) {
+ for (var f in this.fields) {
+ if (this.fields[f].name == field) {
+ $.extend(this.fields[f] , obj);
+ this.refresh();
+ return true;
+ }
+ }
+ return false;
+ },
+
+ reload: function (callBack) {
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (url && this.recid != 0) {
+ // this.clear();
+ this.request(callBack);
+ } else {
+ // this.refresh(); // no need to refresh
+ if (typeof callBack == 'function') callBack();
+ }
+ },
+
+ clear: function () {
+ this.recid = 0;
+ this.record = {};
+ $().w2tag();
+ this.refresh();
+ },
+
+ error: function (msg) {
+ var obj = this;
+ // let the management of the error outside of the grid
+ var eventData = this.trigger({ target: this.name, type: 'error', message: msg , xhr: this.last.xhr });
+ if (eventData.isCancelled === true) {
+ if (typeof callBack == 'function') callBack();
+ return;
+ }
+ // need a time out because message might be already up)
+ setTimeout(function () { w2alert(msg, 'Error'); }, 1);
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ validate: function (showErrors) {
+ if (typeof showErrors == 'undefined') showErrors = true;
+ $().w2tag(); // hide all tags before validating
+ // validate before saving
+ var errors = [];
+ for (var f in this.fields) {
+ var field = this.fields[f];
+ if (this.record[field.name] == null) this.record[field.name] = '';
+ switch (field.type) {
+ case 'int':
+ if (this.record[field.name] && !w2utils.isInt(this.record[field.name])) {
+ errors.push({ field: field, error: w2utils.lang('Not an integer') });
+ }
+ break;
+ case 'float':
+ if (this.record[field.name] && !w2utils.isFloat(this.record[field.name])) {
+ errors.push({ field: field, error: w2utils.lang('Not a float') });
+ }
+ break;
+ case 'money':
+ if (this.record[field.name] && !w2utils.isMoney(this.record[field.name])) {
+ errors.push({ field: field, error: w2utils.lang('Not in money format') });
+ }
+ break;
+ case 'color':
+ case 'hex':
+ if (this.record[field.name] && !w2utils.isHex(this.record[field.name])) {
+ errors.push({ field: field, error: w2utils.lang('Not a hex number') });
+ }
+ break;
+ case 'email':
+ if (this.record[field.name] && !w2utils.isEmail(this.record[field.name])) {
+ errors.push({ field: field, error: w2utils.lang('Not a valid email') });
+ }
+ break;
+ case 'checkbox':
+ // convert true/false
+ if (this.record[field.name] == true) this.record[field.name] = 1; else this.record[field.name] = 0;
+ break;
+ case 'date':
+ // format date before submit
+ if (!field.options.format) field.options.format = w2utils.settings.date_format;
+ if (this.record[field.name] && !w2utils.isDate(this.record[field.name], field.options.format)) {
+ errors.push({ field: field, error: w2utils.lang('Not a valid date') + ': ' + field.options.format });
+ } else {
+ }
+ break;
+ case 'list':
+ case 'combo':
+ break;
+ case 'enum':
+ break;
+ }
+ // === check required - if field is '0' it should be considered not empty
+ var val = this.record[field.name];
+ if (field.required && (val === '' || ($.isArray(val) && val.length == 0) || ($.isPlainObject(val) && $.isEmptyObject(val)))) {
+ errors.push({ field: field, error: w2utils.lang('Required field') });
+ }
+ if (field.equalto && this.record[field.name] != this.record[field.equalto]) {
+ errors.push({ field: field, error: w2utils.lang('Field should be equal to ') + field.equalto });
+ }
+ }
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'validate', errors: errors });
+ if (eventData.isCancelled === true) return;
+ // show error
+ if (showErrors) for (var e in eventData.errors) {
+ var err = eventData.errors[e];
+ if (err.field.type == 'radio') { // for radio and checkboxes
+ $($(err.field.el).parents('div')[0]).w2tag(err.error, { "class": 'w2ui-error' });
+ } else if (['enum', 'file'].indexOf(err.field.type) != -1) {
+ (function (err) {
+ setTimeout(function () {
+ var fld = $(err.field.el).data('w2field').helpers.multi;
+ $(err.field.el).w2tag(err.error);
+ $(fld).addClass('w2ui-error');
+ }, 1);
+ })(err);
+ } else {
+ $(err.field.el).w2tag(err.error, { "class": 'w2ui-error' });
+ }
+ this.goto(errors[0].field.page);
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ return errors;
+ },
+
+ getChanges: function () {
+ var differ = function(record, original, result) {
+ for (var i in record) {
+ if (typeof record[i] == "object") {
+ result[i] = differ(record[i], original[i] || {}, {});
+ if (!result[i] || $.isEmptyObject(result[i])) delete result[i];
+ } else if (record[i] != original[i]) {
+ result[i] = record[i];
+ }
+ }
+ return result;
+ }
+ return differ(this.record, this.original, {});
+ },
+
+ request: function (postData, callBack) { // if (1) param then it is call back if (2) then postData and callBack
+ var obj = this;
+ // check for multiple params
+ if (typeof postData == 'function') {
+ callBack = postData;
+ postData = null;
+ }
+ if (typeof postData == 'undefined' || postData == null) postData = {};
+ if (!this.url || (typeof this.url == 'object' && !this.url.get)) return;
+ if (this.recid == null || typeof this.recid == 'undefined') this.recid = 0;
+ // build parameters list
+ var params = {};
+ // add list params
+ params['cmd'] = 'get-record';
+ params['recid'] = this.recid;
+ // append other params
+ $.extend(params, this.postData);
+ $.extend(params, postData);
+ // event before
+ var eventData = this.trigger({ phase: 'before', type: 'request', target: this.name, url: this.url, postData: params });
+ if (eventData.isCancelled === true) { if (typeof callBack == 'function') callBack({ status: 'error', message: 'Request aborted.' }); return; }
+ // default action
+ this.record = {};
+ this.original = {};
+ // call server to get data
+ this.lock(this.msgRefresh);
+ var url = eventData.url;
+ if (typeof eventData.url == 'object' && eventData.url.get) url = eventData.url.get;
+ if (this.last.xhr) try { this.last.xhr.abort(); } catch (e) {};
+ // process url with routeData
+ if (!$.isEmptyObject(obj.routeData)) {
+ var info = w2utils.parseRoute(url);
+ if (info.keys.length > 0) {
+ for (var k = 0; k < info.keys.length; k++) {
+ if (obj.routeData[info.keys[k].name] == null) continue;
+ url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), obj.routeData[info.keys[k].name]);
+ }
+ }
+ }
+ var ajaxOptions = {
+ type : 'POST',
+ url : url,
+ data : eventData.postData,
+ dataType : 'text' // expected from server
+ };
+ if (w2utils.settings.dataType == 'HTTP') {
+ ajaxOptions.data = String($.param(ajaxOptions.data, false)).replace(/%5B/g, '[').replace(/%5D/g, ']');
+ }
+ if (w2utils.settings.dataType == 'RESTFULL') {
+ ajaxOptions.type = 'GET';
+ ajaxOptions.data = String($.param(ajaxOptions.data, false)).replace(/%5B/g, '[').replace(/%5D/g, ']');
+ }
+ if (w2utils.settings.dataType == 'JSON') {
+ ajaxOptions.type = 'POST';
+ ajaxOptions.data = JSON.stringify(ajaxOptions.data);
+ ajaxOptions.contentType = 'application/json';
+ }
+ this.last.xhr = $.ajax(ajaxOptions)
+ .done(function (data, status, xhr) {
+ obj.unlock();
+ // event before
+ var eventData = obj.trigger({ phase: 'before', target: obj.name, type: 'load', xhr: xhr });
+ if (eventData.isCancelled === true) {
+ if (typeof callBack == 'function') callBack({ status: 'error', message: 'Request aborted.' });
+ return;
+ }
+ // parse server response
+ var data;
+ var responseText = obj.last.xhr.responseText;
+ if (status != 'error') {
+ // default action
+ if (typeof responseText != 'undefined' && responseText != '') {
+ // check if the onLoad handler has not already parsed the data
+ if (typeof responseText == "object") {
+ data = responseText;
+ } else {
+ // $.parseJSON or $.getJSON did not work because those expect perfect JSON data - where everything is in double quotes
+ //
+ // TODO: avoid (potentially malicious) code injection from the response.
+ try { eval('data = '+ responseText); } catch (e) { }
+ }
+ if (typeof data == 'undefined') {
+ data = {
+ status : 'error',
+ message : obj.msgNotJSON,
+ responseText : responseText
+ }
+ }
+ if (data['status'] == 'error') {
+ obj.error(data['message']);
+ } else {
+ obj.record = $.extend({}, data.record);
+ obj.original = $.extend({}, data.record);
+ }
+ }
+ } else {
+ obj.error('AJAX Error ' + xhr.status + ': '+ xhr.statusText);
+ data = {
+ status : 'error',
+ message : obj.msgAJAXerror,
+ responseText : responseText
+ };
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ obj.refresh();
+ // call back
+ if (typeof callBack == 'function') callBack(data);
+ })
+ .fail(function (xhr, status, error) {
+ // trigger event
+ var errorObj = { status: status, error: error, rawResponseText: xhr.responseText };
+ var eventData2 = obj.trigger({ phase: 'before', type: 'error', error: errorObj, xhr: xhr });
+ if (eventData2.isCancelled === true) return;
+ // default behavior
+ if (status != 'abort') {
+ var data;
+ try { data = $.parseJSON(xhr.responseText) } catch (e) {}
+ console.log('ERROR: Server communication failed.',
+ '\n EXPECTED:', { status: 'success', items: [{ id: 1, text: 'item' }] },
+ '\n OR:', { status: 'error', message: 'error message' },
+ '\n RECEIVED:', typeof data == 'object' ? data : xhr.responseText);
+ }
+ // event after
+ obj.trigger($.extend(eventData2, { phase: 'after' }));
+ });
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ submit: function (postData, callBack) {
+ return this.save(postData, callBack);
+ },
+
+ save: function (postData, callBack) {
+ var obj = this;
+ $(this.box).find(':focus').change(); // trigger onchange
+ // check for multiple params
+ if (typeof postData == 'function') {
+ callBack = postData;
+ postData = null;
+ }
+ // validation
+ var errors = obj.validate(true);
+ if (errors.length !== 0) return;
+ // submit save
+ if (typeof postData == 'undefined' || postData == null) postData = {};
+ if (!obj.url || (typeof obj.url == 'object' && !obj.url.save)) {
+ console.log("ERROR: Form cannot be saved because no url is defined.");
+ return;
+ }
+ obj.lock(obj.msgSaving + ' <span id="'+ obj.name +'_progress"></span>');
+ // need timer to allow to lock
+ setTimeout(function () {
+ // build parameters list
+ var params = {};
+ // add list params
+ params['cmd'] = 'save-record';
+ params['recid'] = obj.recid;
+ // append other params
+ $.extend(params, obj.postData);
+ $.extend(params, postData);
+ params.record = $.extend(true, {}, obj.record);
+ // event before
+ var eventData = obj.trigger({ phase: 'before', type: 'submit', target: obj.name, url: obj.url, postData: params });
+ if (eventData.isCancelled === true) return;
+ // default action
+ var url = eventData.url;
+ if (typeof eventData.url == 'object' && eventData.url.save) url = eventData.url.save;
+ if (obj.last.xhr) try { obj.last.xhr.abort(); } catch (e) {};
+ // process url with routeData
+ if (!$.isEmptyObject(obj.routeData)) {
+ var info = w2utils.parseRoute(url);
+ if (info.keys.length > 0) {
+ for (var k = 0; k < info.keys.length; k++) {
+ if (obj.routeData[info.keys[k].name] == null) continue;
+ url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), obj.routeData[info.keys[k].name]);
+ }
+ }
+ }
+ var ajaxOptions = {
+ type : 'POST',
+ url : url,
+ data : eventData.postData,
+ dataType : 'text', // expected from server
+ xhr : function() {
+ var xhr = new window.XMLHttpRequest();
+ // upload
+ xhr.upload.addEventListener("progress", function(evt) {
+ if (evt.lengthComputable) {
+ var percent = Math.round(evt.loaded / evt.total * 100);
+ $('#'+ obj.name + '_progress').text(''+ percent + '%');
+ }
+ }, false);
+ return xhr;
+ }
+ };
+ if (w2utils.settings.dataType == 'HTTP') {
+ ajaxOptions.data = String($.param(ajaxOptions.data, false)).replace(/%5B/g, '[').replace(/%5D/g, ']');
+ }
+ if (w2utils.settings.dataType == 'RESTFULL') {
+ if (obj.recid != 0) ajaxOptions.type = 'PUT';
+ ajaxOptions.data = String($.param(ajaxOptions.data, false)).replace(/%5B/g, '[').replace(/%5D/g, ']');
+ }
+ if (w2utils.settings.dataType == 'JSON') {
+ ajaxOptions.type = 'POST';
+ ajaxOptions.data = JSON.stringify(ajaxOptions.data);
+ ajaxOptions.contentType = 'application/json';
+ }
+
+ obj.last.xhr = $.ajax(ajaxOptions)
+ .done(function (data, status, xhr) {
+ obj.unlock();
+ // event before
+ var eventData = obj.trigger({ phase: 'before', target: obj.name, type: 'save', xhr: xhr, status: status });
+ if (eventData.isCancelled === true) return;
+ // parse server response
+ var data;
+ var responseText = xhr.responseText;
+ if (status != 'error') {
+ // default action
+ if (typeof responseText != 'undefined' && responseText != '') {
+ // check if the onLoad handler has not already parsed the data
+ if (typeof responseText == "object") {
+ data = responseText;
+ } else {
+ // $.parseJSON or $.getJSON did not work because those expect perfect JSON data - where everything is in double quotes
+ //
+ // TODO: avoid (potentially malicious) code injection from the response.
+ try { eval('data = '+ responseText); } catch (e) { }
+ }
+ if (typeof data == 'undefined') {
+ data = {
+ status : 'error',
+ message : obj.msgNotJSON,
+ responseText : responseText
+ }
+ }
+ if (data['status'] == 'error') {
+ obj.error(data['message']);
+ } else {
+ obj.original = $.extend({}, obj.record);
+ }
+ }
+ } else {
+ obj.error('AJAX Error ' + xhr.status + ': '+ xhr.statusText);
+ data = {
+ status : 'error',
+ message : obj.msgAJAXerror,
+ responseText : responseText
+ };
+ }
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ obj.refresh();
+ // call back
+ if (data.status == 'success' && typeof callBack == 'function') callBack(data);
+ })
+ .fail(function (xhr, status, error) {
+ // trigger event
+ var errorObj = { status: status, error: error, rawResponseText: xhr.responseText };
+ var eventData2 = obj.trigger({ phase: 'before', type: 'error', error: errorObj, xhr: xhr });
+ if (eventData2.isCancelled === true) return;
+ // default behavior
+ console.log('ERROR: server communication failed. The server should return',
+ { status: 'success' }, 'OR', { status: 'error', message: 'error message' },
+ ', instead the AJAX request produced this: ', errorObj);
+ // event after
+ obj.trigger($.extend(eventData2, { phase: 'after' }));
+ });
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ }, 50);
+ },
+
+ lock: function (msg, showSpinner) {
+ var box = $(this.box).find('> div:first-child');
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift(box);
+ w2utils.lock.apply(window, args);
+ },
+
+ unlock: function () {
+ var obj = this;
+ setTimeout(function () { w2utils.unlock(obj.box); }, 25); // needed timer so if server fast, it will not flash
+ },
+
+ goto: function (page) {
+ if (typeof page != 'undefined') this.page = page;
+ // if it was auto size, resize it
+ if ($(this.box).data('auto-size') === true) $(this.box).height(0);
+ this.refresh();
+ },
+
+ generateHTML: function () {
+ var pages = []; // array for each page
+ var group = '';
+ var page;
+ for (var f in this.fields) {
+ var html = '';
+ var field = this.fields[f];
+ if (typeof field.html == 'undefined') field.html = {};
+ field.html = $.extend(true, { caption: '', span: 6, attr: '', text: '', page: 0 }, field.html);
+ if (typeof page == 'undefined') page = field.html.page;
+ if (field.html.caption == '') field.html.caption = field.name;
+ var input = '<input name="'+ field.name +'" type="text" '+ field.html.attr +'/>';
+ if ((field.type === 'pass') || (field.type === 'password')){
+ input = '<input name="' + field.name + '" type = "password" ' + field.html.attr + '/>';
+ }
+ if (field.type == 'checkbox') input = '<input name="'+ field.name +'" type="checkbox" '+ field.html.attr +'/>';
+ if (field.type == 'textarea') input = '<textarea name="'+ field.name +'" '+ field.html.attr +'></textarea>';
+ if (field.type == 'toggle') input = '<input name="'+ field.name +'" type="checkbox" '+ field.html.attr +' class="w2ui-toggle"/><div><div></div></div>';
+ if (field.html.group) {
+ if (group != '') html += '\n </div>';
+ html += '\n <div class="w2ui-group-title">'+ field.html.group + '</div>\n <div class="w2ui-group">';
+ group = field.html.group;
+ }
+ if (field.html.page != page && group != '') {
+ pages[pages.length-1] += '\n </div>';
+ group = '';
+ }
+ html += '\n <div class="w2ui-field '+ (typeof field.html.span != 'undefined' ? 'w2ui-span'+ field.html.span : '') +'">'+
+ '\n <label>' + w2utils.lang(field.html.caption) +'</label>'+
+ '\n <div>'+ input + w2utils.lang(field.html.text) + '</div>'+
+ '\n </div>';
+ if (typeof pages[field.html.page] == 'undefined') pages[field.html.page] = '';
+ pages[field.html.page] += html;
+ page = field.html.page;
+ }
+ if (group != '') pages[pages.length-1] += '\n </div>';
+ if (this.tabs.tabs) {
+ for (var i = 0; i < this.tabs.tabs.length; i++) if (typeof pages[i] == 'undefined') pages[i] = '';
+ }
+ for (var p in pages) pages[p] = '<div class="w2ui-page page-'+ p +'">' + pages[p] + '\n</div>';
+ // buttons if any
+ var buttons = '';
+ if (!$.isEmptyObject(this.actions)) {
+ var addClass = '';
+ buttons += '\n<div class="w2ui-buttons">';
+ for (var a in this.actions) {
+ if (['save', 'update', 'create'].indexOf(a.toLowerCase()) != -1) addClass = 'btn-green'; else addClass = '';
+ buttons += '\n <button name="'+ a +'" class="btn '+ addClass +'">'+ w2utils.lang(a) +'</button>';
+ }
+ buttons += '\n</div>';
+ }
+ return pages.join('') + buttons;
+ },
+
+ action: function (action, event) {
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: action, type: 'action', originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // default actions
+ if (typeof (this.actions[action]) == 'function') {
+ this.actions[action].call(this, event);
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ },
+
+ resize: function () {
+ var obj = this;
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'resize' });
+ if (eventData.isCancelled === true) return;
+ // default behaviour
+ var main = $(this.box).find('> div');
+ var header = $(this.box).find('> div .w2ui-form-header');
+ var toolbar = $(this.box).find('> div .w2ui-form-toolbar');
+ var tabs = $(this.box).find('> div .w2ui-form-tabs');
+ var page = $(this.box).find('> div .w2ui-page');
+ var cpage = $(this.box).find('> div .w2ui-page.page-'+ this.page);
+ var dpage = $(this.box).find('> div .w2ui-page.page-'+ this.page + ' > div');
+ var buttons = $(this.box).find('> div .w2ui-buttons');
+ // if no height, calculate it
+ resizeElements();
+ if (parseInt($(this.box).height()) == 0 || $(this.box).data('auto-size') === true) {
+ $(this.box).height(
+ (header.length > 0 ? w2utils.getSize(header, 'height') : 0) +
+ ((typeof this.tabs === 'object' && $.isArray(this.tabs.tabs) && this.tabs.tabs.length > 0) ? w2utils.getSize(tabs, 'height') : 0) +
+ ((typeof this.toolbar == 'object' && $.isArray(this.toolbar.items) && this.toolbar.items.length > 0) ? w2utils.getSize(toolbar, 'height') : 0) +
+ (page.length > 0 ? w2utils.getSize(dpage, 'height') + w2utils.getSize(cpage, '+height') + 12 : 0) + // why 12 ???
+ (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0)
+ );
+ $(this.box).data('auto-size', true);
+ }
+ resizeElements();
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+
+ function resizeElements() {
+ // resize elements
+ main.width($(obj.box).width()).height($(obj.box).height());
+ toolbar.css('top', (obj.header != '' ? w2utils.getSize(header, 'height') : 0));
+ tabs.css('top', (obj.header != '' ? w2utils.getSize(header, 'height') : 0)
+ + ((typeof obj.toolbar == 'object' && $.isArray(obj.toolbar.items) && obj.toolbar.items.length > 0) ? w2utils.getSize(toolbar, 'height') : 0));
+ page.css('top', (obj.header != '' ? w2utils.getSize(header, 'height') : 0)
+ + ((typeof obj.toolbar == 'object' && $.isArray(obj.toolbar.items) && obj.toolbar.items.length > 0) ? w2utils.getSize(toolbar, 'height') + 5 : 0)
+ + ((typeof obj.tabs === 'object' && $.isArray(obj.tabs.tabs) && obj.tabs.tabs.length > 0) ? w2utils.getSize(tabs, 'height') + 5 : 0));
+ page.css('bottom', (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0));
+ }
+ },
+
+ refresh: function () {
+ var time = (new Date()).getTime();
+ var obj = this;
+ if (!this.box) return;
+ if (!this.isGenerated || typeof $(this.box).html() == 'undefined') return;
+ // update what page field belongs
+ $(this.box).find('input, textarea, select').each(function (index, el) {
+ var name = (typeof $(el).attr('name') != 'undefined' ? $(el).attr('name') : $(el).attr('id'));
+ var field = obj.get(name);
+ if (field) {
+ // find page
+ var div = $(el).parents('.w2ui-page');
+ if (div.length > 0) {
+ for (var i = 0; i < 100; i++) {
+ if (div.hasClass('page-'+i)) { field.page = i; break; }
+ }
+ }
+ }
+ });
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'refresh', page: this.page })
+ if (eventData.isCancelled === true) return;
+ // default action
+ $(this.box).find('.w2ui-page').hide();
+ $(this.box).find('.w2ui-page.page-' + this.page).show();
+ $(this.box).find('.w2ui-form-header').html(this.header);
+ // refresh tabs if needed
+ if (typeof this.tabs === 'object' && $.isArray(this.tabs.tabs) && this.tabs.tabs.length > 0) {
+ $('#form_'+ this.name +'_tabs').show();
+ this.tabs.active = this.tabs.tabs[this.page].id;
+ this.tabs.refresh();
+ } else {
+ $('#form_'+ this.name +'_tabs').hide();
+ }
+ // refresh tabs if needed
+ if (typeof this.toolbar == 'object' && $.isArray(this.toolbar.items) && this.toolbar.items.length > 0) {
+ $('#form_'+ this.name +'_toolbar').show();
+ this.toolbar.refresh();
+ } else {
+ $('#form_'+ this.name +'_toolbar').hide();
+ }
+ // refresh values of all fields
+ for (var f in this.fields) {
+ var field = this.fields[f];
+ if (typeof field.name == 'undefined' && typeof field.field != 'undefined') field.name = field.field;
+ if (typeof field.field == 'undefined' && typeof field.name != 'undefined') field.field = field.name;
+ field.$el = $(this.box).find('[name="'+ String(field.name).replace(/\\/g, '\\\\') +'"]');
+ field.el = field.$el[0];
+ if (typeof field.el == 'undefined') {
+ console.log('ERROR: Cannot associate field "'+ field.name + '" with html control. Make sure html control exists with the same name.');
+ //return;
+ }
+ if (field.el) field.el.id = field.name;
+ var tmp = $(field).data('w2field');
+ if (tmp) tmp.clear();
+ $(field.$el).off('change').on('change', function () {
+ var value_new = this.value;
+ var value_previous = obj.record[this.name] ? obj.record[this.name] : '';
+ var field = obj.get(this.name);
+ if (['list', 'enum', 'file'].indexOf(field.type) != -1 && $(this).data('selected')) {
+ var nv = $(this).data('selected');
+ var cv = obj.record[this.name];
+ if ($.isArray(nv)) {
+ value_new = [];
+ for (var i in nv) value_new[i] = $.extend(true, {}, nv[i]); // clone array
+ }
+ if ($.isPlainObject(nv)) {
+ value_new = $.extend(true, {}, nv); // clone object
+ }
+ if ($.isArray(cv)) {
+ value_previous = [];
+ for (var i in cv) value_previous[i] = $.extend(true, {}, cv[i]); // clone array
+ }
+ if ($.isPlainObject(cv)) {
+ value_previous = $.extend(true, {}, cv); // clone object
+ }
+ }
+ if (field.type == 'toggle') value_new = ($(this).prop('checked') ? 1 : 0);
+ // clean extra chars
+ if (['int', 'float', 'percent', 'money', 'currency'].indexOf(field.type) != -1) {
+ value_new = $(this).data('w2field').clean(value_new);
+ }
+ if (value_new === value_previous) return;
+ // event before
+ var eventData = obj.trigger({ phase: 'before', target: this.name, type: 'change', value_new: value_new, value_previous: value_previous });
+ if (eventData.isCancelled === true) {
+ $(this).val(obj.record[this.name]); // return previous value
+ return;
+ }
+ // default action
+ var val = this.value;
+ if (this.type == 'select') val = this.value;
+ if (this.type == 'checkbox') val = this.checked ? true : false;
+ if (this.type == 'radio') {
+ field.$el.each(function (index, el) {
+ if (el.checked) val = el.value;
+ });
+ }
+ if (['int', 'float', 'percent', 'money', 'currency', 'list', 'combo', 'enum', 'file', 'toggle'].indexOf(field.type) != -1) {
+ val = value_new;
+ }
+ if (['enum', 'file'].indexOf(field.type) != -1) {
+ if (val.length > 0) {
+ var fld = $(field.el).data('w2field').helpers.multi;
+ $(fld).removeClass('w2ui-error');
+ }
+ }
+ obj.record[this.name] = val;
+ // event after
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ });
+ if (field.required) {
+ $(field.el).parent().parent().addClass('w2ui-required');
+ } else {
+ $(field.el).parent().parent().removeClass('w2ui-required');
+ }
+ }
+ // attach actions on buttons
+ $(this.box).find('button, input[type=button]').each(function (index, el) {
+ $(el).off('click').on('click', function (event) {
+ var action = this.value;
+ if (this.id) action = this.id;
+ if (this.name) action = this.name;
+ obj.action(action, event);
+ });
+ });
+ // init controls with record
+ for (var f in this.fields) {
+ var field = this.fields[f];
+ var value = (typeof this.record[field.name] != 'undefined' ? this.record[field.name] : '');
+ if (!field.el) continue;
+ field.type = String(field.type).toLowerCase();
+ if (!field.options) field.options = {};
+ switch (field.type) {
+ case 'text':
+ case 'textarea':
+ case 'email':
+ case 'pass':
+ case 'password':
+ field.el.value = value;
+ break;
+ case 'int':
+ case 'float':
+ case 'money':
+ case 'currency':
+ case 'percent':
+ case 'hex':
+ case 'alphanumeric':
+ case 'color':
+ case 'date':
+ case 'time':
+ field.el.value = value;
+ $(field.el).w2field($.extend({}, field.options, { type: field.type }));
+ break;
+ case 'toggle':
+ if (w2utils.isFloat(value)) value = parseFloat(value);
+ $(field.el).prop('checked', (value ? true : false));
+ this.record[field.name] = (value ? 1 : 0);
+ break;
+ // enums
+ case 'list':
+ case 'combo':
+ if (field.type == 'list' && !$.isPlainObject(value)) {
+ // find value from items
+ for (var i in field.options.items) {
+ var item = field.options.items[i];
+ if ($.isPlainObject(item) && item.id == value) {
+ value = $.extend(true, {}, item);
+ obj.record[field.name] = value;
+ break;
+ } else if (i == value) {
+ value = { id: i, text: item };
+ obj.record[field.name] = value;
+ break;
+ }
+ }
+ } else if (field.type == 'combo' && !$.isPlainObject(value)) {
+ field.el.value = value;
+ } else if ($.isPlainObject(value) && typeof value.text != 'undefined') {
+ field.el.value = value.text;
+ } else {
+ field.el.value = '';
+ }
+ if (!$.isPlainObject(value)) value = {};
+ $(field.el).w2field($.extend({}, field.options, { type: field.type, selected: value }));
+ break;
+ case 'enum':
+ case 'file':
+ if (!$.isArray(value)) value = [];
+ $(field.el).w2field($.extend({}, field.options, { type: field.type, selected: value }));
+ break;
+
+ // standard HTML
+ case 'select':
+ // generate options
+ var items = field.options.items;
+ if (typeof items != 'undefined' && items.length > 0) {
+ items = w2obj.field.prototype.normMenu(items);
+ $(field.el).html('');
+ for (var it in items) {
+ $(field.el).append('<option value="'+ items[it].id +'">' + items[it].text + '</option');
+ }
+ }
+ $(field.el).val(value);
+ break;
+ case 'radio':
+ $(field.$el).prop('checked', false).each(function (index, el) {
+ if ($(el).val() == value) $(el).prop('checked', true);
+ });
+ break;
+ case 'checkbox':
+ $(field.el).prop('checked', value ? true : false);
+ break;
+ default:
+ $(field.el).w2field($.extend({}, field.options, { type: field.type }));
+ break;
+ }
+ }
+ // wrap pages in div
+ var tmp = $(this.box).find('.w2ui-page');
+ for (var i = 0; i < tmp.length; i++) {
+ if ($(tmp[i]).find('> *').length > 1) $(tmp[i]).wrapInner('<div></div>');
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ this.resize();
+ return (new Date()).getTime() - time;
+ },
+
+ render: function (box) {
+ var time = (new Date()).getTime();
+ var obj = this;
+ if (typeof box == 'object') {
+ // remove from previous box
+ if ($(this.box).find('#form_'+ this.name +'_tabs').length > 0) {
+ $(this.box).removeAttr('name')
+ .removeClass('w2ui-reset w2ui-form')
+ .html('');
+ }
+ this.box = box;
+ }
+ if (!this.isGenerated) return;
+ if (!this.box) return;
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'render', box: (typeof box != 'undefined' ? box : this.box) });
+ if (eventData.isCancelled === true) return;
+ // default actions
+ if ($.isEmptyObject(this.original) && !$.isEmptyObject(this.record)) {
+ this.original = $.extend(true, {}, this.record);
+ }
+ var html = '<div>' +
+ (this.header != '' ? '<div class="w2ui-form-header">' + this.header + '</div>' : '') +
+ ' <div id="form_'+ this.name +'_toolbar" class="w2ui-form-toolbar"></div>' +
+ ' <div id="form_'+ this.name +'_tabs" class="w2ui-form-tabs"></div>' +
+ this.formHTML +
+ '</div>';
+ $(this.box).attr('name', this.name)
+ .addClass('w2ui-reset w2ui-form')
+ .html(html);
+ if ($(this.box).length > 0) $(this.box)[0].style.cssText += this.style;
+
+ // init toolbar regardless it is defined or not
+ if (typeof this.toolbar.render !== 'function') {
+ this.toolbar = $().w2toolbar($.extend({}, this.toolbar, { name: this.name +'_toolbar', owner: this }));
+ this.toolbar.on('click', function (event) {
+ var eventData = obj.trigger({ phase: 'before', type: 'toolbar', target: event.target, originalEvent: event });
+ if (eventData.isCancelled === true) return;
+ // no default action
+ obj.trigger($.extend(eventData, { phase: 'after' }));
+ });
+ }
+ if (typeof this.toolbar == 'object' && typeof this.toolbar.render == 'function') {
+ this.toolbar.render($('#form_'+ this.name +'_toolbar')[0]);
+ }
+ // init tabs regardless it is defined or not
+ if (typeof this.tabs.render !== 'function') {
+ this.tabs = $().w2tabs($.extend({}, this.tabs, { name: this.name +'_tabs', owner: this }));
+ this.tabs.on('click', function (event) {
+ obj.goto(this.get(event.target, true));
+ });
+ }
+ if (typeof this.tabs == 'object' && typeof this.tabs.render == 'function') {
+ this.tabs.render($('#form_'+ this.name +'_tabs')[0]);
+ }
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ // after render actions
+ this.resize();
+ var url = (typeof this.url != 'object' ? this.url : this.url.get);
+ if (url && this.recid != 0) {
+ this.request();
+ } else {
+ this.refresh();
+ }
+ // attach to resize event
+ if ($('.w2ui-layout').length == 0) { // if there is layout, it will send a resize event
+ this.tmp_resize = function (event) { w2ui[obj.name].resize(); }
+ $(window).off('resize', 'body').on('resize', 'body', this.tmp_resize);
+ }
+ setTimeout(function () { obj.resize(); obj.refresh(); }, 150); // need timer because resize is on timer
+ // focus on load
+ function focusEl() {
+ var inputs = $(obj.box).find('input, select, textarea');
+ if (inputs.length > obj.focus) inputs[obj.focus].focus();
+ }
+ if (this.focus >= 0) setTimeout(focusEl, 500); // need timeout to allow form to render
+ return (new Date()).getTime() - time;
+ },
+
+ destroy: function () {
+ // event before
+ var eventData = this.trigger({ phase: 'before', target: this.name, type: 'destroy' });
+ if (eventData.isCancelled === true) return;
+ // clean up
+ if (typeof this.toolbar == 'object' && this.toolbar.destroy) this.toolbar.destroy();
+ if (typeof this.tabs == 'object' && this.tabs.destroy) this.tabs.destroy();
+ if ($(this.box).find('#form_'+ this.name +'_tabs').length > 0) {
+ $(this.box)
+ .removeAttr('name')
+ .removeClass('w2ui-reset w2ui-form')
+ .html('');
+ }
+ delete w2ui[this.name];
+ // event after
+ this.trigger($.extend(eventData, { phase: 'after' }));
+ $(window).off('resize', 'body')
+ }
+ };
+
+ $.extend(w2form.prototype, w2utils.event);
+ w2obj.form = w2form;
+})();
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.css b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.css
new file mode 100644
index 00000000..c69eecd4
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.css
@@ -0,0 +1,2 @@
+/* w2ui 1.4 (c) http://w2ui.com, vitmalina@gmail.com */
+@font-face{font-family:w2ui-font;src:url("data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAWIAAoAAAAACAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAAA9AAAAEMAAABWQLxMsmNtYXAAAAE4AAAAOgAAAUriGRC2Z2x5ZgAAAXQAAAH9AAACgLu4vTRoZWFkAAADdAAAADAAAAA2AOYXBGhoZWEAAAOkAAAAIAAAACQD8wHHaG10eAAAA8QAAAAWAAAAIA7dAABsb2NhAAAD3AAAABIAAAASAngBuG1heHAAAAPwAAAAHwAAACABFQA2bmFtZQAABBAAAAEtAAACIsTQ/zJwb3N0AAAFQAAAAEgAAABi4/7ZEHicY2BkvMM4gYGVgYPRhTGNgYHBHUp/ZZBkaGFgYGJgZWbACgLSXFMYHD4yfmRnPPD/AIMe4wEGR6AwI0gOANHZC/IAeJxjYGBgZoBgGQZGBhBwAfIYwXwWBg0gzQakGRmYGBg+sv//D1LwkRFE8zNA1QMBIxvDiAcAddwGvgAAeJxFkLFv01AQxu97LrHdRImjpnaS1gnEia3IoqAktkkiEhaEOiCsDkEo8dyBDkytKpYKVWwssKKKAYmBREKMLJSFoRJ/AGJhY0NZGFgSzrUinvR+d++7T+/ePQLRci4IJ3SFCLIjG8CH+ZPwHHdwkkSS2PMXP3DGmUxpoo123lrt5nj8fjyejsc4W0zwNtl8FfHCKV5QnaOhF3IJUrUbkGPYnSGcGH6risBvGQhdVY0iVXXVkhJN1JL6/6xOIqWk4tRlJiVFiSJFSUrsZ+tkoqpEgt/6Hb/wjtZog2gonBx24AyFJUuNwMvha+/CvLi3vq1f785GsxGuTqfWc5O1N/r2+lNrOl38ZHnWpdUMr/CaLKLGZiHl4hI1+zasGB2/Dy9GSzfRbul4qaWPtHSQ0Y7SWpxmgnSc/mblMKNpmcOVEhfj+5d/8BGfqMl9bKuWFYWKaLf8YIAq9IKc5TY7ojNgTTcC7iEjaN4yPdswbM+81t8UimRLojq66YbdWq0bus375t21bwgcw/H6nmNUyhIkR/DsTasXPgx7VtV8oDx6XIxHE8vl8rMAzqlEDZ7QseUBPP6tLOQKDH5HqooKfLvB2gABa1lgflzI60VxsLd3IJj1YRn5/Wx9Syy++LvArn/JzH4e5WE98TCLer5wnBNb9WcrB5PoH084dg8AAAB4nGNgZGBgAOKMsPib8fw2Xxm4mRhA4PzjbBcY/f////1MjIwHgFwOBrA0AFcuDPF4nGNgZGBgPPD/AIMeEwMDw/9/TEwMQBEUwAEAe34EvHicY2JgYGCCYsbJCJpxO4QNABdTAesAAAAAAAAAEgAsAGgAjgC+AP4BQAAAeJxjYGRgYOBg0GJgZgABJiDmAkIGhv9gPgMADYEBTAB4nG2PTW7CMBCFXyBQFaQKtVKl7qwuuqkIPwsWHAD2LNiH4ARQEkeOQeICPUHP0DP0BF32DD1KX8IoixZbHn/z5o1/AAzwBQ/V8HBbx2q0cMPswm3SQNgnPwl30MezcJf6ULiHV8yE+3hAyBM8vzrtHk64hTu8Cbepvwv75A/hDh7xKdyl/i3cwxo/wn28eLN9ZPJhbHK30skxDW2TN7DWttybXE2CcaMtda5t6PRWbc6qPCVT52IVW5OpBas6TY0qrDnoyAU754r5aBSLHkQmwx4RDHL+Oq53hxU0EhyR8sf2Sv2/smaHRclKlStMEGB8xbekL6+9ITONLb0bnBlLnHjnlKqjW3FZ9mSkhfRqviclKxR17UAloh5gV3cVmGPEGf/xB/Ursl9uDmByAAAAeJxtwUEOgCAMBMAu0sI3SdMEIwKh8n8PXp2hQB+mf5kIAQciGIKEzFpNr6Sj7bs76xruMq3r2eJs22XZtPKIW1laiV6rCBDA") format("woff");font-weight:400;font-style:normal}[class^=w2ui-icon-]:before,[class*=" w2ui-icon-"]:before{font-family:w2ui-font;display:inline-block;vertical-align:middle;line-height:1;font-weight:400;font-style:normal;speak:none;text-decoration:inherit;text-transform:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w2ui-icon-check:before{content:"\f101"}.w2ui-icon-columns:before{content:"\f102"}.w2ui-icon-cross:before{content:"\f103"}.w2ui-icon-pencil:before{content:"\f104"}.w2ui-icon-plus:before{content:"\f105"}.w2ui-icon-reload:before{content:"\f106"}.w2ui-icon-search:before{content:"\f107"}.w2ui-reset{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;font-family:Verdana,Arial,sans-serif;font-size:11px}.w2ui-reset *{color:default;line-height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0}.w2ui-reset table{font-family:Verdana,Arial,sans-serif;font-size:11px;max-width:none;background-color:transparent;border-collapse:separate;border-spacing:0}.w2ui-reset input,.w2ui-reset textarea{width:auto;height:auto;vertical-align:baseline;padding:4px}.w2ui-reset select{padding:1px;height:23px}.w2ui-centered{position:absolute;left:0;right:0;top:50%;-webkit-transform:translateY(-50%);-moz-transform:translateY(-50%);-ms-transform:translateY(-50%);-o-transform:translateY(-50%);transform:translateY(-50%);max-height:100%;margin:0;padding:0 10px;text-align:center}.w2ui-disabled,.w2ui-readonly{background-color:#f1f1f1!important;color:#777!important}input:not([type=button]),select,textarea{padding:4px;border:1px solid #bbb;border-radius:3px;color:#000;background-color:#fff}input:not([type=button]):focus,select:focus,textarea:focus{outline-color:#72b2ff}input:not([type=button]):disabled,select:disabled,textarea:disabled,input:not([type=button])[readonly],select[readonly],textarea[readonly]{background-color:#f1f1f1;color:#777}input::-ms-clear{display:none}input:-ms-input-placeholder{color:#aaa!important}select{padding:2px}input[type=checkbox].w2ui-toggle{position:absolute;opacity:0;width:46px;height:22px;padding:0;margin:0;margin-left:2px}input[type=checkbox].w2ui-toggle+div{display:inline-block;width:46px;height:22px;border:1px solid #bbb;border-radius:30px;background-color:#eee;-webkit-transition-duration:.3s;-webkit-transition-property:background-color,box-shadow;-moz-transition-duration:.3s;-moz-transition-property:background-color,box-shadow;box-shadow:inset 0 0 0 0 rgba(0,0,0,.4);margin-left:2px}input[type=checkbox].w2ui-toggle:disabled+div{opacity:.3}input[type=checkbox].w2ui-toggle+div>div{float:left;width:22px;height:22px;border-radius:inherit;background:#f5f5f5;-webkit-transition-duration:.3s;-webkit-transition-property:transform,background-color,box-shadow;-moz-transition-duration:.3s;-moz-transition-property:transform,background-color;box-shadow:0 0 1px #323232,0 0 0 1px rgba(200,200,200,.6);pointer-events:none;margin-top:-1px;margin-left:-1px}input[type=checkbox].w2ui-toggle:checked+div{border:1px solid #00a23f;box-shadow:inset 0 0 0 12px #54B350}input[type=checkbox].w2ui-toggle:checked+div>div{-webkit-transform:translate3d(24px,0,0);-moz-transform:translate3d(24px,0,0);background-color:#fff;box-shadow:0 2px 5px rgba(0,0,0,.3),0 0 0 1px #00a23f}input[type=checkbox].w2ui-toggle.blue:checked+div{border:1px solid #206FAD;box-shadow:inset 0 0 0 12px #35A6EB}input[type=checkbox].w2ui-toggle.blue:checked+div>div{box-shadow:0 2px 5px rgba(0,0,0,.3),0 0 0 1px #206fad}input[type=checkbox].w2ui-toggle:focus{outline:0}.w2ui-overlay{position:absolute;margin-top:6px;margin-left:-17px;display:none;z-index:1300;color:inherit;background-color:#fbfbfb;border:3px solid #777;box-shadow:0 2px 10px #999;border-radius:4px;text-align:left}.w2ui-overlay table td{color:inherit}.w2ui-overlay:before{content:"";position:absolute;-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg);width:12px;height:12px;border:3px solid #777;border-color:inherit;background-color:inherit;border-left:1px solid transparent;border-bottom:1px solid transparent;border-bottom-left-radius:50px;margin:-9px 0 0 30px}.w2ui-overlay:after{display:none;content:"";position:absolute;-webkit-transform:rotate(135deg);-moz-transform:rotate(135deg);-ms-transform:rotate(135deg);-o-transform:rotate(135deg);transform:rotate(135deg);width:12px;height:12px;border:3px solid #777;border-color:inherit;background-color:inherit;border-left:1px solid transparent;border-bottom:1px solid transparent;border-bottom-left-radius:50px;margin:-7px 0 0 30px}.w2ui-overlay.w2ui-overlay-popup{z-index:1700}.w2ui-tag{position:absolute;z-index:1300;opacity:0;-webkit-transition:opacity .3s;-moz-transition:opacity .3s;-ms-transition:opacity .3s;-o-transition:opacity .3s;transition:opacity .3s}.w2ui-tag .w2ui-tag-body{background-color:rgba(60,60,60,.82);display:inline-block;position:absolute;border-radius:4px;padding:4px 10px;margin-left:10px;margin-top:0;color:#fff!important;box-shadow:1px 1px 3px #000;line-height:100%;font-size:11px;font-family:Verdana,Arial,sans-serif}.w2ui-tag .w2ui-tag-body:before{content:"";position:absolute;width:0;height:0;border-top:5px solid transparent;border-right:5px solid rgba(60,60,60,.82);border-bottom:5px solid transparent;margin:2px 0 0 -15px}.w2ui-tag.w2ui-tag-popup{z-index:1700}.w2ui-overlay table.w2ui-drop-menu{width:100%;color:#000;background-color:#fff;padding:5px 0;cursor:default}.w2ui-overlay table.w2ui-drop-menu td{white-space:nowrap}.w2ui-overlay table.w2ui-drop-menu .w2ui-item-even{color:inherit;background-color:#fff}.w2ui-overlay table.w2ui-drop-menu .w2ui-item-odd{color:inherit;background-color:#f3f6fa}.w2ui-overlay table.w2ui-drop-menu .w2ui-item-group{color:#444;font-weight:700;background-color:#ECEDF0;border-bottom:1px solid #D3D2D4}.w2ui-overlay table.w2ui-drop-menu td.menu-icon{padding:3px 0 4px 6px;width:20px}.w2ui-overlay table.w2ui-drop-menu td.menu-text{padding:8px 10px 8px 5px;width:auto}.w2ui-overlay table.w2ui-drop-menu td.menu-count{text-align:right}.w2ui-overlay table.w2ui-drop-menu td.menu-count>span{border:1px solid #9da4af;border-radius:20px;width:auto;height:18px;padding:2px 7px;margin:3px 5px 0;background-color:#e7f0fc;color:#667274;box-shadow:0 0 2px #fff;text-shadow:1px 1px 1px #e6e6e6}.w2ui-overlay table.w2ui-drop-menu tr:hover{color:inherit;background-color:#e6f0ff}.w2ui-overlay table.w2ui-drop-menu tr.w2ui-selected{background-color:#b6d5fb}.w2ui-overlay table.w2ui-drop-menu tr.w2ui-selected td{color:inherit}.w2ui-overlay table.w2ui-drop-menu tr.w2ui-disabled{opacity:.4;background-color:#fff!important}.w2ui-overlay table.w2ui-drop-menu .w2ui-icon{font-size:14px;color:#8d99a7;display:inline-block;padding-top:4px}.w2ui-marker{color:#444;background-color:rgba(252,244,161,.48)}.w2ui-spinner{display:inline-block;background-size:100%;background-repeat:no-repeat;background-image:url(data:image/gif;base64,R0lGODlhgACAAKIAAP///93d3bu7u5mZmQAA/wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFBQAEACwCAAIAfAB8AAAD/0i63P4wygYqmDjrzbtflvWNZGliYXiubKuloivPLlzReD7al+7/Eh5wSFQIi8hHYBkwHUmD6CD5YTJLz49USuVYraRsZ7vtar7XnQ1Kjpoz6LRHvGlz35O4nEPP2O94EnpNc2sef1OBGIOFMId/inB6jSmPdpGScR19EoiYmZobnBCIiZ95k6KGGp6ni4wvqxilrqBfqo6skLW2YBmjDa28r6Eosp27w8Rov8ekycqoqUHODrTRvXsQwArC2NLF29UM19/LtxO5yJd4Au4CK7DUNxPebG4e7+8n8iv2WmQ66BtoYpo/dvfacBjIkITBE9DGlMvAsOIIZjIUAixliv9ixYZVtLUos5GjwI8gzc3iCGghypQqrbFsme8lwZgLZtIcYfNmTJ34WPTUZw5oRxdD9w0z6iOpO15MgTh1BTTJUKos39jE+o/KS64IFVmsFfYT0aU7capdy7at27dw48qdS7eu3bt480I02vUbX2F/JxYNDImw4GiGE/P9qbhxVpWOI/eFKtlNZbWXuzlmG1mv58+gQ4seTbq06dOoU6vGQZJy0FNlMcV+czhQ7SQmYd8eMhPs5BxVdfcGEtV3buDBXQ+fURxx8oM6MT9P+Fh6dOrH2zavc13u9JXVJb520Vp8dvC76wXMuN5Sepm/1WtkEZHDefnzR9Qvsd9+/wi8+en3X0ntYVcSdAE+UN4zs7ln24CaLagghIxBaGF8kFGoIYV+Ybghh841GIyI5ICIFoklJsigihmimJOLEbLYIYwxSgigiZ+8l2KB+Ml4oo/w8dijjcrouCORKwIpnJIjMnkkksalNeR4fuBIm5UEYImhIlsGCeWNNJphpJdSTlkml1jWeOY6TnaRpppUctcmFW9mGSaZceYopH9zkjnjUe59iR5pdapWaGqHopboaYua1qije67GJ6CuJAAAIfkEBQUABAAsCgACAFcAMAAAA/9Iutz+ML5Ag7w46z0r5WAoSp43nihXVmnrdusrv+s332dt4Tyo9yOBUJD6oQBIQGs4RBlHySSKyczVTtHoidocPUNZaZAr9F5FYbGI3PWdQWn1mi36buLKFJvojsHjLnshdhl4L4IqbxqGh4gahBJ4eY1kiX6LgDN7fBmQEJI4jhieD4yhdJ2KkZk8oiSqEaatqBekDLKztBG2CqBACq4wJRi4PZu1sA2+v8C6EJexrBAD1AOBzsLE0g/V1UvYR9sN3eR6lTLi4+TlY1wz6Qzr8u1t6FkY8vNzZTxaGfn6mAkEGFDgL4LrDDJDyE4hEIbdHB6ESE1iD4oVLfLAqPETIsOODwmCDJlv5MSGJklaS6khAQAh+QQFBQAEACwfAAIAVwAwAAAD/0i63P5LSAGrvTjrNuf+YKh1nWieIumhbFupkivPBEzR+GnnfLj3ooFwwPqdAshAazhEGUXJJIrJ1MGOUamJ2jQ9QVltkCv0XqFh5IncBX01afGYnDqD40u2z76JK/N0bnxweC5sRB9vF34zh4gjg4uMjXobihWTlJUZlw9+fzSHlpGYhTminKSepqebF50NmTyor6qxrLO0L7YLn0ALuhCwCrJAjrUqkrjGrsIkGMW/BMEPJcphLgDaABjUKNEh29vdgTLLIOLpF80s5xrp8ORVONgi8PcZ8zlRJvf40tL8/QPYQ+BAgjgMxkPIQ6E6hgkdjoNIQ+JEijMsasNY0RQix4gKP+YIKXKkwJIFF6JMudFEAgAh+QQFBQAEACw8AAIAQgBCAAAD/kg0PPowykmrna3dzXvNmSeOFqiRaGoyaTuujitv8Gx/661HtSv8gt2jlwIChYtc0XjcEUnMpu4pikpv1I71astytkGh9wJGJk3QrXlcKa+VWjeSPZHP4Rtw+I2OW81DeBZ2fCB+UYCBfWRqiQp0CnqOj4J1jZOQkpOUIYx/m4oxg5cuAaYBO4Qop6c6pKusrDevIrG2rkwptrupXB67vKAbwMHCFcTFxhLIt8oUzLHOE9Cy0hHUrdbX2KjaENzey9Dh08jkz8Tnx83q66bt8PHy8/T19vf4+fr6AP3+/wADAjQmsKDBf6AOKjS4aaHDgZMeSgTQcKLDhBYPEswoA1BBAgAh+QQFBQAEACxOAAoAMABXAAAD7Ei6vPOjyUkrhdDqfXHm4OZ9YSmNpKmiqVqykbuysgvX5o2HcLxzup8oKLQQix0UcqhcVo5ORi+aHFEn02sDeuWqBGCBkbYLh5/NmnldxajX7LbPBK+PH7K6narfO/t+SIBwfINmUYaHf4lghYyOhlqJWgqDlAuAlwyBmpVnnaChoqOkpaanqKmqKgGtrq+wsbA1srW2ry63urasu764Jr/CAb3Du7nGt7TJsqvOz9DR0tPU1TIA2ACl2dyi3N/aneDf4uPklObj6OngWuzt7u/d8fLY9PXr9eFX+vv8+PnYlUsXiqC3c6PmUUgAACH5BAUFAAQALE4AHwAwAFcAAAPpSLrc/m7IAau9bU7MO9GgJ0ZgOI5leoqpumKt+1axPJO1dtO5vuM9yi8TlAyBvSMxqES2mo8cFFKb8kzWqzDL7Xq/4LB4TC6bz1yBes1uu9uzt3zOXtHv8xN+Dx/x/wJ6gHt2g3Rxhm9oi4yNjo+QkZKTCgGWAWaXmmOanZhgnp2goaJdpKGmp55cqqusrZuvsJays6mzn1m4uRAAvgAvuBW/v8GwvcTFxqfIycA3zA/OytCl0tPPO7HD2GLYvt7dYd/ZX99j5+Pi6tPh6+bvXuTuzujxXens9fr7YPn+7egRI9PPHrgpCQAAIfkEBQUABAAsPAA8AEIAQgAAA/lIutz+UI1Jq7026h2x/xUncmD5jehjrlnqSmz8vrE8u7V5z/m5/8CgcEgsGo/IpHLJbDqf0Kh0ShBYBdTXdZsdbb/Yrgb8FUfIYLMDTVYz2G13FV6Wz+lX+x0fdvPzdn9WeoJGAYcBN39EiIiKeEONjTt0kZKHQGyWl4mZdREAoQAcnJhBXBqioqSlT6qqG6WmTK+rsa1NtaGsuEu6o7yXubojsrTEIsa+yMm9SL8osp3PzM2cStDRykfZ2tfUtS/bRd3ewtzV5pLo4eLjQuUp70Hx8t9E9eqO5Oku5/ztdkxi90qPg3x2EMpR6IahGocPCxp8AGtigwQAIfkEBQUABAAsHwBOAFcAMAAAA/9Iutz+MMo36pg4682J/V0ojs1nXmSqSqe5vrDXunEdzq2ta3i+/5DeCUh0CGnF5BGULC4tTeUTFQVONYAs4CfoCkZPjFar83rBx8l4XDObSUL1Ott2d1U4yZwcs5/xSBB7dBMBhgEYfncrTBGDW4WHhomKUY+QEZKSE4qLRY8YmoeUfkmXoaKInJ2fgxmpqqulQKCvqRqsP7WooriVO7u8mhu5NacasMTFMMHCm8qzzM2RvdDRK9PUwxzLKdnaz9y/Kt8SyR3dIuXmtyHpHMcd5+jvWK4i8/TXHff47SLjQvQLkU+fG29rUhQ06IkEG4X/Rryp4mwUxSgLL/7IqFETB8eONT6ChCFy5ItqJomES6kgAQAh+QQFBQAEACwKAE4AVwAwAAAD/0i63A4QuEmrvTi3yLX/4MeNUmieITmibEuppCu3sDrfYG3jPKbHveDktxIaF8TOcZmMLI9NyBPanFKJp4A2IBx4B5lkdqvtfb8+HYpMxp3Pl1qLvXW/vWkli16/3dFxTi58ZRcChwIYf3hWBIRchoiHiotWj5AVkpIXi4xLjxiaiJR/T5ehoomcnZ+EGamqq6VGoK+pGqxCtaiiuJVBu7yaHrk4pxqwxMUzwcKbyrPMzZG90NGDrh/JH8t72dq3IN1jfCHb3L/e5ebh4ukmxyDn6O8g08jt7tf26ybz+m/W9GNXzUQ9fm1Q/APoSWAhhfkMAmpEbRhFKwsvCsmosRIHx444PoKcIXKkjIImjTzjkQAAIfkEBQUABAAsAgA8AEIAQgAAA/VIBNz+8KlJq72Yxs1d/uDVjVxogmQqnaylvkArT7A63/V47/m2/8CgcEgsGo/IpHLJbDqf0Kh0Sj0FroGqDMvVmrjgrDcTBo8v5fCZki6vCW33Oq4+0832O/at3+f7fICBdzsChgJGeoWHhkV0P4yMRG1BkYeOeECWl5hXQ5uNIAOjA1KgiKKko1CnqBmqqk+nIbCkTq20taVNs7m1vKAnurtLvb6wTMbHsUq4wrrFwSzDzcrLtknW16tI2tvERt6pv0fi48jh5h/U6Zs77EXSN/BE8jP09ZFA+PmhP/xvJgAMSGBgQINvEK5ReIZhQ3QEMTBLAAAh+QQFBQAEACwCAB8AMABXAAAD50i6DA4syklre87qTbHn4OaNYSmNqKmiqVqyrcvBsazRpH3jmC7yD98OCBF2iEXjBKmsAJsWHDQKmw571l8my+16v+CweEwum8+hgHrNbrvbtrd8znbR73MVfg838f8BeoB7doN0cYZvaIuMjY6PkJGSk2gClgJml5pjmp2YYJ6dX6GeXaShWaeoVqqlU62ir7CXqbOWrLafsrNctjIDwAMWvC7BwRWtNsbGFKc+y8fNsTrQ0dK3QtXAYtrCYd3eYN3c49/a5NVj5eLn5u3s6e7x8NDo9fbL+Mzy9/T5+tvUzdN3Zp+GBAAh+QQJBQAEACwCAAIAfAB8AAAD/0i63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdArcQK2TOL7/nl4PSMwIfcUk5YhUOh3M5nNKiOaoWCuWqt1Ou16l9RpOgsvEMdocXbOZ7nQ7DjzTaeq7zq6P5fszfIASAYUBIYKDDoaGIImKC4ySH3OQEJKYHZWWi5iZG0ecEZ6eHEOio6SfqCaqpaytrpOwJLKztCO2jLi1uoW8Ir6/wCHCxMG2x7muysukzb230M6H09bX2Nna29zd3t/g4cAC5OXm5+jn3Ons7eba7vHt2fL16tj2+QL0+vXw/e7WAUwnrqDBgwgTKlzIsKHDh2gGSBwAccHEixAvaqTYcFCjRoYeNyoM6REhyZIHT4o0qPIjy5YTTcKUmHImx5cwE85cmJPnSYckK66sSAAj0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gwxZJAAA7)}.w2ui-icon{background-repeat:no-repeat;height:16px;width:16px;overflow:hidden;margin:2px;display:inline-block}.w2ui-icon.icon-search,.w2ui-icon.icon-search-down{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAgCAYAAAB+ZAqzAAACuElEQVRYw9WXSWhTQRjHR0UKLqhFaV0OUih68GAOWjyJKypCpAoV8aIiioIICiKiB1GMtE3MYmry2moXDz1UDx7sUXHBhQpSaRVxrYpWcMO9avx/8AJh/CbznHkxdeB3Cd/8589kvuUJkWcdjCTHghUgAi6DJ+AVeAqugSQIggniXywcNBJsB70g44EHYBcYXUhTM8EFj4ZkboKqQpiqAv2GprK8o7/f75t6pjn0M3gNPmri3vtycxAZA64qDvkJ2kENqAQTQQWoBg74qth3B4y3NbZDIX4fzNfsnQtuK/YfsjFVCh4pMq3Co0Y5uMVoUGkpy8aFT5xaeSzVEo45bXdBt4LeaLq1k0RXMYJfdDfFmAuAD4zWlty4UNyZEkm19MUb2zMw8Sfp1u+IWSrcIimLnTG8/SijdU6OO5poDESdtgHZVBzUHm/amhW7zoitMTS2mNHqASPk2FDCCcLMYK6p+obmulyxfiYLA4bGKFvfSnrUvkq5+Lpk8z4yRH8r3l/X4WiqJFfspSQ0CGYZGpsMnkt6L+h31Z76hpMdeOwPQ7H0NFnssST0C8wxNDaDKb6kP06150gsHahNNlVzYheZd7HJ0BiX4VRGhpmIhRixKyZilM2M1mnTArtIUbU3/qVO0H0GvmQ4CY4C3YopYYlHjXlggNG4R33Ypi2tVtwaPeTdNMkq9pVQZQdvFPs32zbx4aAjzxhDRfIAWAeWg7VgrzsY5ht/zoNJtubKwA3LITGjSKRyW3NTwaUCmKOSMd3WHH0ZJRQZZkOP1zFKZ3CB++4+aQ6kEeksWAb2a2L7qDv49S1Q6T72MOgEXa6RGFhP3wpS/B6NOWpRs0UxFg7eqTFHjX1hscxtAz/ymEuIYi0cvgF8Y0w5Ro3dZ3M1boJkTaXEUFlug6fsdsRQWzTj0cey+N/Xb2sj5lTh2M6OAAAAAElFTkSuQmCC) no-repeat center!important;background-size:14px 12px!important;opacity:.9}.w2ui-icon.icon-folder{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC) no-repeat center!important}.w2ui-icon.icon-page{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAINSURBVBgZBcG/r55zGAfg6/4+z3va01NHlYgzEfE7MdCIGISFgS4Gk8ViYyM2Mdlsko4GSf8Do0FLRCIkghhYJA3aVBtEz3nP89wf11VJvPDepdd390+8Nso5nESBQoq0pfvXm9fzWf19453LF85vASqJlz748vInb517dIw6EyYBIIG49u+xi9/c9MdvR//99MPPZ7+4cP4IZhhTPbwzT2d+vGoaVRRp1rRliVvHq+cfvM3TD82+7mun0o/ceO7NT+/4/KOXjwZU1ekk0840bAZzMQ2mooqh0A72d5x/6sB9D5zYnff3PoYBoWBgFKPKqDKqjCpjKr//dcu9p489dra88cydps30KswACfNEKanSaxhlntjJ8Mv12Paie+vZ+0+oeSwwQ0Iw1xAR1CiFNJkGO4wu3ZMY1AAzBI0qSgmCNJsJUEOtJSMaCTBDLyQ0CknAGOgyTyFFiLI2awMzdEcSQgSAAKVUmAeNkxvWJWCGtVlDmgYQ0GFtgg4pNtOwbBcwQy/Rife/2yrRRVI0qYCEBly8Z+P4qMEMy7JaVw72N568e+iwhrXoECQkfH91kY7jwwXMsBx1L93ZruqrK6uuiAIdSnTIKKPLPFcvay8ww/Hh+ufeznTXu49v95IMoQG3784gYXdTqvRmqn/Wpa/ADFX58MW3L71SVU9ETgEIQQQIOOzub+fhIvwPRDgeVjWDahIAAAAASUVORK5CYII=) no-repeat center!important}.w2ui-lock{display:none;position:absolute;z-index:1400;top:0;left:0;width:100%;height:100%;opacity:.15;filter:alpha(opacity=15);background-color:#333}.w2ui-lock-msg{display:none;position:absolute;z-index:1400;top:45%;left:50%;-webkit-transform:translateX(-50%) translateY(-50%);-moz-transform:translateX(-50%) translateY(-50%);-ms-transform:translateX(-50%) translateY(-50%);-o-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);width:200px;height:80px;padding:30px 8px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;font-size:13px;font-family:Verdana,Arial,sans-serif;opacity:.8;filter:alpha(opacity=80);background-color:#555;color:#fff;text-align:center;border-radius:5px;border:2px solid #444}.w2ui-lock-msg .w2ui-spinner{display:inline-block;width:24px;height:24px;margin:-3px 8px -7px -10px}button.btn{display:inline-block;border-radius:4px;margin:0 5px;padding:7px 12px 6px!important;color:#666;font-size:12px!important;border:1px solid #B6B6B6;background-image:-webkit-linear-gradient(#fff 0,#e7e7e7 100%);background-image:-moz-linear-gradient(#fff 0,#e7e7e7 100%);background-image:-ms-linear-gradient(#fff 0,#e7e7e7 100%);background-image:-o-linear-gradient(#fff 0,#e7e7e7 100%);background-image:linear-gradient(#fff 0,#e7e7e7 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffe7e7e7', endColorstr='#ffffffff', GradientType=0);outline:0;box-shadow:0 1px 0 #fff;cursor:default;min-width:75px;line-height:100%!important;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}button.btn:hover{text-decoration:none;border:1px solid #bbb;background-image:-webkit-linear-gradient(#f7f7f7 0,#ddd 100%);background-image:-moz-linear-gradient(#f7f7f7 0,#ddd 100%);background-image:-ms-linear-gradient(#f7f7f7 0,#ddd 100%);background-image:-o-linear-gradient(#f7f7f7 0,#ddd 100%);background-image:linear-gradient(#f7f7f7 0,#ddd 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffdddddd', endColorstr='#fff7f7f7', GradientType=0);color:#333}button.btn:active,button.btn.clicked{border:1px solid #999;background-image:-webkit-linear-gradient(#ccc 0,#ccc 100%);background-image:-moz-linear-gradient(#ccc 0,#ccc 100%);background-image:-ms-linear-gradient(#ccc 0,#ccc 100%);background-image:-o-linear-gradient(#ccc 0,#ccc 100%);background-image:linear-gradient(#ccc 0,#ccc 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffcccccc', endColorstr='#ffcccccc', GradientType=0);text-shadow:1px 1px 1px #eee}button.btn:disabled{border:1px solid #bbb!important;background:#f7f7f7!important;color:#bdbcbc!important;text-shadow:none!important}button.btn-blue{color:#fff;background-image:-webkit-linear-gradient(#80c0f7 0,#269df0 100%);background-image:-moz-linear-gradient(#80c0f7 0,#269df0 100%);background-image:-ms-linear-gradient(#80c0f7 0,#269df0 100%);background-image:-o-linear-gradient(#80c0f7 0,#269df0 100%);background-image:linear-gradient(#80c0f7 0,#269df0 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff269df0', endColorstr='#ff80c0f7', GradientType=0);border:1px solid #538AB7;text-shadow:1px 1px 1px #777}button.btn-blue:hover{color:#fff;background-image:-webkit-linear-gradient(#73b6f0 0,#2391dd 100%);background-image:-moz-linear-gradient(#73b6f0 0,#2391dd 100%);background-image:-ms-linear-gradient(#73b6f0 0,#2391dd 100%);background-image:-o-linear-gradient(#73b6f0 0,#2391dd 100%);background-image:linear-gradient(#73b6f0 0,#2391dd 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff2391dd', endColorstr='#ff73b6f0', GradientType=0);border:1px solid #497BA3;text-shadow:1px 1px 1px #777}button.btn-blue:active,button.btn-blue.clicked{color:#fff;background-image:-webkit-linear-gradient(#1e83c9 0,#1e83c9 100%);background-image:-moz-linear-gradient(#1e83c9 0,#1e83c9 100%);background-image:-ms-linear-gradient(#1e83c9 0,#1e83c9 100%);background-image:-o-linear-gradient(#1e83c9 0,#1e83c9 100%);background-image:linear-gradient(#1e83c9 0,#1e83c9 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff1e83c9', endColorstr='#ff1e83c9', GradientType=0);border:1px solid #1268A6;text-shadow:1px 1px 1px #777}button.btn-green{color:#fff;background-image:-webkit-linear-gradient(#81cf81 0,#52a452 100%);background-image:-moz-linear-gradient(#81cf81 0,#52a452 100%);background-image:-ms-linear-gradient(#81cf81 0,#52a452 100%);background-image:-o-linear-gradient(#81cf81 0,#52a452 100%);background-image:linear-gradient(#81cf81 0,#52a452 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff52a452', endColorstr='#ff81cf81', GradientType=0);border:1px solid #479247;text-shadow:1px 1px 1px #777}button.btn-green:hover{color:#fff;background-image:-webkit-linear-gradient(#6abe68 0,#3f8f3d 100%);background-image:-moz-linear-gradient(#6abe68 0,#3f8f3d 100%);background-image:-ms-linear-gradient(#6abe68 0,#3f8f3d 100%);background-image:-o-linear-gradient(#6abe68 0,#3f8f3d 100%);background-image:linear-gradient(#6abe68 0,#3f8f3d 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff3f8f3d', endColorstr='#ff6abe68', GradientType=0);border:1px solid #479247;text-shadow:1px 1px 1px #777}button.btn-green:active,button.btn-green.clicked{color:#fff;background-image:-webkit-linear-gradient(#377d36 0,#377d36 100%);background-image:-moz-linear-gradient(#377d36 0,#377d36 100%);background-image:-ms-linear-gradient(#377d36 0,#377d36 100%);background-image:-o-linear-gradient(#377d36 0,#377d36 100%);background-image:linear-gradient(#377d36 0,#377d36 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff377d36', endColorstr='#ff377d36', GradientType=0);border:1px solid #555!important;text-shadow:1px 1px 1px #777}button.btn-orange{color:#fff;background-image:-webkit-linear-gradient(#fcc272 0,#fb8822 100%);background-image:-moz-linear-gradient(#fcc272 0,#fb8822 100%);background-image:-ms-linear-gradient(#fcc272 0,#fb8822 100%);background-image:-o-linear-gradient(#fcc272 0,#fb8822 100%);background-image:linear-gradient(#fcc272 0,#fb8822 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fffb8822', endColorstr='#fffcc272', GradientType=0);border:1px solid #B68B4C;text-shadow:1px 1px 1px #777}button.btn-orange:hover{color:#fff;background-image:-webkit-linear-gradient(#f4ad59 0,#f1731f 100%);background-image:-moz-linear-gradient(#f4ad59 0,#f1731f 100%);background-image:-ms-linear-gradient(#f4ad59 0,#f1731f 100%);background-image:-o-linear-gradient(#f4ad59 0,#f1731f 100%);background-image:linear-gradient(#f4ad59 0,#f1731f 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fff1731f', endColorstr='#fff4ad59', GradientType=0);border:1px solid #B68B4C;text-shadow:1px 1px 1px #777}button.btn-orange:active,button.btn-orange.clicked{color:#fff;border:1px solid #666;background-image:-webkit-linear-gradient(#b98747 0,#b98747 100%);background-image:-moz-linear-gradient(#b98747 0,#b98747 100%);background-image:-ms-linear-gradient(#b98747 0,#b98747 100%);background-image:-o-linear-gradient(#b98747 0,#b98747 100%);background-image:linear-gradient(#b98747 0,#b98747 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffb98747', endColorstr='#ffb98747', GradientType=0);text-shadow:1px 1px 1px #777}button.btn-red{color:#fff;background-image:-webkit-linear-gradient(#ff6e70 0,#c72d2d 100%);background-image:-moz-linear-gradient(#ff6e70 0,#c72d2d 100%);background-image:-ms-linear-gradient(#ff6e70 0,#c72d2d 100%);background-image:-o-linear-gradient(#ff6e70 0,#c72d2d 100%);background-image:linear-gradient(#ff6e70 0,#c72d2d 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffc72d2d', endColorstr='#ffff6e70', GradientType=0);border:1px solid #BB3C3E;text-shadow:1px 1px 1px #777}button.btn-red:hover{color:#fff;background-image:-webkit-linear-gradient(#ee696c 0,#ae2527 100%);background-image:-moz-linear-gradient(#ee696c 0,#ae2527 100%);background-image:-ms-linear-gradient(#ee696c 0,#ae2527 100%);background-image:-o-linear-gradient(#ee696c 0,#ae2527 100%);background-image:linear-gradient(#ee696c 0,#ae2527 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffae2527', endColorstr='#ffee696c', GradientType=0);border:1px solid #BB3C3E;text-shadow:1px 1px 1px #777}button.btn-red:active,button.btn-red.clicked{color:#fff;border:1px solid #861C1E;background-image:-webkit-linear-gradient(#9c2123 0,#9c2123 100%);background-image:-moz-linear-gradient(#9c2123 0,#9c2123 100%);background-image:-ms-linear-gradient(#9c2123 0,#9c2123 100%);background-image:-o-linear-gradient(#9c2123 0,#9c2123 100%);background-image:linear-gradient(#9c2123 0,#9c2123 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff9c2123', endColorstr='#ff9c2123', GradientType=0);text-shadow:1px 1px 1px #777}.w2ui-form{position:relative;color:#000;background-color:#f5f6f7;border:1px solid silver;border-radius:3px;padding:0;overflow:hidden!important}.w2ui-form>div{position:absolute;overflow:hidden}.w2ui-form .w2ui-form-header{position:absolute;left:0;right:0;border-bottom:1px solid #99bbe8!important;overflow:hidden;color:#444;font-size:13px;text-align:center;padding:8px;background-image:-webkit-linear-gradient(#dae6f3,#c2d5ed);background-image:-moz-linear-gradient(#dae6f3,#c2d5ed);background-image:-ms-linear-gradient(#dae6f3,#c2d5ed);background-image:-o-linear-gradient(#dae6f3,#c2d5ed);background-image:linear-gradient(#dae6f3,#c2d5ed);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffdae6f3', endColorstr='#ffc2d5ed', GradientType=0);border-top-left-radius:3px;border-top-right-radius:3px}.w2ui-form .w2ui-form-toolbar{position:absolute;left:0;right:0;margin:0;padding:6px 3px;border-bottom:1px solid #d5d8d8}.w2ui-form .w2ui-form-tabs{margin:0;padding:0}.w2ui-form .w2ui-tabs{position:absolute;left:0;right:0;border-top-left-radius:3px;border-top-right-radius:3px;padding-top:5px!important;background-color:#fafafa}.w2ui-form .w2ui-tabs .w2ui-tab.active{background-color:#f5f6f7}.w2ui-form .w2ui-page{position:absolute;left:0;right:0;overflow:auto;padding:10px;border-left:1px solid inherit;border-right:1px solid inherit;background-color:inherit;border-radius:3px}.w2ui-form .w2ui-buttons{position:absolute;left:0;right:0;bottom:0;text-align:center;border-top:1px solid #d5d8d8;border-bottom:0 solid #d5d8d8;background-color:#fafafa;padding:15px 0!important;border-bottom-left-radius:3px;border-bottom-right-radius:3px}.w2ui-form .w2ui-buttons input[type=button],.w2ui-form .w2ui-buttons button{min-width:80px;margin-right:5px}.w2ui-form input[type=checkbox],.w2ui-form input[type=radio]{margin-top:4px;margin-bottom:4px}.w2ui-form input[type=checkbox].w2ui-toggle{margin:0}.w2ui-group-title{padding:5px 2px;color:#8D96A2;text-shadow:1px 1px 2px #fdfdfd;font-size:120%}.w2ui-group{background-color:#ebecef;margin:5px 0 10px;padding:10px 5px;border-top:1px solid #cedcea;border-bottom:1px solid #cedcea}.w2ui-field>label{display:block;float:left;margin-top:7px;margin-bottom:3px;width:120px;padding:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right;min-height:20px;color:#666}.w2ui-field>div{margin-bottom:3px;margin-left:128px;padding:3px;min-height:28px;float:none}.w2ui-field.w2ui-required>div{position:relative}.w2ui-field.w2ui-required>div::before{content:'*';position:absolute;margin-top:5px;margin-left:-9px;color:red}.w2ui-field.w2ui-span1>label{width:20px}.w2ui-field.w2ui-span1>div{margin-left:28px}.w2ui-field.w2ui-span2>label{width:40px}.w2ui-field.w2ui-span2>div{margin-left:48px}.w2ui-field.w2ui-span3>label{width:60px}.w2ui-field.w2ui-span3>div{margin-left:68px}.w2ui-field.w2ui-span4>label{width:80px}.w2ui-field.w2ui-span4>div{margin-left:88px}.w2ui-field.w2ui-span5>label{width:100px}.w2ui-field.w2ui-span5>div{margin-left:108px}.w2ui-field.w2ui-span6>label{width:120px}.w2ui-field.w2ui-span6>div{margin-left:128px}.w2ui-field.w2ui-span7>label{width:140px}.w2ui-field.w2ui-span7>div{margin-left:148px}.w2ui-field.w2ui-span8>label{width:160px}.w2ui-field.w2ui-span8>div{margin-left:168px}.w2ui-field.w2ui-span9>label{width:180px}.w2ui-field.w2ui-span9>div{margin-left:188px}.w2ui-field.w2ui-span10>label{width:200px}.w2ui-field.w2ui-span10>div{margin-left:208px}.w2ui-error{border:1px solid #ffa8a8!important;background-color:#fff4eb!important}.w2field{padding:3px;border-radius:3px;border:1px solid silver}.w2ui-field-helper{position:absolute;display:inline-block;line-height:100%;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none}.w2ui-field-helper .w2ui-field-up{position:absolute;top:0;padding:2px 3px}.w2ui-field-helper .w2ui-field-down{position:absolute;bottom:0;padding:2px 3px}.w2ui-field-helper .arrow-up:hover{border-bottom-color:#81C6FF}.w2ui-field-helper .arrow-down:hover{border-top-color:#81C6FF}.arrow-up{background:0 0;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:5px solid #777;font-size:0;line-height:0}.arrow-down{background:0 0;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid #777;font-size:0;line-height:0}.arrow-left{background:0 0;width:0;height:0;border-bottom:4px solid transparent;border-top:4px solid transparent;border-right:5px solid #777;font-size:0;line-height:0}.arrow-right{background:0 0;width:0;height:0;border-bottom:4px solid transparent;border-top:4px solid transparent;border-left:5px solid #777;font-size:0;line-height:0}.w2ui-color{padding:5px;padding-top:8px;background-color:#fff;border-radius:3px}.w2ui-color>table{table-layout:fixed;width:160px}.w2ui-color>table td{width:20px;height:20px;text-align:center}.w2ui-color>table td div{cursor:pointer;display:inline-block;width:16px;height:17px;padding:1px 4px;border:1px solid transparent;color:#fff;text-shadow:0 0 2px #000}.w2ui-color>table td div:hover{outline:1px solid #666;border:1px solid #fff}.w2ui-calendar{margin:0;padding:1px;line-height:108%}.w2ui-calendar .w2ui-calendar-title{margin:0 -1px;padding:7px 2px;background-image:-webkit-linear-gradient(#f6f6f6,#d9d9d9);background-image:-moz-linear-gradient(#f6f6f6,#d9d9d9);background-image:-ms-linear-gradient(#f6f6f6,#d9d9d9);background-image:-o-linear-gradient(#f6f6f6,#d9d9d9);background-image:linear-gradient(#f6f6f6,#d9d9d9);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#ffd9d9d9', GradientType=0);border-bottom:1px solid #bbb;color:#555;text-align:center;text-shadow:1px 1px 1px #eee;cursor:pointer}.w2ui-calendar .w2ui-calendar-jump{position:absolute;top:27px;left:0;right:0;bottom:0;background-color:#FaFaFa}.w2ui-calendar .w2ui-calendar-jump>:first-child{position:absolute;top:0;left:0;bottom:0;width:110px;overflow:hidden;padding-top:5px;border-right:1px solid silver}.w2ui-calendar .w2ui-calendar-jump>:last-child{position:absolute;top:0;right:0;bottom:0;width:88px;overflow-x:hidden;overflow-y:auto;padding-top:5px;text-align:center}.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-month,.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year{display:inline-block;padding:5px 0;text-align:center;float:left;margin:2px;width:50px;cursor:default;border:1px solid transparent;border-radius:2px}.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year{float:none;width:95%}.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-month:hover,.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year:hover{border:1px solid #ccc;color:#000;background-color:#efefef}.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-month.selected,.w2ui-calendar .w2ui-calendar-jump .w2ui-jump-year.selected{border:1px solid #ccc;color:#000;background-color:#dadada}.w2ui-calendar .w2ui-calendar-previous,.w2ui-calendar .w2ui-calendar-next{width:24px;height:20px;color:#666;border:1px solid transparent;border-radius:3px;padding:2px 3px 1px 2px;margin:-4px 0 0 0;cursor:default}.w2ui-calendar .w2ui-calendar-previous:hover,.w2ui-calendar .w2ui-calendar-next:hover{border:1px solid silver;background-color:#efefef}.w2ui-calendar .w2ui-calendar-previous>div,.w2ui-calendar .w2ui-calendar-next>div{position:absolute;border-left:4px solid #888;border-top:4px solid #888;border-right:4px solid transparent;border-bottom:4px solid transparent;width:0;height:0;padding:0;margin:3px 0 0}.w2ui-calendar .w2ui-calendar-previous{float:left}.w2ui-calendar .w2ui-calendar-previous>div{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg);margin-left:6px}.w2ui-calendar .w2ui-calendar-next{float:right}.w2ui-calendar .w2ui-calendar-next>div{-webkit-transform:rotate(135deg);-moz-transform:rotate(135deg);-ms-transform:rotate(135deg);-o-transform:rotate(135deg);transform:rotate(135deg);margin-left:2px;margin-right:2px}.w2ui-calendar table.w2ui-calendar-days{padding:0}.w2ui-calendar table.w2ui-calendar-days td{border:1px solid #fff;color:#000;background-color:#f9f9f9;padding:6px;cursor:default;text-align:right}.w2ui-calendar table.w2ui-calendar-days td.w2ui-saturday,.w2ui-calendar table.w2ui-calendar-days td.w2ui-sunday{border:1px solid #fff;color:#c8493b;background-color:#f9f9f9}.w2ui-calendar table.w2ui-calendar-days td.w2ui-saturday:hover,.w2ui-calendar table.w2ui-calendar-days td.w2ui-sunday:hover{border:1px solid #ccc;color:#000;background-color:#e9e9e9}.w2ui-calendar table.w2ui-calendar-days td.w2ui-saturday.w2ui-blocked,.w2ui-calendar table.w2ui-calendar-days td.w2ui-sunday.w2ui-blocked{text-decoration:line-through;border:1px solid #fff;color:#ccc;background-color:#fff}.w2ui-calendar table.w2ui-calendar-days td.w2ui-today{border:1px solid #8cb067;color:#000;background-color:#e2f7cd}.w2ui-calendar table.w2ui-calendar-days td:hover{border:1px solid #ccc;color:#000;background-color:#e9e9e9}.w2ui-calendar table.w2ui-calendar-days td.w2ui-blocked{text-decoration:line-through;border:1px solid #fff;color:#ccc;background-color:#fff}.w2ui-calendar table.w2ui-calendar-days td.w2ui-day-empty{border:1px solid #fff;background-color:#fdfdfd}.w2ui-calendar table.w2ui-calendar-days tr.w2ui-day-title td{border:1px solid #fff;color:gray;background-color:#fff;text-align:center;padding:6px}.w2ui-calendar-time{padding:5px;cursor:default}.w2ui-calendar-time td div{padding:7px 10px;text-align:center;border:1px solid transparent;white-space:nowrap}.w2ui-calendar-time td:nth-child(even){background-color:#f6f6f6}.w2ui-calendar-time td div:hover{border:1px solid #ccc;color:#000;background-color:#e9e9e9}.w2ui-calendar-time td div.w2ui-blocked{text-decoration:line-through;border:1px solid #fff;color:#ccc;background-color:#fff}.w2ui-select{cursor:default;color:#000!important;background-image:-webkit-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:-moz-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:-ms-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:-o-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%)}.w2ui-list{color:inherit;position:absolute;padding:0;margin:0;min-height:25px;overflow:auto;border:1px solid silver;border-radius:3px;font-size:6px;line-height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;background-color:#fff}.w2ui-list input[type=text]{-webkit-box-shadow:none;-moz-box-shadow:none;-ms-box-shadow:none;-o-box-shadow:none;box-shadow:none}.w2ui-list ul{list-style-type:none;background-color:#000;margin:0;padding:0}.w2ui-list ul li{float:left;margin:2px 1px 0 2px;border-radius:3px;width:auto;padding:3px 10px 1px 7px;border:1px solid #88b0d6;background-color:#eff3f5;white-space:nowrap;cursor:default;font-family:verdana;font-size:11px;line-height:100%;height:20px;overflow:hidden;text-overflow:ellipsis;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-list ul li:hover{background-color:#d0dbe1}.w2ui-list ul li:last-child{border-radius:0;border:1px solid transparent;background-color:transparent}.w2ui-list ul li:last-child input{padding:1px;padding-top:0;margin:0;border:0;outline:0;height:auto;line-height:100%;font-size:inherit;font-family:inherit;background-color:transparent}.w2ui-list ul li .w2ui-list-remove{float:right;width:15px;height:14px;margin:-1px -9px 0 3px;border-radius:15px}.w2ui-list ul li .w2ui-list-remove:hover{background-color:#D77F7F;color:#fff}.w2ui-list ul li .w2ui-list-remove:before{position:relative;top:0;padding:0;margin:0;left:5px;color:inherit;opacity:.7;text-shadow:inherit;font-size:inherit;font-variant:small-caps;content:'x';line-height:100%}.w2ui-list ul li>span.file-size{pointer-events:none;color:#777}.w2ui-list .w2ui-enum-placeholder{display:inline;position:absolute;pointer-events:none;color:#999;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-list.w2ui-file-dragover{background-color:#E4FFDA;border:1px solid #93E07D}.w2ui-layout{overflow:hidden!important;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-layout *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-layout>div{position:absolute;overflow:hidden;border:0;margin:0;padding:0;outline:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-layout>div .w2ui-panel{display:none;position:absolute;z-index:120}.w2ui-layout>div .w2ui-panel .w2ui-panel-title{padding:5px;background-image:-webkit-linear-gradient(#dae6f3,#c2d5ed);background-image:-moz-linear-gradient(#dae6f3,#c2d5ed);background-image:-ms-linear-gradient(#dae6f3,#c2d5ed);background-image:-o-linear-gradient(#dae6f3,#c2d5ed);background-image:linear-gradient(#dae6f3,#c2d5ed);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffdae6f3', endColorstr='#ffc2d5ed', GradientType=0);border:1px solid #b9cee9;border-bottom:1px solid #99bbe8}.w2ui-layout>div .w2ui-panel .w2ui-panel-tabs{position:absolute;left:0;top:0;right:0;z-index:2;display:none;overflow:hidden;background-color:#fafafa;padding:4px 0}.w2ui-layout>div .w2ui-panel .w2ui-panel-tabs>.w2ui-tab.active{background-color:#f5f6f7}.w2ui-layout>div .w2ui-panel .w2ui-panel-toolbar{position:absolute;left:0;top:0;right:0;z-index:2;display:none;overflow:hidden;background-color:#fafafa;border-bottom:1px solid silver;padding:4px}.w2ui-layout>div .w2ui-panel .w2ui-panel-content{position:absolute;left:0;top:0;right:0;bottom:0;z-index:1;color:inherit;background-color:#f5f6f7}.w2ui-layout>div .w2ui-resizer{display:none;position:absolute;z-index:121;background-color:transparent}.w2ui-layout>div .w2ui-resizer:hover,.w2ui-layout>div .w2ui-resizer.active{background-color:#d7e4f2}.w2ui-grid{position:relative;border:1px solid silver;border-radius:2px;overflow:hidden!important}.w2ui-grid>div{position:absolute;overflow:hidden}.w2ui-grid .w2ui-grid-header{position:absolute;border-bottom:1px solid #99bbe8!important;height:28px;overflow:hidden;color:#444;font-size:13px;text-align:center;padding:7px;background-image:-webkit-linear-gradient(#dae6f3,#c2d5ed);background-image:-moz-linear-gradient(#dae6f3,#c2d5ed);background-image:-ms-linear-gradient(#dae6f3,#c2d5ed);background-image:-o-linear-gradient(#dae6f3,#c2d5ed);background-image:linear-gradient(#dae6f3,#c2d5ed);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffdae6f3', endColorstr='#ffc2d5ed', GradientType=0);border-top-left-radius:2px;border-top-right-radius:2px}.w2ui-grid .w2ui-grid-toolbar{position:absolute;border-bottom:1px solid silver;background-color:#eaeaea;height:38px;padding:7px 3px 4px;margin:0;box-shadow:0 1px 2px #ddd}.w2ui-grid .w2ui-toolbar-search{width:160px;margin-right:3px}.w2ui-grid .w2ui-toolbar-search .w2ui-search-all{outline:0!important;width:160px;border-radius:10px;line-height:normal;height:22px;border:1px solid #b9b9b9;color:#000;background-color:#fff;padding:3px 18px 3px 23px;margin:0}.w2ui-grid .w2ui-toolbar-search .w2ui-search-down{position:absolute;margin-top:-7px;margin-left:6px}.w2ui-grid .w2ui-toolbar-search .w2ui-search-clear{position:absolute;width:16px;height:16px;margin-top:-8px;margin-left:-20px;border-radius:15px;cursor:default}.w2ui-grid .w2ui-toolbar-search .w2ui-search-clear:hover{background-color:#D77F7F;color:#fff}.w2ui-grid .w2ui-toolbar-search .w2ui-search-clear:before{position:relative;top:1px;left:5px;opacity:.6;color:inherit;text-shadow:inherit;content:'x';cursor:default}.w2ui-grid .w2ui-grid-body{position:absolute;overflow:hidden;padding:0;background-color:#fff;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.w2ui-grid .w2ui-grid-body input,.w2ui-grid .w2ui-grid-body select,.w2ui-grid .w2ui-grid-body textarea{user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;-o-user-select:text}.w2ui-grid .w2ui-grid-body .w2ui-grid-columns{overflow:hidden;position:absolute;left:0;top:0;right:0;box-shadow:0 1px 4px #ddd;height:auto}.w2ui-grid .w2ui-grid-body .w2ui-grid-columns table{height:auto}.w2ui-grid .w2ui-grid-body .w2ui-grid-columns .w2ui-resizer{position:absolute;z-index:1000;display:block;background-image:none;background-color:rgba(0,0,0,0);padding:0;margin:0;width:6px;height:12px;cursor:col-resize}.w2ui-grid .w2ui-grid-body .w2ui-grid-records{position:absolute;left:0;right:0;top:0;bottom:0}.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd{color:inherit;background-color:#fff}.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd:hover{color:inherit;background-color:#e6f0ff}.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd.w2ui-empty-record:hover{background-color:#fff}.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even{color:inherit;background-color:#f3f6fa}.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even:hover{color:inherit;background-color:#e6f0ff}.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even.w2ui-empty-record:hover{background-color:#f3f6fa}.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-selected,.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-selected{color:#000!important;background-color:#b6d5ff!important}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded{background-color:#CCDCF0!important}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded1{height:0;border-bottom:1px solid #b2bac0;background-color:#CCDCF0}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded1>div{height:100%;margin:0;padding:0}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded2{height:0;border-radius:0;border-bottom:1px solid #b2bac0}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded2>div{height:0;border:0;transition:height .3s,opacity .3s}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more{border-top:1px solid #d6d5d7;cursor:pointer}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more>div{text-align:center;color:#777;background-color:rgba(233,237,243,.5);padding:10px 0 15px;border-top:1px solid #fff}.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more>div:hover{color:inherit;background-color:#e6f0ff}.w2ui-grid .w2ui-grid-body table{border-spacing:0;border-collapse:collapse;table-layout:fixed;width:1px}.w2ui-grid .w2ui-grid-body table .w2ui-head{margin:0;padding:0;border-right:1px solid #c5c5c5;border-bottom:1px solid #c5c5c5;color:#000;background-image:-webkit-linear-gradient(#f9f9f9,#e4e4e4);background-image:-moz-linear-gradient(#f9f9f9,#e4e4e4);background-image:-ms-linear-gradient(#f9f9f9,#e4e4e4);background-image:-o-linear-gradient(#f9f9f9,#e4e4e4);background-image:linear-gradient(#f9f9f9,#e4e4e4);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#ffe4e4e4', GradientType=0)}.w2ui-grid .w2ui-grid-body table .w2ui-head>div{padding:7px 3px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;position:relative}.w2ui-grid .w2ui-grid-body table .w2ui-head.w2ui-col-intersection{border-right-color:#72b2ff}.w2ui-grid .w2ui-grid-body table .w2ui-head.w2ui-reorder-cols-head:hover{cursor:move}.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker{padding:0;position:absolute;height:100%;top:0}.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker.left{left:0;margin-left:-5px}.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker.right{right:0;margin-right:-5px}.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker .top-marker{position:absolute;top:0;height:0;width:0;border-top:5px solid #72b2ff;border-left:5px solid transparent;border-right:5px solid transparent}.w2ui-grid .w2ui-grid-body table .w2ui-head .col-intersection-marker .bottom-marker{position:absolute;bottom:0;height:0;width:0;border-bottom:5px solid #72b2ff;border-left:5px solid transparent;border-right:5px solid transparent}.w2ui-grid .w2ui-grid-body table td{border-right:1px solid #d6d5d7;border-bottom:0 solid #d6d5d7;cursor:default;overflow:hidden}.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data{margin:0;padding:0}.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data>div{padding:3px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data>div.flexible-record{height:auto;overflow:visible;white-space:normal}.w2ui-grid .w2ui-grid-body table td:last-child{border-right:0}.w2ui-grid .w2ui-grid-body table .w2ui-col-number{width:34px;color:#777;background-color:rgba(233,237,243,.5)}.w2ui-grid .w2ui-grid-body table .w2ui-col-number div{padding:0 7px 0 3px;text-align:right}.w2ui-grid .w2ui-grid-body table .w2ui-col-select{width:26px}.w2ui-grid .w2ui-grid-body table .w2ui-col-select div{padding:0;text-align:center;overflow:hidden}.w2ui-grid .w2ui-grid-body table .w2ui-col-select div input[type=checkbox]{margin-top:2px;position:relative}.w2ui-grid .w2ui-grid-body table .w2ui-col-expand{width:26px}.w2ui-grid .w2ui-grid-body table .w2ui-col-expand div{padding:0;text-align:center;font-weight:700}.w2ui-grid .w2ui-grid-body div.w2ui-col-header{height:auto!important;width:100%;overflow:hidden;padding-right:10px!important}.w2ui-grid .w2ui-grid-body div.w2ui-col-header>div.w2ui-sort-up{border:4px solid transparent;border-bottom:5px solid #8D99A7;margin-top:-2px;margin-right:-7px;float:right}.w2ui-grid .w2ui-grid-body div.w2ui-col-header>div.w2ui-sort-down{border:4px solid transparent;border-top:5px solid #8D99A7;margin-top:2px;margin-right:-7px;float:right}.w2ui-grid .w2ui-grid-body .w2ui-col-group{text-align:center}.w2ui-grid .w2ui-changed{background:url(data:image/gif;base64,R0lGODlhCgAKAJEAALAABf///wAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAIPlI8Hy8mbxIsSUnup3rQAADs=) no-repeat top right}.w2ui-grid .w2ui-editable{overflow:hidden;height:100%!important;margin:0!important;padding:0!important}.w2ui-grid .w2ui-editable input{border:0;border-radius:0;margin:0;padding:4px 3px;width:100%;height:100%}.w2ui-grid .w2ui-editable input.w2ui-select{outline:0!important;background:#fff}.w2ui-grid .w2ui-grid-summary{position:absolute;box-shadow:0 -1px 4px #aaa}.w2ui-grid .w2ui-grid-summary table{color:inherit}.w2ui-grid .w2ui-grid-summary table .w2ui-odd{background-color:#eef5eb}.w2ui-grid .w2ui-grid-summary table .w2ui-even{background-color:#f8fff5}.w2ui-grid .w2ui-grid-footer{position:absolute;margin:0;padding:0;text-align:center;height:24px;overflow:hidden;user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;-o-user-select:text;box-shadow:0 -1px 4px #eee;color:#444;background-color:#f8f8f8;border-top:1px solid #ddd;border-bottom-left-radius:2px;border-bottom-right-radius:2px}.w2ui-grid .w2ui-grid-footer .w2ui-footer-left{float:left;padding-top:5px;padding-left:5px}.w2ui-grid .w2ui-grid-footer .w2ui-footer-right{float:right;padding-top:5px;padding-right:5px}.w2ui-grid .w2ui-grid-footer .w2ui-footer-center{padding:2px;text-align:center}.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav{width:110px;margin:0 auto;padding:0;text-align:center}.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav input[type=text]{padding:1px 2px 2px;border-radius:3px;width:40px;text-align:center}.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav a.w2ui-footer-btn{display:inline-block;border-radius:3px;cursor:pointer;font-size:11px;line-height:16px;padding:1px 5px;width:30px;height:18px;margin-top:-1px;color:#000;background-color:transparent}.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav a.w2ui-footer-btn:hover{color:#000;background-color:#aec8ff}.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd,.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even,.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd:hover,.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even:hover{background-color:inherit}.w2ui-ss .w2ui-grid-records table td{border-right-width:1px;border-bottom:1px solid #efefef}.w2ui-ss .w2ui-grid-records table tr:first-child td{border-bottom:0}.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-selected,.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-selected{background-color:#EEF4FE!important}.w2ui-ss .w2ui-changed{background:inherit}.w2ui-ss .w2ui-grid-body .w2ui-selection{position:absolute;border:2px solid #6299DA;pointer-events:none}.w2ui-ss .w2ui-grid-body .w2ui-selection .w2ui-selection-resizer{cursor:crosshair;position:absolute;bottom:0;right:0;width:6px;height:6px;margin-right:-3px;margin-bottom:-3px;background-color:#457FC2;border:.5px solid #fff;outline:1px solid #fff;pointer-events:auto}.w2ui-overlay .w2ui-select-field{padding:8px 5px;cursor:default}.w2ui-overlay .w2ui-select-field table{font-size:11px;font-family:Verdana,Arial,sans-serif;border-spacing:0;border-collapse:border-collapse}.w2ui-overlay .w2ui-select-field table tr:hover{background-color:#b6d5ff}.w2ui-overlay .w2ui-select-field table td:nth-child(1){padding:3px 3px 3px 6px}.w2ui-overlay .w2ui-select-field table td:nth-child(1) input{margin:3px 2px 2px}.w2ui-overlay .w2ui-select-field table td:nth-child(2){padding:3px 15px 3px 3px}.w2ui-overlay .w2ui-col-on-off{padding:4px 0}.w2ui-overlay .w2ui-col-on-off table{border-spacing:0;border-collapse:border-collapse}.w2ui-overlay .w2ui-col-on-off table tr:hover{background-color:#b6d5ff}.w2ui-overlay .w2ui-col-on-off table td input[type=checkbox]{margin:3px 2px 2px}.w2ui-overlay .w2ui-col-on-off table td label{display:block;padding:3px 0;padding-right:10px}.w2ui-overlay .w2ui-col-on-off table td:first-child{padding:4px 0 4px 6px}.w2ui-overlay .w2ui-col-on-off table td:last-child{padding:4px 6px 4px 0}.w2ui-overlay .w2ui-grid-searches{text-align:left;padding:0;border-top:0;background-color:#f7f6f0}.w2ui-overlay .w2ui-grid-searches table{padding:4px;padding-top:12px;border-collapse:border-collapse}.w2ui-overlay .w2ui-grid-searches table td{padding:4px}.w2ui-overlay .w2ui-grid-searches table td.close-btn{width:20px;padding-right:20px}.w2ui-overlay .w2ui-grid-searches table td.close-btn button{min-width:24px;height:24px;padding-top:6px!important}.w2ui-overlay .w2ui-grid-searches table td.caption{text-align:right;padding-right:5px;border-right:1px solid #e8e8e3}.w2ui-overlay .w2ui-grid-searches table td.operator{text-align:left;padding:0 10px;padding-right:5px;border-right:1px solid #e8e8e3}.w2ui-overlay .w2ui-grid-searches table td.operator select{width:100%;color:#000;padding:0 15px 0 5px;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-image:-webkit-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:-moz-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:-ms-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:-o-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-image:linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%)}.w2ui-overlay .w2ui-grid-searches table td.operator select::-ms-expand{display:none}.w2ui-overlay .w2ui-grid-searches table td.value{padding-right:5px;padding-left:5px}.w2ui-overlay .w2ui-grid-searches table td.value input[type=text]{border-radius:3px;padding:3px;margin-right:3px;height:23px}.w2ui-overlay .w2ui-grid-searches table td.value select{padding:3px;margin-right:3px;height:23px}.w2ui-overlay .w2ui-grid-searches table td.actions{border-right:0}.w2ui-overlay .w2ui-grid-searches table td.actions>div{margin:-7px;margin-top:15px;padding:13px 0;text-align:center;background-color:#efefe9;border-top:1px solid #e8e8e3}.w2ui-popup{position:fixed;z-index:1600;overflow:hidden;font-family:Verdana,Arial,sans-serif;border-radius:6px;padding:0;margin:0;border:1px solid #777;background-color:#eee;box-shadow:0 0 25px #555}.w2ui-popup,.w2ui-popup *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-popup .w2ui-msg-title{padding:6px;border-radius:6px 6px 0 0;background-image:-webkit-linear-gradient(#ececec,#dfdfdf);background-image:-moz-linear-gradient(#ececec,#dfdfdf);background-image:-ms-linear-gradient(#ececec,#dfdfdf);background-image:-o-linear-gradient(#ececec,#dfdfdf);background-image:linear-gradient(#ececec,#dfdfdf);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffececec', endColorstr='#ffdfdfdf', GradientType=0);border-bottom:2px solid #bfbfbf;position:absolute;overflow:hidden;height:32px;left:0;right:0;top:0;text-overflow:ellipsis;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;cursor:move;font-size:15px;color:#555;z-index:300}.w2ui-popup .w2ui-msg-button{float:right;width:18px;height:18px;cursor:pointer;overflow:hidden;padding:0;margin:0 3px 0 0;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAj1JREFUeNrslr9rFFEQxz/zZi/qxSgW2lsqkiYoBku5Ro1o4UFKEYkgSaxSCLYqdv5IEVPYCMJJwERWrK0CKhoQ8hdobQTjXW7njcXlYnLunQQu0YDTLOy+Nzvfme98Z8Td2ckW2OGWdMvRvYfT/RGfBPoBBVpLK0AEPgVkdGL06vt/CoB5nBaRE8AXYKXNsQIwaB4fAwOtH+88mn4m7ifN4vUYebWBKkFKqjIV3N9NjI2Uuw5ARI45fBanH+F77iFnN8JHETmS68P9NHBQNTwHL8foaSN4SqoyA/SZyL4tqQAQBVYCLOFYlNxmq0WorVLpN9Oe5LKt1CsgRVWpAOfB66phBuhTkepSdfnKVjaxNJMSWn/iawmTtpeDp6pWBpaBoqrMqoYU6AOqIbFhxGa3R4V8nfNNKLUESzXJhoCvQC+wF/gW1C5IiC+2XUbD5jA3rd4C26NR3945IA2iRzqRJgdElJJlSQocAKrAD2A/6Ev3cLajjN59MDWHyKl2voOI1zKbv3Xj2lCHJFoz+LXuBoIAjnUklEvJrDDT5LwmdhG8blkyBxRjXSu4loE0X4VEznXKV3SnoOFMB7YUolBcbcKNdxuPXUBPu8pbLXsK0ghebVjEXgNoYmXLtGLuxd6ePU+AQ20AaIrb4DpFycmSv81/7YsiMgAstB1kQgE47O4LuQmCNwGOB7VxCb/URsRSTbhkmU4ifGiZHd1Z5m7fnxoIQSaBo39YJRZj9LGb4yPzXWm1/9voX7afAwAC5tacDTA2XgAAAABJRU5ErkJggg==) no-repeat center left;background-position:0 0;color:transparent!important;border-radius:3px;border:1px solid transparent}.w2ui-popup .w2ui-msg-close{margin-top:0;background-position:-32px 0}.w2ui-popup .w2ui-msg-close:hover{background-color:#ccc;border:1px solid #aaa}.w2ui-popup .w2ui-msg-max{background-position:-16px 0}.w2ui-popup .w2ui-msg-max:hover{background-color:#ccc;border:1px solid #aaa}.w2ui-popup .w2ui-box1,.w2ui-popup .w2ui-box2{position:absolute;left:0;right:0;top:32px;bottom:55px;z-index:100}.w2ui-popup .w2ui-msg-body{font-size:13px;line-height:130%;padding:0 7px 7px;color:#000;background-color:#eee;position:absolute;overflow:auto;width:100%;height:100%}.w2ui-popup .w2ui-popup-message{position:absolute;z-index:250;background-color:#f9f9f9;border:1px solid #999;box-shadow:0 0 15px #aaa;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;border-top:0;border-radius:0 0 6px 6px;overflow:auto}.w2ui-popup .w2ui-msg-buttons{padding:12px;border-radius:0 0 6px 6px;border-top:1px solid #d5d8d8;background-color:#f1f1f1;text-align:center;position:absolute;overflow:hidden;height:52px;left:0;right:0;bottom:0;z-index:200}.w2ui-popup .w2ui-msg-no-title{border-top-left-radius:6px;border-top-right-radius:6px;top:0!important}.w2ui-popup .w2ui-msg-no-buttons{border-bottom-left-radius:6px;border-bottom-right-radius:6px;bottom:0!important}.w2ui-sidebar{cursor:default;overflow:hidden!important;background-color:#edf1f6!important;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-sidebar *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-sidebar>div{position:relative;overflow:hidden}.w2ui-sidebar .w2ui-sidebar-top{position:absolute;z-index:2;top:0;left:0;right:0}.w2ui-sidebar .w2ui-sidebar-bottom{position:absolute;z-index:2;bottom:0;left:0;right:0}.w2ui-sidebar .w2ui-sidebar-div{position:absolute;z-index:1;overflow:auto;top:0;bottom:0;left:0;right:0;padding:2px 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.w2ui-sidebar .w2ui-sidebar-div table{width:100%}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node{background-color:#edf1f6;border-top:1px solid transparent;border-bottom:1px solid transparent;margin:0;padding:1px 0}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node table{pointer-events:none}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-caption,.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image,.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image>span,.w2ui-sidebar .w2ui-sidebar-div .w2ui-node td.w2ui-node-dots{color:#000;text-shadow:0 0 0 #fff;pointer-events:none}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-caption:hover,.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image:hover,.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image>span:hover,.w2ui-sidebar .w2ui-sidebar-div .w2ui-node td.w2ui-node-dots:hover{color:inherit}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node:hover{border-top:1px solid #f9f9f9;border-bottom:1px solid #f9f9f9;background-color:#d7e1ef}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image{width:22px;text-align:center;pointer-events:none}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node .w2ui-node-image>span{color:#516173!important}.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover{background-image:-webkit-linear-gradient(#69b1e0,#4a96d3);background-image:-moz-linear-gradient(#69b1e0,#4a96d3);background-image:-ms-linear-gradient(#69b1e0,#4a96d3);background-image:-o-linear-gradient(#69b1e0,#4a96d3);background-image:linear-gradient(#69b1e0,#4a96d3);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff69b1e0', endColorstr='#ff4a96d3', GradientType=0);border-top:1px solid #5295cd;border-bottom:1px solid #2661a6}.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected .w2ui-node-caption,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover .w2ui-node-caption,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected .w2ui-node-image,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover .w2ui-node-image,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected .w2ui-node-image>span,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover .w2ui-node-image>span,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected td.w2ui-node-dots,.w2ui-sidebar .w2ui-sidebar-div .w2ui-selected:hover td.w2ui-node-dots{color:#fff!important;text-shadow:1px 1px 2px #666!important}.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover{background:transparent!important;border-top:1px solid transparent;border-bottom:1px solid transparent}.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled .w2ui-node-caption,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover .w2ui-node-caption,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled .w2ui-node-image,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover .w2ui-node-image,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled .w2ui-node-image>span,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover .w2ui-node-image>span,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled td.w2ui-node-dots,.w2ui-sidebar .w2ui-sidebar-div .w2ui-disabled:hover td.w2ui-node-dots{opacity:.4;filter:alpha(opacity=40);color:#000!important;text-shadow:0 0 0 #fff!important}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-caption{white-space:nowrap;padding:5px 0 5px 3px;margin:1px 0 1px 22px;position:relative;z-index:1;font-size:12px}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-group{white-space:nowrap;overflow:hidden;padding:10px 0 10px 10px;margin:0;cursor:default;color:#868b92;background-color:transparent}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-group :nth-child(1){margin-right:10px;float:right;color:transparent}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-group :nth-child(2){font-weight:400;text-transform:uppercase}.w2ui-sidebar .w2ui-sidebar-div .w2ui-node-sub{overflow:hidden}.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-dots{width:18px;padding:0 0 1px 7px;text-align:center}.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-dots .w2ui-expand{width:16px;margin-top:-3px;pointer-events:auto}.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data{padding:1px 1px 3px}.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-image{padding:3px 0 0;float:left}.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-image>span{font-size:16px;color:#000;text-shadow:0 0 0 #fff}.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-image.w2ui-icon{margin-top:3px}.w2ui-sidebar .w2ui-sidebar-div td.w2ui-node-data .w2ui-node-count{float:right;border:1px solid #9da4af;border-radius:20px;width:auto;height:18px;padding:2px 7px;margin:3px 4px -2px 0;background-color:#e7f0fc;color:#667274;box-shadow:0 0 2px #fff;text-shadow:1px 1px 1px #e6e6e6;position:relative;z-index:2}.w2ui-tabs{cursor:default;overflow:hidden!important;background-color:#fafafa;padding:3px 0;padding-bottom:0!important}.w2ui-tabs table{border-bottom:1px solid silver;padding:0 7px}.w2ui-tabs .w2ui-tab{padding:6px 20px;text-align:center;color:#000;background-color:transparent;border:1px solid silver;border-bottom:1px solid silver;white-space:nowrap;margin:1px 1px -1px 0;border-top-left-radius:4px;border-top-right-radius:4px;cursor:default}.w2ui-tabs .w2ui-tab.active{color:#000;background-color:#fff;border:1px solid silver;border-bottom:1px solid transparent}.w2ui-tabs .w2ui-tab.closable{padding:6px 28px 6px 20px}.w2ui-tabs .w2ui-tab-close{color:#555;text-shadow:1px 1px 1px #bbb;float:right;margin:6px 4px 0 0;padding:0 0 0 5px;width:16px;height:16px;opacity:.9;border:0;border-top:3px solid transparent;border-radius:9px}.w2ui-tabs .w2ui-tab-close:hover{background-color:#D77F7F;color:#fff}.w2ui-tabs .w2ui-tab-close:before{position:relative;top:-2px;left:0;opacity:.6;color:inherit;text-shadow:inherit;content:'x'}.w2ui-toolbar{margin:0;padding:2px;outline:0;background-color:#efefef;overflow:hidden!important;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.w2ui-toolbar .disabled{opacity:.3;filter:alpha(opacity=30)}.w2ui-toolbar table{table-layout:auto!important}.w2ui-toolbar table td{border:0!important}.w2ui-toolbar table.w2ui-button{margin:0 1px;border-radius:4px;height:24px;border:1px solid transparent;background-color:transparent}.w2ui-toolbar table.w2ui-button .w2ui-tb-image{width:16px;height:16px;padding:0;margin:2px 4px 3px 3px!important;border:0!important;text-align:center}.w2ui-toolbar table.w2ui-button .w2ui-tb-image>span{font-size:15px;margin-top:3px;display:block;color:#8d99a7}.w2ui-toolbar table.w2ui-button .w2ui-tb-caption{color:#000;padding:0 4px 0 2px}.w2ui-toolbar table.w2ui-button .w2ui-tb-count{padding:0 4px 0 0}.w2ui-toolbar table.w2ui-button .w2ui-tb-count>span{border:1px solid #9da4af;border-radius:20px;width:auto;height:18px;padding:2px 7px;background-color:#e7f0fc;color:#667274;box-shadow:0 0 2px #fff;text-shadow:1px 1px 1px #e6e6e6}.w2ui-toolbar table.w2ui-button .w2ui-tb-down{padding:3px}.w2ui-toolbar table.w2ui-button .w2ui-tb-down>div{border:4px solid transparent;border-top:5px solid #8D99A7;margin-top:5px}.w2ui-toolbar table.w2ui-button.over{border:1px solid #ccc;background-color:#eee}.w2ui-toolbar table.w2ui-button.over .w2ui-tb-caption{color:#000}.w2ui-toolbar table.w2ui-button.down{border:1px solid #aaa;background-color:#ddd}.w2ui-toolbar table.w2ui-button.down .w2ui-tb-caption{color:#666}.w2ui-toolbar table.w2ui-button.checked{border:1px solid #aaa;background-color:#fff}.w2ui-toolbar table.w2ui-button.checked .w2ui-tb-caption{color:#000}.w2ui-toolbar table.w2ui-button table{height:17px;border-radius:4px;cursor:default}.w2ui-toolbar .w2ui-break{background-image:-webkit-linear-gradient(top,rgba(153,153,153,.1) 0,#999 40%,#999 60%,rgba(153,153,153,.1) 100%);background-image:-moz-linear-gradient(top,rgba(153,153,153,.1) 0,#999 40%,#999 60%,rgba(153,153,153,.1) 100%);background-image:-ms-linear-gradient(top,rgba(153,153,153,.1) 0,#999 40%,#999 60%,rgba(153,153,153,.1) 100%);background-image:-o-linear-gradient(top,rgba(153,153,153,.1) 0,#999 40%,#999 60%,rgba(153,153,153,.1) 100%);background-image:linear-gradient(top,rgba(153,153,153,.1) 0,#999 40%,#999 60%,rgba(153,153,153,.1) 100%);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff999999', endColorstr='#ff999999', GradientType=0);width:1px!important;height:22px;padding:0;margin:0 6px}.w2ui-listview{overflow:auto!important;background-color:#fff!important;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-listview *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.w2ui-listview>ul{list-style-type:none;margin:0;cursor:default}.w2ui-listview>ul>li{display:inline-block;vertical-align:top;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;border:1px solid transparent;border-radius:4px}.w2ui-listview>ul>li.w2ui-focused{border:1px solid #2661a6}.w2ui-listview>ul>li.w2ui-selected{border:1px solid #2661a6}.w2ui-listview>ul>li.w2ui-selected,.w2ui-listview>ul>li.w2ui-selected.hover{background-image:-webkit-linear-gradient(#69b1e0,#4a96d3);background-image:-moz-linear-gradient(#69b1e0,#4a96d3);background-image:-ms-linear-gradient(#69b1e0,#4a96d3);background-image:-o-linear-gradient(#69b1e0,#4a96d3);background-image:linear-gradient(#69b1e0,#4a96d3);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff69b1e0', endColorstr='#ff4a96d3', GradientType=0)}.w2ui-listview>ul>li.w2ui-selected>div>div.caption,.w2ui-listview>ul>li.w2ui-selected.hover>div>div.caption{color:#fff}.w2ui-listview>ul>li.w2ui-selected>div>div.description,.w2ui-listview>ul>li.w2ui-selected.hover>div>div.description{color:#ddd}.w2ui-listview>ul>li.w2ui-selected>div>div.extra>div>div,.w2ui-listview>ul>li.w2ui-selected.hover>div>div.extra>div>div{color:#ddd}.w2ui-listview>ul>li.hover{background-color:#d7e1ef;border:1px solid #2661a6}.w2ui-listview>ul>li div{vertical-align:middle}.w2ui-listview>ul>li>div>div.caption{display:block;text-align:center;word-wrap:break-word;max-height:50px;color:#000;font-size:12px}.w2ui-listview>ul>li>div>div.description{display:none;text-align:left;color:#777;font-size:12px}.w2ui-listview>ul>li>div>div.extra{display:none}.w2ui-listview>ul>li>div>div.extra>div>div{color:#777}.w2ui-icon-small>ul{padding:1px 0 0 1px}.w2ui-icon-small>ul>li{margin:0 1px 1px 0;padding:2px;width:250px;white-space:nowrap}.w2ui-icon-small>ul>li>div>div.w2ui-listview-img{display:inline-block;width:26px;height:22px;font-size:21px;margin-right:2px}.w2ui-icon-small>ul>li>div>div.caption{display:inline-block}.w2ui-icon-medium>ul{padding:4px 0 0 4px}.w2ui-icon-medium>ul>li{margin:0 4px 4px 0;padding:4px;width:100px}.w2ui-icon-medium>ul>li>div>div.w2ui-listview-img{display:block;width:92px;height:60px;font-size:57px;margin-left:auto;margin-right:auto;background-position:center}.w2ui-icon-large>ul{padding:4px 0 0 4px}.w2ui-icon-large>ul>li{margin:0 4px 4px 0;padding:4px;width:160px}.w2ui-icon-large>ul>li>div>div.w2ui-listview-img{display:block;width:152px;height:120px;font-size:114px;margin-left:auto;margin-right:auto;background-position:center}.w2ui-icon-tile>ul{padding:1px 0 0 1px}.w2ui-icon-tile>ul>li{margin:0 1px 1px 0;padding:4px;width:250px;white-space:nowrap}.w2ui-icon-tile>ul>li>div>div.w2ui-listview-img{display:inline-block;width:72px;height:60px;font-size:57px;float:left;margin-right:4px}.w2ui-icon-tile>ul>li>div>div.caption{text-align:left}.w2ui-icon-tile>ul>li>div>div.description{display:block}.w2ui-table>ul{padding:0}.w2ui-table>ul>li{width:100%;padding:2px;border-radius:0;border-bottom:1px dotted #d3d3d3}.w2ui-table>ul>li>div{display:inline-block;position:relative;width:100%;white-space:nowrap;overflow:hidden}.w2ui-table>ul>li>div>div.w2ui-listview-img{display:inline-block;width:38px;height:32px;font-size:31px;margin-right:2px}.w2ui-table>ul>li>div>div.caption{display:inline-block}.w2ui-table>ul>li>div>div.extra{display:inline-block;position:absolute;right:0;height:100%;background-color:#fff}.w2ui-table>ul>li>div>div.extra>div:before{display:inline-block;height:100%;width:0;content:'';vertical-align:middle}.w2ui-table>ul>li>div>div.extra>div{display:inline}.w2ui-table>ul>li>div>div.extra>div>div{display:inline-block;font-size:12px}.w2ui-table>ul>li.w2ui-selected div.extra,.w2ui-table>ul>li.w2ui-selected.hover div.extra{background-image:-webkit-linear-gradient(#69b1e0,#4a96d3);background-image:-moz-linear-gradient(#69b1e0,#4a96d3);background-image:-ms-linear-gradient(#69b1e0,#4a96d3);background-image:-o-linear-gradient(#69b1e0,#4a96d3);background-image:linear-gradient(#69b1e0,#4a96d3);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff69b1e0', endColorstr='#ff4a96d3', GradientType=0)}.w2ui-table>ul>li.hover div.extra{background-color:#d7e1ef}.w2ui-listview>ul>li div.icon-none{border:1px solid rgba(102,102,102,.35)} \ No newline at end of file
diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.js b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.js
new file mode 100644
index 00000000..d82e32ed
--- /dev/null
+++ b/simple/simple-http/src/test/java/org/simpleframework/http/socket/table/w2ui-1.4.min.js
@@ -0,0 +1,11 @@
+/* w2ui 1.4 (c) http://w2ui.com, vitmalina@gmail.com */
+var w2ui=w2ui||{},w2obj=w2obj||{},w2utils=function(){function a(a){var b=/^[-+]?[0-9]+$/;return b.test(a)}function b(a){return("number"==typeof a||"string"==typeof a&&""!==a)&&!isNaN(Number(a))}function c(a){var b=w2utils.settings,c=new RegExp("^"+(b.currencyPrefix?"\\"+b.currencyPrefix+"?":"")+"[-+]?[0-9]*[.]?[0-9]+"+(b.currencySuffix?"\\"+b.currencySuffix+"?":"")+"$","i");return"string"==typeof a&&(a=a.replace(new RegExp(b.groupSymbol,"g"),"")),"object"==typeof a||""===a?!1:c.test(a)}function d(a){var b=/^[a-fA-F0-9]+$/;return b.test(a)}function e(a){var b=/^[a-zA-Z0-9_-]+$/;return b.test(a)}function f(a){var b=/^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;return b.test(a)}function g(b,c,d){if(!b)return!1;var e,f,g,h="Invalid Date";if(null==c&&(c=w2utils.settings.date_format),"function"==typeof b.getUTCFullYear&&"function"==typeof b.getUTCMonth&&"function"==typeof b.getUTCDate)g=b.getUTCFullYear(),e=b.getUTCMonth(),f=b.getUTCDate();else if("function"==typeof b.getFullYear&&"function"==typeof b.getMonth&&"function"==typeof b.getDate)g=b.getFullYear(),e=b.getMonth(),f=b.getDate();else{if(b=String(b),RegExp("mon","ig").test(c)){c=c.replace(/month/gi,"m").replace(/mon/gi,"m").replace(/dd/gi,"d").replace(/[, ]/gi,"/").replace(/\/\//g,"/").toLowerCase(),b=b.replace(/[, ]/gi,"/").replace(/\/\//g,"/").toLowerCase();for(var i=0,j=w2utils.settings.fullmonths.length;j>i;i++){var k=w2utils.settings.fullmonths[i];b=b.replace(RegExp(k,"ig"),parseInt(i)+1).replace(RegExp(k.substr(0,3),"ig"),parseInt(i)+1)}}var l=b.replace(/-/g,"/").replace(/\./g,"/").toLowerCase().split("/"),m=c.replace(/-/g,"/").replace(/\./g,"/").toLowerCase();"mm/dd/yyyy"===m&&(e=l[0],f=l[1],g=l[2]),"m/d/yyyy"===m&&(e=l[0],f=l[1],g=l[2]),"dd/mm/yyyy"===m&&(e=l[1],f=l[0],g=l[2]),"d/m/yyyy"===m&&(e=l[1],f=l[0],g=l[2]),"yyyy/dd/mm"===m&&(e=l[2],f=l[1],g=l[0]),"yyyy/d/m"===m&&(e=l[2],f=l[1],g=l[0]),"yyyy/mm/dd"===m&&(e=l[1],f=l[2],g=l[0]),"yyyy/m/d"===m&&(e=l[1],f=l[2],g=l[0]),"mm/dd/yy"===m&&(e=l[0],f=l[1],g=l[2]),"m/d/yy"===m&&(e=l[0],f=l[1],g=parseInt(l[2])+1900),"dd/mm/yy"===m&&(e=l[1],f=l[0],g=parseInt(l[2])+1900),"d/m/yy"===m&&(e=l[1],f=l[0],g=parseInt(l[2])+1900),"yy/dd/mm"===m&&(e=l[2],f=l[1],g=parseInt(l[0])+1900),"yy/d/m"===m&&(e=l[2],f=l[1],g=parseInt(l[0])+1900),"yy/mm/dd"===m&&(e=l[1],f=l[2],g=parseInt(l[0])+1900),"yy/m/d"===m&&(e=l[1],f=l[2],g=parseInt(l[0])+1900)}return a(g)&&a(e)&&a(f)?(g=+g,e=+e,f=+f,h=new Date(g,e-1,f),null==e?!1:"Invalid Date"===h?!1:h.getMonth()+1!==e||h.getDate()!==f||h.getFullYear()!==g?!1:d===!0?h:!0):!1}function h(a,b){if(null==a)return!1;var c,d;a=String(a),a=a.toUpperCase(),d=a.indexOf("PM")>=0;var e=d||a.indexOf("AM")>=0;c=e?12:24,a=a.replace("AM","").replace("PM",""),a=$.trim(a);var f=a.split(":"),g=parseInt(f[0]||0),h=parseInt(f[1]||0);return e&&1===f.length||2===f.length?""===f[0]||0>g||g>c||!this.isInt(f[0])||f[0].length>2?!1:2===f.length&&(""===f[1]||0>h||h>59||!this.isInt(f[1])||2!==f[1].length)?!1:e||c!==g||0===h?e&&1===f.length&&0===g?!1:b===!0?(d&&(g+=12),{hours:g,minutes:h}):!0:!1:!1}function i(a){if(""===a||null==a)return"";var b=new Date(a);if(w2utils.isInt(a)&&(b=new Date(Number(a))),"Invalid Date"===b)return"";var c=new Date,d=(c.getTime()-b.getTime())/1e3,e="",f="";return 0>d?(e='<span style="color: #aaa">future</span>',f=""):60>d?(e=Math.floor(d),f="sec",0>d&&(e=0,f="sec")):3600>d?(e=Math.floor(d/60),f="min"):86400>d?(e=Math.floor(d/60/60),f="hour"):2592e3>d?(e=Math.floor(d/24/60/60),f="day"):31557600>d?(e=Math.floor(d/365.25/24/60/60*10)/10,f="month"):d>=31557600&&(e=Math.floor(d/365.25/24/60/60*10)/10,f="year"),e+" "+f+(e>1?"s":"")}function j(a){if(""===a||null==a)return"";var b=new Date(a);if(w2utils.isInt(a)&&(b=new Date(Number(a))),"Invalid Date"===b)return"";var c=w2utils.settings.shortmonths,d=new Date,e=new Date;e.setTime(e.getTime()-864e5);var f=c[b.getMonth()]+" "+b.getDate()+", "+b.getFullYear(),g=c[d.getMonth()]+" "+d.getDate()+", "+d.getFullYear(),h=c[e.getMonth()]+" "+e.getDate()+", "+e.getFullYear(),i=b.getHours()-(b.getHours()>12?12:0)+":"+(b.getMinutes()<10?"0":"")+b.getMinutes()+" "+(b.getHours()>=12?"pm":"am"),j=b.getHours()-(b.getHours()>12?12:0)+":"+(b.getMinutes()<10?"0":"")+b.getMinutes()+":"+(b.getSeconds()<10?"0":"")+b.getSeconds()+" "+(b.getHours()>=12?"pm":"am"),k=f;return f===g&&(k=i),f===h&&(k=w2utils.lang("Yesterday")),'<span title="'+f+" "+j+'">'+k+"</span>"}function k(a){if(!w2utils.isFloat(a)||""===a)return"";if(a=parseFloat(a),0===a)return 0;var b=["Bt","KB","MB","GB","TB"],c=parseInt(Math.floor(Math.log(a)/Math.log(1024)));return(Math.floor(a/Math.pow(1024,c)*10)/10).toFixed(0===c?0:1)+" "+b[c]}function l(a,b){var c="";return null==b&&(b=w2utils.settings.groupSymbol||","),(w2utils.isFloat(a)||w2utils.isInt(a)||w2utils.isMoney(a))&&(E=String(a).split("."),c=String(E[0]).replace(/(\d)(?=(\d\d\d)+(?!\d))/g,"$1"+b),null!=E[1]&&(c+="."+E[1])),c}function m(a,b){w2utils.settings.shortmonths,w2utils.settings.fullmonths;if(b||(b=this.settings.date_format),""===a||null==a)return"";var c=new Date(a);if(w2utils.isInt(a)&&(c=new Date(Number(a))),"Invalid Date"===c)return"";var d=c.getFullYear(),e=c.getMonth(),f=c.getDate();return b.toLowerCase().replace("month",w2utils.settings.fullmonths[e]).replace("mon",w2utils.settings.shortmonths[e]).replace(/yyyy/g,d).replace(/yyy/g,d).replace(/yy/g,d>2e3?100+parseInt(String(d).substr(2)):String(d).substr(2)).replace(/(^|[^a-z$])y/g,"$1"+d).replace(/mm/g,(10>e+1?"0":"")+(e+1)).replace(/dd/g,(10>f?"0":"")+f).replace(/(^|[^a-z$])m/g,"$1"+(e+1)).replace(/(^|[^a-z$])d/g,"$1"+f)}function n(a,b){w2utils.settings.shortmonths,w2utils.settings.fullmonths;if(b||(b="h12"===this.settings.time_format?"hh:mi pm":"h24:mi"),""===a||null==a)return"";var c=new Date(a);if(w2utils.isInt(a)&&(c=new Date(Number(a))),w2utils.isTime(a)){var d=w2utils.isTime(a,!0);c=new Date,c.setHours(d.hours),c.setMinutes(d.minutes)}if("Invalid Date"===c)return"";var e="am",f=c.getHours(),g=c.getHours(),h=c.getMinutes(),i=c.getSeconds();return 10>h&&(h="0"+h),10>i&&(i="0"+i),(-1!==b.indexOf("am")||-1!==b.indexOf("pm"))&&(f>=12&&(e="pm"),f>12&&(f-=12)),b.toLowerCase().replace("am",e).replace("pm",e).replace("hh",f).replace("h24",g).replace("mm",h).replace("mi",h).replace("ss",i).replace(/(^|[^a-z$])h/g,"$1"+f).replace(/(^|[^a-z$])m/g,"$1"+h).replace(/(^|[^a-z$])s/g,"$1"+i)}function o(a,b){var c;return c="string"!=typeof b?[this.settings.date_format,this.settings.time_format]:b.split("|"),this.formatDate(a,c[0])+" "+this.formatTime(a,c[1])}function p(a){if(null===a)return a;switch(typeof a){case"number":break;case"string":a=$.trim(String(a).replace(/(<([^>]+)>)/gi,""));break;case"object":for(var b in a)a[b]=this.stripTags(a[b])}return a}function q(a){if(null===a)return a;switch(typeof a){case"number":break;case"string":a=String(a).replace(/&/g,"&amp;").replace(/>/g,"&gt;").replace(/</g,"&lt;").replace(/"/g,"&quot;");break;case"object":for(var b in a)a[b]=this.encodeTags(a[b])}return a}function r(a){return""===a||null==a?"":String(a).replace(/([;&,\.\+\*\~'`:"\!\^#$%@\[\]\(\)=<>\|\/? {}\\])/g,"\\$1")}function s(a){function b(a){for(var a=String(a).replace(/\r\n/g,"\n"),b="",c=0;c<a.length;c++){var d=a.charCodeAt(c);128>d?b+=String.fromCharCode(d):d>127&&2048>d?(b+=String.fromCharCode(d>>6|192),b+=String.fromCharCode(63&d|128)):(b+=String.fromCharCode(d>>12|224),b+=String.fromCharCode(d>>6&63|128),b+=String.fromCharCode(63&d|128))}return b}var c,d,e,f,g,h,i,j="",k=0,l="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";for(a=b(a);k<a.length;)c=a.charCodeAt(k++),d=a.charCodeAt(k++),e=a.charCodeAt(k++),f=c>>2,g=(3&c)<<4|d>>4,h=(15&d)<<2|e>>6,i=63&e,isNaN(d)?h=i=64:isNaN(e)&&(i=64),j=j+l.charAt(f)+l.charAt(g)+l.charAt(h)+l.charAt(i);return j}function t(a){function b(a){for(var b,c,d="",e=0,f=0;e<a.length;)f=a.charCodeAt(e),128>f?(d+=String.fromCharCode(f),e++):f>191&&224>f?(b=a.charCodeAt(e+1),d+=String.fromCharCode((31&f)<<6|63&b),e+=2):(b=a.charCodeAt(e+1),c=a.charCodeAt(e+2),d+=String.fromCharCode((15&f)<<12|(63&b)<<6|63&c),e+=3);return d}var c,d,e,f,g,h,i,j="",k=0,l="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";for(a=a.replace(/[^A-Za-z0-9\+\/\=]/g,"");k<a.length;)f=l.indexOf(a.charAt(k++)),g=l.indexOf(a.charAt(k++)),h=l.indexOf(a.charAt(k++)),i=l.indexOf(a.charAt(k++)),c=f<<2|g>>4,d=(15&g)<<4|h>>2,e=(3&h)<<6|i,j+=String.fromCharCode(c),64!==h&&(j+=String.fromCharCode(d)),64!==i&&(j+=String.fromCharCode(e));return j=b(j)}function u(a,b,c,d){function e(a,b,c){var d=!!window.webkitURL;return d||"undefined"==typeof c||(b=c),";"+a+": "+b+"; -webkit-"+a+": "+b+"; -moz-"+a+": "+b+"; -ms-"+a+": "+b+"; -o-"+a+": "+b+";"}var f=$(a).width(),g=$(a).height(),h=.5;if(!a||!b)return void console.log("ERROR: Cannot do transition when one of the divs is null");switch(a.parentNode.style.cssText+=e("perspective","700px")+"; overflow: hidden;",a.style.cssText+="; position: absolute; z-index: 1019; "+e("backface-visibility","hidden"),b.style.cssText+="; position: absolute; z-index: 1020; "+e("backface-visibility","hidden"),c){case"slide-left":a.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),b.style.cssText+="overflow: hidden; "+e("transform","translate3d("+f+"px, 0, 0)","translate("+f+"px, 0)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+";"+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),a.style.cssText+=e("transition",h+"s")+";"+e("transform","translate3d(-"+f+"px, 0, 0)","translate(-"+f+"px, 0)")},1);break;case"slide-right":a.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),b.style.cssText+="overflow: hidden; "+e("transform","translate3d(-"+f+"px, 0, 0)","translate(-"+f+"px, 0)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","translate3d(0px, 0, 0)","translate(0px, 0)"),a.style.cssText+=e("transition",h+"s")+"; "+e("transform","translate3d("+f+"px, 0, 0)","translate("+f+"px, 0)")},1);break;case"slide-down":a.style.cssText+="overflow: hidden; z-index: 1; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),b.style.cssText+="overflow: hidden; z-index: 0; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),a.style.cssText+=e("transition",h+"s")+"; "+e("transform","translate3d(0, "+g+"px, 0)","translate(0, "+g+"px)")},1);break;case"slide-up":a.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),b.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, "+g+"px, 0)","translate(0, "+g+"px)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),a.style.cssText+=e("transition",h+"s")+"; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)")},1);break;case"flip-left":a.style.cssText+="overflow: hidden; "+e("transform","rotateY(0deg)"),b.style.cssText+="overflow: hidden; "+e("transform","rotateY(-180deg)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateY(0deg)"),a.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateY(180deg)")},1);break;case"flip-right":a.style.cssText+="overflow: hidden; "+e("transform","rotateY(0deg)"),b.style.cssText+="overflow: hidden; "+e("transform","rotateY(180deg)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateY(0deg)"),a.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateY(-180deg)")},1);break;case"flip-down":a.style.cssText+="overflow: hidden; "+e("transform","rotateX(0deg)"),b.style.cssText+="overflow: hidden; "+e("transform","rotateX(180deg)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateX(0deg)"),a.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateX(-180deg)")},1);break;case"flip-up":a.style.cssText+="overflow: hidden; "+e("transform","rotateX(0deg)"),b.style.cssText+="overflow: hidden; "+e("transform","rotateX(-180deg)"),$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateX(0deg)"),a.style.cssText+=e("transition",h+"s")+"; "+e("transform","rotateX(180deg)")},1);break;case"pop-in":a.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),b.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)")+"; "+e("transform","scale(.8)")+"; opacity: 0;",$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; "+e("transform","scale(1)")+"; opacity: 1;",a.style.cssText+=e("transition",h+"s")+";"},1);break;case"pop-out":a.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)")+"; "+e("transform","scale(1)")+"; opacity: 1;",b.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)")+"; opacity: 0;",$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; opacity: 1;",a.style.cssText+=e("transition",h+"s")+"; "+e("transform","scale(1.7)")+"; opacity: 0;"},1);break;default:a.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)"),b.style.cssText+="overflow: hidden; "+e("transform","translate3d(0, 0, 0)","translate(0, 0)")+"; opacity: 0;",$(b).show(),window.setTimeout(function(){b.style.cssText+=e("transition",h+"s")+"; opacity: 1;",a.style.cssText+=e("transition",h+"s")},1)}setTimeout(function(){"slide-down"===c&&($(a).css("z-index","1019"),$(b).css("z-index","1020")),b&&$(b).css({opacity:"1","-webkit-transition":"","-moz-transition":"","-ms-transition":"","-o-transition":"","-webkit-transform":"","-moz-transform":"","-ms-transform":"","-o-transform":"","-webkit-backface-visibility":"","-moz-backface-visibility":"","-ms-backface-visibility":"","-o-backface-visibility":""}),a&&($(a).css({opacity:"1","-webkit-transition":"","-moz-transition":"","-ms-transition":"","-o-transition":"","-webkit-transform":"","-moz-transform":"","-ms-transform":"","-o-transform":"","-webkit-backface-visibility":"","-moz-backface-visibility":"","-ms-backface-visibility":"","-o-backface-visibility":""}),a.parentNode&&$(a.parentNode).css({"-webkit-perspective":"","-moz-perspective":"","-ms-perspective":"","-o-perspective":""})),"function"==typeof d&&d()},1e3*h)}function v(a,b,c){var d={};"object"==typeof b?d=b:(d.msg=b,d.spinner=c),d.msg||0===d.msg||(d.msg=""),w2utils.unlock(a),$(a).prepend('<div class="w2ui-lock"></div><div class="w2ui-lock-msg"></div>');var e=$(a).find(".w2ui-lock"),f=$(a).find(".w2ui-lock-msg");d.msg||f.css({"background-color":"transparent",border:"0px"}),d.spinner===!0&&(d.msg='<div class="w2ui-spinner" '+(d.msg?"":'style="width: 35px; height: 35px"')+"></div>"+d.msg),null!=d.opacity&&e.css("opacity",d.opacity),"function"==typeof e.fadeIn?(e.fadeIn(200),f.html(d.msg).fadeIn(200)):(e.show(),f.html(d.msg).show(0)),$().w2tag()}function w(a){$(a).find(".w2ui-lock").remove(),$(a).find(".w2ui-lock-msg").remove()}function x(a,b){var c=$(a),d={left:parseInt(c.css("border-left-width"))||0,right:parseInt(c.css("border-right-width"))||0,top:parseInt(c.css("border-top-width"))||0,bottom:parseInt(c.css("border-bottom-width"))||0},e={left:parseInt(c.css("margin-left"))||0,right:parseInt(c.css("margin-right"))||0,top:parseInt(c.css("margin-top"))||0,bottom:parseInt(c.css("margin-bottom"))||0},f={left:parseInt(c.css("padding-left"))||0,right:parseInt(c.css("padding-right"))||0,top:parseInt(c.css("padding-top"))||0,bottom:parseInt(c.css("padding-bottom"))||0};switch(b){case"top":return d.top+e.top+f.top;case"bottom":return d.bottom+e.bottom+f.bottom;case"left":return d.left+e.left+f.left;case"right":return d.right+e.right+f.right;case"width":return d.left+d.right+e.left+e.right+f.left+f.right+parseInt(c.width());case"height":return d.top+d.bottom+e.top+e.bottom+f.top+f.bottom+parseInt(c.height());case"+width":return d.left+d.right+e.left+e.right+f.left+f.right;case"+height":return d.top+d.bottom+e.top+e.bottom+f.top+f.bottom}return 0}function y(a){var b=this.settings.phrases[a];return null==b?a:b}function z(a){a||(a="en-us"),5===a.length&&(a="locale/"+a+".json"),$.ajax({url:a,type:"GET",dataType:"JSON",async:!1,cache:!1,success:function(a){w2utils.settings=$.extend(!0,w2utils.settings,a);var b=w2obj.grid.prototype;for(var c in b.buttons)b.buttons[c].caption=w2utils.lang(b.buttons[c].caption),b.buttons[c].hint=w2utils.lang(b.buttons[c].hint);b.msgDelete=w2utils.lang(b.msgDelete),b.msgNotJSON=w2utils.lang(b.msgNotJSON),b.msgRefresh=w2utils.lang(b.msgRefresh)},error:function(){console.log("ERROR: Cannot load locale "+a)}})}function A(){if(E.scrollBarSize)return E.scrollBarSize;var a='<div id="_scrollbar_width" style="position: absolute; top: -300px; width: 100px; height: 100px; overflow-y: scroll;"> <div style="height: 120px">1</div></div>';return $("body").append(a),E.scrollBarSize=100-$("#_scrollbar_width > div").width(),$("#_scrollbar_width").remove(),String(navigator.userAgent).indexOf("MSIE")>=0&&(E.scrollBarSize=E.scrollBarSize/2),E.scrollBarSize}function B(a,b){return a&&"undefined"!=typeof a.name?"undefined"!=typeof w2ui[a.name]?(console.log('ERROR: The parameter "name" is not unique. There are other objects already created with the same name (obj: '+a.name+")."),!1):w2utils.isAlphaNumeric(a.name)?!0:(console.log('ERROR: The parameter "name" has to be alpha-numeric (a-z, 0-9, dash and underscore). '),!1):(console.log('ERROR: The parameter "name" is required but not supplied in $().'+b+"()."),!1)}function C(a,b,c,d){$.isArray(b)||(b=[b]);for(var e=0;e<b.length;e++)if(b[e].id===a)return console.log('ERROR: The parameter "id='+a+'" is not unique within the current '+c+". (obj: "+d+")"),!1;return!0}function D(a){var b=[],c=a.replace(/\/\(/g,"(?:/").replace(/\+/g,"__plus__").replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g,function(a,c,d,e,f,g){return b.push({name:e,optional:!!g}),c=c||"",""+(g?"":c)+"(?:"+(g?c:"")+(d||"")+(f||d&&"([^/.]+?)"||"([^/]+?)")+")"+(g||"")}).replace(/([\/.])/g,"\\$1").replace(/__plus__/g,"(.+)").replace(/\*/g,"(.*)");return{path:new RegExp("^"+c+"$","i"),keys:b}}var E={},F={version:"1.4.0",settings:{locale:"en-us",date_format:"m/d/yyyy",date_display:"Mon d, yyyy",time_format:"h12",currencyPrefix:"$",currencySuffix:"",currencyPrecision:2,groupSymbol:",",shortmonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],fullmonths:["January","February","March","April","May","June","July","August","September","October","November","December"],shortdays:["M","T","W","T","F","S","S"],fulldays:["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],dataType:"HTTP",phrases:{}},isInt:a,isFloat:b,isMoney:c,isHex:d,isAlphaNumeric:e,isEmail:f,isDate:g,isTime:h,age:i,date:j,size:k,formatNumber:l,formatDate:m,formatTime:n,formatDateTime:o,stripTags:p,encodeTags:q,escapeId:r,base64encode:s,base64decode:t,transition:u,lock:v,unlock:w,lang:y,locale:z,getSize:x,scrollBarSize:A,checkName:B,checkUniqueId:C,parseRoute:D,isIOS:-1!=navigator.userAgent.toLowerCase().indexOf("iphone")||-1!=navigator.userAgent.toLowerCase().indexOf("ipod")||-1!=navigator.userAgent.toLowerCase().indexOf("ipad")?!0:!1,isIE:-1!=navigator.userAgent.toLowerCase().indexOf("msie")||-1!=navigator.userAgent.toLowerCase().indexOf("trident")?!0:!1};return F}();w2utils.event={on:function(a,b){return $.isPlainObject(a)||(a={type:a}),a=$.extend({type:null,execute:"before",target:null,onComplete:null},a),a.type?b?($.isArray(this.handlers)||(this.handlers=[]),void this.handlers.push({event:a,handler:b})):void console.log("ERROR: You must specify event handler function when calling .on() method of "+this.name):void console.log("ERROR: You must specify event type when calling .on() method of "+this.name)},off:function(a,b){if($.isPlainObject(a)||(a={type:a}),a=$.extend({},{type:null,execute:"before",target:null,onComplete:null},a),!a.type)return void console.log("ERROR: You must specify event type when calling .off() method of "+this.name);b||(b=null);for(var c=[],d=0,e=this.handlers.length;e>d;d++){var f=this.handlers[d];(f.event.type!==a.type&&"*"!==a.type||f.event.target!==a.target&&null!==a.target||f.handler!==b&&null!==b)&&c.push(f)}this.handlers=c},trigger:function(a){var a=$.extend({type:null,phase:"before",target:null},a,{isStopped:!1,isCancelled:!1,preventDefault:function(){this.isCancelled=!0},stopPropagation:function(){this.isStopped=!0}});"before"===a.phase&&(a.onComplete=null);var b,c,d;null==a.target&&(a.target=null),$.isArray(this.handlers)||(this.handlers=[]);for(var e=this.handlers.length-1;e>=0;e--){var f=this.handlers[e];if(!(f.event.type!==a.type&&"*"!==f.event.type||f.event.target!==a.target&&null!==f.event.target||f.event.execute!==a.phase&&"*"!==f.event.execute&&"*"!==f.event.phase)&&(a=$.extend({},f.event,a),b=[],d=RegExp(/\((.*?)\)/).exec(f.handler),d&&(b=d[1].split(/\s*,\s*/)),2===b.length?f.handler.call(this,a.target,a):f.handler.call(this,a),a.isStopped===!0||a.stop===!0))return a}var g="on"+a.type.substr(0,1).toUpperCase()+a.type.substr(1);return"before"===a.phase&&"function"==typeof this[g]&&(c=this[g],b=[],d=RegExp(/\((.*?)\)/).exec(c),d&&(b=d[1].split(/\s*,\s*/)),2===b.length?c.call(this,a.target,a):c.call(this,a),a.isStopped===!0||a.stop===!0)?a:null!=a.object&&"before"===a.phase&&"function"==typeof a.object[g]&&(c=a.object[g],b=[],d=RegExp(/\((.*?)\)/).exec(c),d&&(b=d[1].split(/\s*,\s*/)),2===b.length?c.call(this,a.target,a):c.call(this,a),a.isStopped===!0||a.stop===!0)?a:("after"===a.phase&&"function"==typeof a.onComplete&&a.onComplete.call(this,a),a)}},w2utils.keyboard=function(a){function b(){$(document).on("keydown",c),$(document).on("mousedown",d)}function c(a){var b=a.target.tagName;-1===$.inArray(b,["INPUT","SELECT","TEXTAREA"])&&"true"!==$(a.target).prop("contenteditable")&&g&&w2ui[g]&&"function"==typeof w2ui[g].keydown&&w2ui[g].keydown.call(w2ui[g],a)}function d(a){var b=(a.target.tagName,$(a.target).parents(".w2ui-reset"));if(b.length>0){var c=b.attr("name");w2ui[c]&&w2ui[c].keyboard&&(g=c)}}function e(a){return"undefined"!=typeof a&&(g=a),g}function f(){g=null}var g=null;return a.active=e,a.clear=f,b(),a}({}),function(){$.fn.w2render=function(a){$(this).length>0&&("string"==typeof a&&w2ui[a]&&w2ui[a].render($(this)[0]),"object"==typeof a&&a.render($(this)[0]))},$.fn.w2destroy=function(a){!a&&this.length>0&&(a=this.attr("name")),"string"==typeof a&&w2ui[a]&&w2ui[a].destroy(),"object"==typeof a&&a.destroy()},$.fn.w2marker=function(a){return $(this).each(""===a||null==a?function(a,b){b.innerHTML=b.innerHTML.replace(/\<span class=\"w2ui\-marker\"\>(.*)\<\/span\>/gi,"$1")}:function(b,c){function d(a){return'<span class="w2ui-marker">'+a+"</span>"}"string"==typeof a&&(a=[a]),c.innerHTML=c.innerHTML.replace(/\<span class=\"w2ui\-marker\"\>(.*)\<\/span\>/gi,"$1");for(var e in a){var f=a[e];"string"!=typeof f&&(f=String(f)),f=f.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&").replace(/&/g,"&amp;").replace(/</g,"&gt;").replace(/>/g,"&lt;");var g=new RegExp(f+"(?!([^<]+)?>)","gi");c.innerHTML=c.innerHTML.replace(g,d)}})},$.fn.w2tag=function(a,b){return $.isPlainObject(b)||(b={}),$.isPlainObject(b.css)||(b.css={}),"undefined"==typeof b["class"]&&(b["class"]=""),0===$(this).length?void $(".w2ui-tag").each(function(a,b){var c=$(b).data("options");null==c&&(c={}),$($(b).data("taged-el")).removeClass(c["class"]),clearInterval($(b).data("timer")),$(b).remove()}):$(this).each(function(c,d){function e(){$tag=$("#w2ui-tag-"+g),$tag.length<=0||(clearInterval($tag.data("timer")),$tag.remove(),$(d).off("keypress",e).removeClass(b["class"]),$(d).length>0&&($(d)[0].style.cssText=i),"function"==typeof b.onHide&&b.onHide())}var f=d.id,g=w2utils.escapeId(d.id);if(""===a||null==a)$("#w2ui-tag-"+g).css("opacity",0),setTimeout(function(){clearInterval($("#w2ui-tag-"+g).data("timer")),$("#w2ui-tag-"+g).remove()},300);else{clearInterval($("#w2ui-tag-"+g).data("timer")),$("#w2ui-tag-"+g).remove(),$("body").append('<div id="w2ui-tag-'+f+'" class="w2ui-tag '+($(d).parents(".w2ui-popup").length>0?"w2ui-tag-popup":"")+'" style=""></div>');var h=setInterval(function(){return 0===$(d).length||0===$(d).offset().left&&0===$(d).offset().top?(clearInterval($("#w2ui-tag-"+g).data("timer")),void e()):void($("#w2ui-tag-"+g).data("position")!==$(d).offset().left+d.offsetWidth+"x"+$(d).offset().top&&$("#w2ui-tag-"+g).css({"-webkit-transition":".2s","-moz-transition":".2s","-ms-transition":".2s","-o-transition":".2s",left:$(d).offset().left+d.offsetWidth+"px",top:$(d).offset().top+"px"}).data("position",$(d).offset().left+d.offsetWidth+"x"+$(d).offset().top))},100);setTimeout(function(){$(d).offset()&&($("#w2ui-tag-"+g).css({opacity:"1",left:$(d).offset().left+d.offsetWidth+"px",top:$(d).offset().top+"px"}).html('<div style="margin-top: -2px 0px 0px -2px; white-space: nowrap;"> <div class="w2ui-tag-body">'+a+"</div> </div>").data("text",a).data("taged-el",d).data("options",b).data("position",$(d).offset().left+d.offsetWidth+"x"+$(d).offset().top).data("timer",h),$(d).off("keypress",e).on("keypress",e).off("change",e).on("change",e).css(b.css).addClass(b["class"]),"function"==typeof b.onShow&&b.onShow())},1);var i="";$(d).length>0&&(i=$(d)[0].style.cssText)}})},$.fn.w2overlay=function(a,b){function c(){var a=$("#w2ui-overlay"+g);if(a.data("element")===f[0]&&0!==a.length){var b=$(f).offset().left+"x"+$(f).offset().top;a.data("position")!==b?d():setTimeout(c,250)}}function d(){var a=$("#w2ui-overlay"+g);if(a.data("keepOpen")===!0)return void a.removeData("keepOpen");var c;"function"==typeof b.onHide&&(c=b.onHide()),c!==!1&&(a.remove(),$(document).off("click",d),clearInterval(a.data("timer")))}function e(){var a=$("#w2ui-overlay"+g),c=a.find(" > div");if(a.length>0){c.height("auto").width("auto");var d=!1,h=!1,i=c.height(),j=c.width();switch(b.width&&b.width<j&&(j=b.width),30>j&&(j=30),b.tmp.contentHeight&&(i=b.tmp.contentHeight,c.height(i),setTimeout(function(){c.height()>c.find("div.menu > table").height()&&c.find("div.menu").css("overflow-y","hidden")},1),setTimeout(function(){c.find("div.menu").css("overflow-y","auto")},10)),b.tmp.contentWidth&&(j=b.tmp.contentWidth,c.width(j),setTimeout(function(){c.width()>c.find("div.menu > table").width()&&c.find("div.menu").css("overflow-x","hidden")},1),setTimeout(function(){c.find("div.menu").css("overflow-y","auto")},10)),b.align){case"both":b.left=17,0===b.width&&(b.width=w2utils.getSize($(f),"width"));break;case"left":b.left=17;break;case"right":b.tipLeft=j-45,b.left=w2utils.getSize($(f),"width")-j+10}var k=(j-17)/2,l=b.left,m=b.width,n=b.tipLeft;m=30!==j||m?b.width?b.width:"auto":30,25>k&&(l=25-k,n=Math.floor(k)),a.css({top:f.offset().top+w2utils.getSize(f,"height")+b.top+7+"px",left:(f.offset().left>25?f.offset().left:25)+l+"px","min-width":m,"min-height":b.height?b.height:"auto"});var o=window.innerHeight+$(document).scrollTop()-c.offset().top-7,p=window.innerWidth+$(document).scrollLeft()-c.offset().left-7;o>-50&&210>o||b.openAbove===!0?(o=c.offset().top-$(document).scrollTop()-7,b.maxHeight&&o>b.maxHeight&&(o=b.maxHeight),i>o&&(h=!0,c.height(o).width(j).css({"overflow-y":"auto"}),i=o),a.css("top",$(f).offset().top-i-24+b.top+"px"),a.find(">style").html("#w2ui-overlay"+g+":before { display: none; margin-left: "+parseInt(n)+"px; }#w2ui-overlay"+g+":after { display: block; margin-left: "+parseInt(n)+"px; }")):(b.maxHeight&&o>b.maxHeight&&(o=b.maxHeight),i>o&&(h=!0,c.height(o).width(j).css({"overflow-y":"auto"})),a.find(">style").html("#w2ui-overlay"+g+":before { display: block; margin-left: "+parseInt(n)+"px; }#w2ui-overlay"+g+":after { display: none; margin-left: "+parseInt(n)+"px; }")),j=c.width(),p=window.innerWidth+$(document).scrollLeft()-c.offset().left-7,b.maxWidth&&p>b.maxWidth&&(p=b.maxWidth),j>p&&"both"!==b.align&&(b.align="right",setTimeout(function(){e()},1)),h&&d&&c.width(j+w2utils.scrollBarSize()+2)}}var f=this,g="",h={name:null,html:"",align:"none",left:0,top:0,tipLeft:30,width:0,height:0,maxWidth:null,maxHeight:null,style:"","class":"",onShow:null,onHide:null,openAbove:!1,tmp:{}};1==arguments.length&&(b="object"==typeof a?a:{html:a}),2==arguments.length&&(b.html=a),$.isPlainObject(b)||(b={}),b=$.extend({},h,b),b.name&&(g="-"+b.name);var i;if(0===this.length||""===b.html||null==b.html)return $("#w2ui-overlay"+g).length>0?(i=$("#w2ui-overlay"+g)[0].hide,"function"==typeof i&&i()):$("#w2ui-overlay"+g).remove(),$(this);$("#w2ui-overlay"+g).length>0&&(i=$("#w2ui-overlay"+g)[0].hide,$(document).off("click",i),"function"==typeof i&&i()),$("body").append('<div id="w2ui-overlay'+g+'" style="display: none" class="w2ui-reset w2ui-overlay '+($(this).parents(".w2ui-popup, .w2ui-overlay-popup").length>0?"w2ui-overlay-popup":"")+'"> <style></style> <div style="'+b.style+'" class="'+b["class"]+'"></div></div>');var j=$("#w2ui-overlay"+g),k=j.find(" > div");k.html(b.html);var l=k.css("background-color");return null!=l&&"rgba(0, 0, 0, 0)"!==l&&"transparent"!==l&&j.css("background-color",l),j.data("element",f.length>0?f[0]:null).data("options",b).data("position",$(f).offset().left+"x"+$(f).offset().top).fadeIn("fast").on("mousedown",function(a){$("#w2ui-overlay"+g).data("keepOpen",!0),-1===["INPUT","TEXTAREA","SELECT"].indexOf(a.target.tagName)&&a.preventDefault()}),j[0].hide=d,j[0].resize=e,e(),setTimeout(function(){e(),$(document).off("click",d).on("click",d),"function"==typeof b.onShow&&b.onShow()},10),c(),$(this)},$.fn.w2menu=function(a,b){function c(){setTimeout(function(){$("#w2ui-overlay"+h+" tr.w2ui-selected").removeClass("w2ui-selected");var a=$("#w2ui-overlay"+h+" tr[index="+b.index+"]"),c=$("#w2ui-overlay"+h+" div.menu").scrollTop();if(a.addClass("w2ui-selected"),b.tmp&&(b.tmp.contentHeight=$("#w2ui-overlay"+h+" table").height()+(b.search?50:10)),b.tmp&&(b.tmp.contentWidth=$("#w2ui-overlay"+h+" table").width()),$("#w2ui-overlay"+h).length>0&&$("#w2ui-overlay"+h)[0].resize(),a.length>0){var d=a[0].offsetTop-5,e=$("#w2ui-overlay"+h+" div.menu"),f=e.height();$("#w2ui-overlay"+h+" div.menu").scrollTop(c),(c>d||d+a.height()>c+f)&&$("#w2ui-overlay"+h+" div.menu").animate({scrollTop:d-(f-2*a.height())/2},200,"linear")}},1)}function d(a){var d=this.value,e=a.keyCode,f=!1;switch(e){case 13:$("#w2ui-overlay"+h).remove(),$.fn.w2menuHandler(a,b.index);break;case 9:case 27:$("#w2ui-overlay"+h).remove(),$.fn.w2menuHandler(a,-1);break;case 38:for(b.index=w2utils.isInt(b.index)?parseInt(b.index):0,b.index--;b.index>0&&b.items[b.index].hidden;)b.index--;if(0===b.index&&b.items[b.index].hidden)for(;b.items[b.index]&&b.items[b.index].hidden;)b.index++;b.index<0&&(b.index=0),f=!0;break;case 40:for(b.index=w2utils.isInt(b.index)?parseInt(b.index):0,b.index++;b.index<b.items.length-1&&b.items[b.index].hidden;)b.index++;if(b.index===b.items.length-1&&b.items[b.index].hidden)for(;b.items[b.index]&&b.items[b.index].hidden;)b.index--;b.index>=b.items.length&&(b.index=b.items.length-1),f=!0}if(!f){var i=0;for(var j in b.items){var k=b.items[j],l="",m="";-1!==["is","begins with"].indexOf(b.match)&&(l="^"),-1!==["is","ends with"].indexOf(b.match)&&(m="$");try{var n=new RegExp(l+d+m,"i");k.hidden=n.test(k.text)||"..."===k.text?!1:!0}catch(o){}"enum"===g.type&&-1!==$.inArray(k.id,ids)&&(k.hidden=!0),k.hidden!==!0&&i++}for(b.index=0;b.index<b.items.length-1&&b.items[b.index].hidden;)b.index++;0>=i&&(b.index=-1)}$(g).w2menu("refresh",b),c()}function e(){if(b.spinner)return'<table class="w2ui-drop-menu"><tr><td style="padding: 5px 10px 10px 10px; text-align: center"> <div class="w2ui-spinner" style="width: 18px; height: 18px; position: relative; top: 5px;"></div> <div style="display: inline-block; padding: 3px; color: #999;"> Loading...</div></td></tr></table>';for(var a=0,c='<table cellspacing="0" cellpadding="0" class="w2ui-drop-menu">',d=null,e=null,f=0;f<b.items.length;f++){var g=b.items[f];
+if("string"==typeof g?g={id:g,text:g}:(null!=g.text&&null==g.id&&(g.id=g.text),null==g.text&&null!=g.id&&(g.text=g.id),null!=g.caption&&(g.text=g.caption),d=g.img,e=g.icon,null==d&&(d=null),null==e&&(e=null)),g.hidden!==!0){var i="",j=g.text;if("function"==typeof b.render&&(j=b.render(g,b)),d&&(i='<td class="menu-icon"><div class="w2ui-tb-image w2ui-icon '+d+'"></div></td>'),e&&(i='<td class="menu-icon" align="center"><span class="w2ui-icon '+e+'"></span></td>'),"undefined"==typeof j||""===j||/^-+$/.test(j))c+='<tr><td colspan="2" style="padding: 6px; pointer-events: none"><div style="border-top: 1px solid silver;"></div></td></tr>';else{var k=a%2===0?"w2ui-item-even":"w2ui-item-odd";b.altRows!==!0&&(k="");var l=1;""==i&&l++,null==g.count&&l++,c+='<tr index="'+f+'" style="'+(g.style?g.style:"")+'" class="'+k+" "+(b.index===f?"w2ui-selected":"")+" "+(g.disabled===!0?"w2ui-disabled":"")+"\" onmousedown=\"$(this).parent().find('tr').removeClass('w2ui-selected'); $(this).addClass('w2ui-selected');\" onclick=\"event.stopPropagation(); if ("+(g.disabled===!0?"true":"false")+") return; $('#w2ui-overlay"+h+"').remove(); $.fn.w2menuHandler(event, '"+f+"');\">"+i+' <td class="menu-text" colspan="'+l+'">'+j+'</td> <td class="menu-count">'+(null!=g.count?"<span>"+g.count+"</span>":"")+"</td></tr>",a++}}b.items[f]=g}return 0===a&&(c+='<tr><td style="padding: 13px; color: #999; text-align: center">'+b.msgNoItems+"</div></td></tr>"),c+="</table>"}var f={index:null,items:[],render:null,msgNoItems:"No items",onSelect:null,tmp:{}},g=this,h="";if("refresh"!==a){1===arguments.length?b=a:b.items=a,"object"!=typeof b&&(b={}),b=$.extend({},f,b),$.fn.w2menuOptions=b,b.name&&(h="-"+b.name),"function"==typeof b.select&&"function"!=typeof b.onSelect&&(b.onSelect=b.select),"function"==typeof b.onRender&&"function"!=typeof b.render&&(b.render=b.onRender),$.fn.w2menuHandler=function(a,c){"function"==typeof b.onSelect&&setTimeout(function(){b.onSelect({index:c,item:b.items[c],originalEvent:a})},10),setTimeout(function(){$(document).click()},50)};var i="";if(b.search){i+='<div style="position: absolute; top: 0px; height: 40px; left: 0px; right: 0px; border-bottom: 1px solid silver; background-color: #ECECEC; padding: 8px 5px;"> <div class="w2ui-icon icon-search" style="position: absolute; margin-top: 4px; margin-left: 6px; width: 11px; background-position: left !important;"></div> <input id="menu-search" type="text" style="width: 100%; outline: none; padding-left: 20px;" onclick="event.stopPropagation();"></div>',b.style+=";background-color: #ECECEC",b.index=0;for(var j in b.items)b.items[j].hidden=!1}i+='<div class="menu" style="position: absolute; top: '+(b.search?40:0)+'px; bottom: 0px; width: 100%; overflow: auto;">'+e()+"</div>";var k=$(this).w2overlay(i,b);return setTimeout(function(){if($("#w2ui-overlay"+h+" #menu-search").on("keyup",d).on("keydown",function(a){9===a.keyCode&&(a.stopPropagation(),a.preventDefault())}),b.search){if(-1!=["text","password"].indexOf($(g)[0].type)||"texarea"==$(g)[0].tagName)return;$("#w2ui-overlay"+h+" #menu-search").focus()}},200),c(),k}if($("#w2ui-overlay"+h).length>0){b=$.extend($.fn.w2menuOptions,b);var l=$("#w2ui-overlay"+h+" div.menu").scrollTop();$("#w2ui-overlay"+h+" div.menu").html(e()),$("#w2ui-overlay"+h+" div.menu").scrollTop(l),c()}else $(this).w2menu(b)}}(),function(){var w2grid=function(a){this.name=null,this.box=null,this.header="",this.url="",this.routeData={},this.columns=[],this.columnGroups=[],this.records=[],this.summary=[],this.searches=[],this.searchData=[],this.sortData=[],this.postData={},this.toolbar={},this.show={header:!1,toolbar:!1,footer:!1,columnHeaders:!0,lineNumbers:!1,expandColumn:!1,selectColumn:!1,emptyRecords:!0,toolbarReload:!0,toolbarColumns:!0,toolbarSearch:!0,toolbarAdd:!1,toolbarEdit:!1,toolbarDelete:!1,toolbarSave:!1,selectionBorder:!0,recordTitles:!0,skipRecords:!0},this.autoLoad=!0,this.fixedBody=!0,this.recordHeight=24,this.keyboard=!0,this.selectType="row",this.multiSearch=!0,this.multiSelect=!0,this.multiSort=!0,this.reorderColumns=!1,this.reorderRows=!1,this.markSearch=!0,this.total=0,this.limit=100,this.offset=0,this.style="",this.ranges=[],this.menu=[],this.method=null,this.recid=null,this.parser=null,this.onAdd=null,this.onEdit=null,this.onRequest=null,this.onLoad=null,this.onDelete=null,this.onDeleted=null,this.onSubmit=null,this.onSave=null,this.onSelect=null,this.onUnselect=null,this.onClick=null,this.onDblClick=null,this.onContextMenu=null,this.onMenuClick=null,this.onColumnClick=null,this.onColumnResize=null,this.onSort=null,this.onSearch=null,this.onChange=null,this.onRestore=null,this.onExpand=null,this.onCollapse=null,this.onError=null,this.onKeydown=null,this.onToolbar=null,this.onColumnOnOff=null,this.onCopy=null,this.onPaste=null,this.onSelectionExtend=null,this.onEditField=null,this.onRender=null,this.onRefresh=null,this.onReload=null,this.onResize=null,this.onDestroy=null,this.onStateSave=null,this.onStateRestore=null,this.last={field:"all",caption:w2utils.lang("All Fields"),logic:"OR",search:"",searchIds:[],selection:{indexes:[],columns:{}},multi:!1,scrollTop:0,scrollLeft:0,sortData:null,sortCount:0,xhr:null,range_start:null,range_end:null,sel_ind:null,sel_col:null,sel_type:null,edit_col:null},$.extend(!0,this,w2obj.grid,a)};$.fn.w2grid=function(a){if("object"==typeof a||!a){if(!w2utils.checkName(a,"w2grid"))return;var b=a.columns,c=a.columnGroups,d=a.records,e=a.searches,f=a.searchData,g=a.sortData,h=a.postData,i=a.toolbar,j=new w2grid(a);$.extend(j,{postData:{},records:[],columns:[],searches:[],toolbar:{},sortData:[],searchData:[],handlers:[]}),null!=j.onExpand&&(j.show.expandColumn=!0),$.extend(!0,j.toolbar,i);for(var k in b)j.columns[k]=$.extend(!0,{},b[k]);for(var k in c)j.columnGroups[k]=$.extend(!0,{},c[k]);for(var k in e)j.searches[k]=$.extend(!0,{},e[k]);for(var k in f)j.searchData[k]=$.extend(!0,{},f[k]);for(var k in g)j.sortData[k]=$.extend(!0,{},g[k]);j.postData=$.extend(!0,{},h);for(var l in d){if(null==d[l].recid||"undefined"==typeof d[l].recid)return void console.log("ERROR: Cannot add records without recid. (obj: "+j.name+")");j.records[l]=$.extend(!0,{},d[l])}for(var m in j.columns){var n=j.columns[m];if("undefined"!=typeof n.searchable&&null==j.getSearch(n.field)){var o=n.searchable,p="";n.searchable===!0&&(o="text",p='size="20"'),j.addSearch({field:n.field,caption:n.caption,type:o,attr:p})}}return j.initToolbar(),0!==$(this).length&&j.render($(this)[0]),w2ui[j.name]=j,j}if(w2ui[$(this).attr("name")]){var q=w2ui[$(this).attr("name")];return q[a].apply(q,Array.prototype.slice.call(arguments,1)),this}console.log("ERROR: Method "+a+" does not exist on jQuery.w2grid")},w2grid.prototype={msgDelete:"Are you sure you want to delete selected records?",msgNotJSON:"Returned data is not in valid JSON format.",msgAJAXerror:"AJAX error. See console for more details.",msgRefresh:"Refreshing...",buttons:{reload:{type:"button",id:"w2ui-reload",icon:"w2ui-icon-reload",hint:"Reload data in the list"},columns:{type:"drop",id:"w2ui-column-on-off",icon:"w2ui-icon-columns",hint:"Show/hide columns",arrow:!1,html:""},search:{type:"html",id:"w2ui-search",html:'<div class="w2ui-icon icon-search-down w2ui-search-down" title="Select Search Field" onclick="var obj = w2ui[$(this).parents(\'div.w2ui-grid\').attr(\'name\')]; obj.searchShowFields();"></div>'},"search-go":{type:"check",id:"w2ui-search-advanced",caption:"Search...",hint:"Open Search Fields"},add:{type:"button",id:"w2ui-add",caption:"Add New",hint:"Add new record",icon:"w2ui-icon-plus"},edit:{type:"button",id:"w2ui-edit",caption:"Edit",hint:"Edit selected record",icon:"w2ui-icon-pencil",disabled:!0},"delete":{type:"button",id:"w2ui-delete",caption:"Delete",hint:"Delete selected records",icon:"w2ui-icon-cross",disabled:!0},save:{type:"button",id:"w2ui-save",caption:"Save",hint:"Save changed records",icon:"w2ui-icon-check"}},add:function(a){$.isArray(a)||(a=[a]);var b=0;for(var c in a)this.recid||"undefined"!=typeof a[c].recid||(a[c].recid=a[c][this.recid]),null!=a[c].recid&&"undefined"!=typeof a[c].recid?(this.records.push(a[c]),b++):console.log("ERROR: Cannot add record without recid. (obj: "+this.name+")");var d="object"!=typeof this.url?this.url:this.url.get;return d||(this.total=this.records.length,this.localSort(),this.localSearch()),this.refresh(),b},find:function(a,b){("undefined"==typeof a||null==a)&&(a={});var c=[],d=!1;for(var e in a)-1!=String(e).indexOf(".")&&(d=!0);for(var f=0;f<this.records.length;f++){var g=!0;for(var e in a){var h=this.records[f][e];d&&-1!=String(e).indexOf(".")&&(h=this.parseField(this.records[f],e)),a[e]!=h&&(g=!1)}g&&b!==!0&&c.push(this.records[f].recid),g&&b===!0&&c.push(f)}return c},set:function(a,b,c){if("object"==typeof a&&(c=b,b=a,a=null),null==a){for(var d in this.records)$.extend(!0,this.records[d],b);c!==!0&&this.refresh()}else{var e=this.get(a,!0);if(null==e)return!1;$.extend(!0,this.records[e],b),c!==!0&&this.refreshRow(a)}return!0},get:function(a,b){for(var c=0;c<this.records.length;c++)if(this.records[c].recid==a)return b===!0?c:this.records[c];return null},remove:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.records.length-1;c>=0;c--)this.records[c].recid==arguments[b]&&(this.records.splice(c,1),a++);var d="object"!=typeof this.url?this.url:this.url.get;return d||(this.localSort(),this.localSearch()),this.refresh(),a},addColumn:function(a,b){var c=0;1==arguments.length?(b=a,a=this.columns.length):("string"==typeof a&&(a=this.getColumn(a,!0)),null===a&&(a=this.columns.length)),$.isArray(b)||(b=[b]);for(var d in b)this.columns.splice(a,0,b[d]),a++,c++;return this.refresh(),c},removeColumn:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.columns.length-1;c>=0;c--)this.columns[c].field==arguments[b]&&(this.columns.splice(c,1),a++);return this.refresh(),a},getColumn:function(a,b){for(var c=0;c<this.columns.length;c++)if(this.columns[c].field==a)return b===!0?c:this.columns[c];return null},toggleColumn:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.columns.length-1;c>=0;c--){var d=this.columns[c];d.field==arguments[b]&&(d.hidden=!d.hidden,a++)}return this.refresh(),a},showColumn:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.columns.length-1;c>=0;c--){var d=this.columns[c];d.gridMinWidth&&delete d.gridMinWidth,d.field==arguments[b]&&d.hidden!==!1&&(d.hidden=!1,a++)}return this.refresh(),a},hideColumn:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.columns.length-1;c>=0;c--){var d=this.columns[c];d.field==arguments[b]&&d.hidden!==!0&&(d.hidden=!0,a++)}return this.refresh(),a},addSearch:function(a,b){var c=0;1==arguments.length?(b=a,a=this.searches.length):("string"==typeof a&&(a=this.getSearch(a,!0)),null===a&&(a=this.searches.length)),$.isArray(b)||(b=[b]);for(var d in b)this.searches.splice(a,0,b[d]),a++,c++;return this.searchClose(),c},removeSearch:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.searches.length-1;c>=0;c--)this.searches[c].field==arguments[b]&&(this.searches.splice(c,1),a++);return this.searchClose(),a},getSearch:function(a,b){for(var c=0;c<this.searches.length;c++)if(this.searches[c].field==a)return b===!0?c:this.searches[c];return null},toggleSearch:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.searches.length-1;c>=0;c--)this.searches[c].field==arguments[b]&&(this.searches[c].hidden=!this.searches[c].hidden,a++);return this.searchClose(),a},showSearch:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.searches.length-1;c>=0;c--)this.searches[c].field==arguments[b]&&this.searches[c].hidden!==!1&&(this.searches[c].hidden=!1,a++);return this.searchClose(),a},hideSearch:function(){for(var a=0,b=0;b<arguments.length;b++)for(var c=this.searches.length-1;c>=0;c--)this.searches[c].field==arguments[b]&&this.searches[c].hidden!==!0&&(this.searches[c].hidden=!0,a++);return this.searchClose(),a},getSearchData:function(a){for(var b in this.searchData)if(this.searchData[b].field==a)return this.searchData[b];return null},localSort:function(a){var b="object"!=typeof this.url?this.url:this.url.get;if(b)return void console.log("ERROR: grid.localSort can only be used on local data source, grid.url should be empty.");if(!$.isEmptyObject(this.sortData)){var c=(new Date).getTime(),d=this;d.prepareData();for(var e in this.sortData){var f=this.getColumn(this.sortData[e].field);if(!f)return;f.render&&-1!=["date","age"].indexOf(f.render.split(":")[0])&&(this.sortData[e].field_=f.field+"_"),f.render&&-1!=["time"].indexOf(f.render.split(":")[0])&&(this.sortData[e].field_=f.field+"_")}return this.records.sort(function(a,b){var c=0;for(var e in d.sortData){var f=d.sortData[e].field;d.sortData[e].field_&&(f=d.sortData[e].field_);var g=a[f],h=b[f];if(-1!=String(f).indexOf(".")&&(g=d.parseField(a,f),h=d.parseField(b,f)),"string"==typeof g&&(g=$.trim(g.toLowerCase())),"string"==typeof h&&(h=$.trim(h.toLowerCase())),g>h&&(c="asc"==d.sortData[e].direction?1:-1),h>g&&(c="asc"==d.sortData[e].direction?-1:1),"object"!=typeof g&&"object"==typeof h&&(c=-1),"object"!=typeof h&&"object"==typeof g&&(c=1),null==g&&null!=h&&(c=1),null!=g&&null==h&&(c=-1),0!=c)break}return c}),c=(new Date).getTime()-c,a!==!0&&setTimeout(function(){d.status("Sorting took "+c/1e3+" sec")},10),c}},localSearch:function(a){var b="object"!=typeof this.url?this.url:this.url.get;if(b)return void console.log("ERROR: grid.localSearch can only be used on local data source, grid.url should be empty.");var c=(new Date).getTime(),d=this;if(this.total=this.records.length,this.last.searchIds=[],this.prepareData(),this.searchData.length>0&&!b){this.total=0;for(var e in this.records){var f=this.records[e],g=0;for(var h in this.searchData){var i=this.searchData[h],j=this.getSearch(i.field);if(null!=i){null==j&&(j={field:i.field,type:i.type});var k=String(d.parseField(f,j.field)).toLowerCase();if("undefined"!=typeof i.value)if($.isArray(i.value))var l=i.value[0],m=i.value[1];else var l=String(i.value).toLowerCase();switch(i.operator){case"is":if(f[j.field]==i.value&&g++,"date"==j.type){var k=w2utils.formatDate(f[j.field+"_"],"yyyy-mm-dd"),l=w2utils.formatDate(l,"yyyy-mm-dd");k==l&&g++}if("time"==j.type){var k=w2utils.formatTime(f[j.field+"_"],"h24:mi"),l=w2utils.formatTime(l,"h24:mi");k==l&&g++}break;case"between":if(-1!=["int","float","money","currency","percent"].indexOf(j.type)&&parseFloat(f[j.field])>=parseFloat(l)&&parseFloat(f[j.field])<=parseFloat(m)&&g++,"date"==j.type){var k=f[j.field+"_"],l=w2utils.isDate(l,w2utils.settings.date_format,!0),m=w2utils.isDate(m,w2utils.settings.date_format,!0);null!=m&&(m=new Date(m.getTime()+864e5)),k>=l&&m>k&&g++}if("time"==j.type){var k=f[j.field+"_"],l=w2utils.isTime(l,!0),m=w2utils.isTime(m,!0);l=(new Date).setHours(l.hours,l.minutes,l.seconds?l.seconds:0,0),m=(new Date).setHours(m.hours,m.minutes,m.seconds?m.seconds:0,0),k>=l&&m>k&&g++}break;case"in":var n=i.value;i.svalue&&(n=i.svalue),-1!==n.indexOf(k)&&g++;break;case"not in":var n=i.value;i.svalue&&(n=i.svalue),-1==n.indexOf(k)&&g++;break;case"begins":case"begins with":0==k.indexOf(l)&&g++;break;case"contains":k.indexOf(l)>=0&&g++;break;case"ends":case"ends with":k.indexOf(l)==k.length-l.length&&g++}}}("OR"==this.last.logic&&0!=g||"AND"==this.last.logic&&g==this.searchData.length)&&this.last.searchIds.push(parseInt(e))}this.total=this.last.searchIds.length}return c=(new Date).getTime()-c,a!==!0&&setTimeout(function(){d.status("Search took "+c/1e3+" sec")},10),c},getRangeData:function(a,b){var c=this.get(a[0].recid,!0),d=this.get(a[1].recid,!0),e=a[0].column,f=a[1].column,g=[];if(e==f)for(var h=c;d>=h;h++){var i=this.records[h],j=i[this.columns[e].field]||null;g.push(b!==!0?j:{data:j,column:e,index:h,record:i})}else if(c==d)for(var i=this.records[c],k=e;f>=k;k++){var j=i[this.columns[k].field]||null;g.push(b!==!0?j:{data:j,column:k,index:c,record:i})}else for(var h=c;d>=h;h++){var i=this.records[h];g.push([]);for(var k=e;f>=k;k++){var j=i[this.columns[k].field];g[g.length-1].push(b!==!0?j:{data:j,column:k,index:h,record:i})}}return g},addRange:function(a){var b=0;if("row"==this.selectType)return b;$.isArray(a)||(a=[a]);for(var c in a){if("object"!=typeof a[c]&&(a[c]={name:"selection"}),"selection"==a[c].name){if(this.show.selectionBorder===!1)continue;var d=this.getSelection();if(0==d.length){this.removeRange(a[c].name);continue}{var e=d[0],f=d[d.length-1];$("#grid_"+this.name+"_rec_"+e.recid+" td[col="+e.column+"]"),$("#grid_"+this.name+"_rec_"+f.recid+" td[col="+f.column+"]")}}else{var e=a[c].range[0],f=a[c].range[1];$("#grid_"+this.name+"_rec_"+e.recid+" td[col="+e.column+"]"),$("#grid_"+this.name+"_rec_"+f.recid+" td[col="+f.column+"]")}if(e){var g={name:a[c].name,range:[{recid:e.recid,column:e.column},{recid:f.recid,column:f.column}],style:a[c].style||""},h=!1;for(var i in this.ranges)if(this.ranges[i].name==a[c].name){h=c;break}h!==!1?this.ranges[h]=g:this.ranges.push(g),b++}}return this.refreshRanges(),b},removeRange:function(){for(var a=0,b=0;b<arguments.length;b++){var c=arguments[b];$("#grid_"+this.name+"_"+c).remove();for(var d=this.ranges.length-1;d>=0;d--)this.ranges[d].name==c&&(this.ranges.splice(d,1),a++)}return a},refreshRanges:function(){function a(a){var e=d.getSelection();d.last.move={type:"expand",x:a.screenX,y:a.screenY,divX:0,divY:0,recid:e[0].recid,column:e[0].column,originalRange:[{recid:e[0].recid,column:e[0].column},{recid:e[e.length-1].recid,column:e[e.length-1].column}],newRange:[{recid:e[0].recid,column:e[0].column},{recid:e[e.length-1].recid,column:e[e.length-1].column}]},$(document).off("mousemove",b).on("mousemove",b),$(document).off("mouseup",c).on("mouseup",c)}function b(a){var b=d.last.move;if(b&&"expand"==b.type){b.divX=a.screenX-b.x,b.divY=a.screenY-b.y;var c,e,f=a.originalEvent.target;if("TD"!=f.tagName&&(f=$(f).parents("td")[0]),"undefined"!=typeof $(f).attr("col")&&(e=parseInt($(f).attr("col"))),f=$(f).parents("tr")[0],c=$(f).attr("recid"),b.newRange[1].recid!=c||b.newRange[1].column!=e){var g=$.extend({},b.newRange);return b.newRange=[{recid:b.recid,column:b.column},{recid:c,column:e}],m=d.trigger($.extend(m,{originalRange:b.originalRange,newRange:b.newRange})),m.isCancelled===!0?(b.newRange=g,void(m.newRange=g)):(d.removeRange("grid-selection-expand"),void d.addRange({name:"grid-selection-expand",range:m.newRange,style:"background-color: rgba(100,100,100,0.1); border: 2px dotted rgba(100,100,100,0.5);"}))}}}function c(){d.removeRange("grid-selection-expand"),delete d.last.move,$(document).off("mousemove",b),$(document).off("mouseup",c),d.trigger($.extend(m,{phase:"after"}))}var d=this,e=(new Date).getTime(),f=$("#grid_"+this.name+"_records");for(var g in this.ranges){var h=this.ranges[g],i=h.range[0],j=h.range[1],k=$("#grid_"+this.name+"_rec_"+i.recid+" td[col="+i.column+"]"),l=$("#grid_"+this.name+"_rec_"+j.recid+" td[col="+j.column+"]");0==$("#grid_"+this.name+"_"+h.name).length?f.append('<div id="grid_'+this.name+"_"+h.name+'" class="w2ui-selection" style="'+h.style+'">'+("selection"==h.name?'<div id="grid_'+this.name+'_resizer" class="w2ui-selection-resizer"></div>':"")+"</div>"):$("#grid_"+this.name+"_"+h.name).attr("style",h.style),k.length>0&&l.length>0&&$("#grid_"+this.name+"_"+h.name).css({left:k.position().left-1+f.scrollLeft()+"px",top:k.position().top-1+f.scrollTop()+"px",width:l.position().left-k.position().left+l.width()+3+"px",height:l.position().top-k.position().top+l.height()+3+"px"})}$(this.box).find("#grid_"+this.name+"_resizer").off("mousedown").on("mousedown",a);var m={phase:"before",type:"selectionExtend",target:d.name,originalRange:null,newRange:null};return(new Date).getTime()-e},select:function(){var a=0,b=this.last.selection;this.multiSelect||this.selectNone();for(var c=0;c<arguments.length;c++){var d="object"==typeof arguments[c]?arguments[c].recid:arguments[c],e=this.get(d);if(null!=e){var f=this.get(d,!0),g=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(d));if("row"==this.selectType){if(b.indexes.indexOf(f)>=0)continue;var h=this.trigger({phase:"before",type:"select",target:this.name,recid:d,index:f});if(h.isCancelled===!0)continue;b.indexes.push(f),b.indexes.sort(function(a,b){return a-b}),g.addClass("w2ui-selected").data("selected","yes"),g.find(".w2ui-grid-select-check").prop("checked",!0),a++}else{var i=arguments[c].column;if(!w2utils.isInt(i)){var j=[];for(var k in this.columns)this.columns[k].hidden||j.push({recid:d,column:parseInt(k)});return this.multiSelect||(j=j.splice(0,1)),this.select.apply(this,j)}var l=b.columns[f]||[];if($.isArray(l)&&-1!=l.indexOf(i))continue;var h=this.trigger({phase:"before",type:"select",target:this.name,recid:d,index:f,column:i});if(h.isCancelled===!0)continue;-1==b.indexes.indexOf(f)&&(b.indexes.push(f),b.indexes.sort(function(a,b){return a-b})),l.push(i),l.sort(function(a,b){return a-b}),g.find(" > td[col="+i+"]").addClass("w2ui-selected"),a++,g.data("selected","yes"),g.find(".w2ui-grid-select-check").prop("checked",!0),b.columns[f]=l}this.trigger($.extend(h,{phase:"after"}))}}return b.indexes.length==this.records.length||0!==this.searchData.length&&b.indexes.length==this.last.searchIds.length?$("#grid_"+this.name+"_check_all").prop("checked",!0):$("#grid_"+this.name+"_check_all").prop("checked",!1),this.status(),this.addRange("selection"),a},unselect:function(){for(var a=0,b=this.last.selection,c=0;c<arguments.length;c++){var d="object"==typeof arguments[c]?arguments[c].recid:arguments[c],e=this.get(d);if(null!=e){var f=this.get(e.recid,!0),g=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(d));if("row"==this.selectType){if(-1==b.indexes.indexOf(f))continue;var h=this.trigger({phase:"before",type:"unselect",target:this.name,recid:d,index:f});if(h.isCancelled===!0)continue;b.indexes.splice(b.indexes.indexOf(f),1),g.removeClass("w2ui-selected").removeData("selected"),0!=g.length&&(g[0].style.cssText="height: "+this.recordHeight+"px; "+g.attr("custom_style")),g.find(".w2ui-grid-select-check").prop("checked",!1),a++}else{var i=arguments[c].column;if(!w2utils.isInt(i)){var j=[];for(var k in this.columns)this.columns[k].hidden||j.push({recid:d,column:parseInt(k)});return this.unselect.apply(this,j)}var l=b.columns[f];if(!$.isArray(l)||-1==l.indexOf(i))continue;var h=this.trigger({phase:"before",type:"unselect",target:this.name,recid:d,column:i});if(h.isCancelled===!0)continue;l.splice(l.indexOf(i),1),$("#grid_"+this.name+"_rec_"+w2utils.escapeId(d)+" > td[col="+i+"]").removeClass("w2ui-selected"),a++,0==l.length&&(delete b.columns[f],b.indexes.splice(b.indexes.indexOf(f),1),g.removeData("selected"),g.find(".w2ui-grid-select-check").prop("checked",!1))}this.trigger($.extend(h,{phase:"after"}))}}return b.indexes.length==this.records.length||0!==this.searchData.length&&b.indexes.length==this.last.searchIds.length?$("#grid_"+this.name+"_check_all").prop("checked",!0):$("#grid_"+this.name+"_check_all").prop("checked",!1),this.status(),this.addRange("selection"),a},selectAll:function(){if(this.multiSelect!==!1){var a=this.trigger({phase:"before",type:"select",target:this.name,all:!0});if(a.isCancelled!==!0){var b="object"!=typeof this.url?this.url:this.url.get,c=this.last.selection,d=[];for(var e in this.columns)d.push(parseInt(e));if(c.indexes=[],b||0===this.searchData.length){var f=this.records.length;0==this.searchData.length||this.url||(f=this.last.searchIds.length);for(var g=0;f>g;g++)c.indexes.push(g),"row"!=this.selectType&&(c.columns[g]=d.slice())}else for(var g=0;g<this.last.searchIds.length;g++)c.indexes.push(this.last.searchIds[g]),"row"!=this.selectType&&(c.columns[this.last.searchIds[g]]=d.slice());this.refresh();var c=this.getSelection();1==c.length?this.toolbar.enable("w2ui-edit"):this.toolbar.disable("w2ui-edit"),c.length>=1?this.toolbar.enable("w2ui-delete"):this.toolbar.disable("w2ui-delete"),this.addRange("selection"),this.trigger($.extend(a,{phase:"after"}))}}},selectNone:function(){var a=this.trigger({phase:"before",type:"unselect",target:this.name,all:!0});if(a.isCancelled!==!0){var b=this.last.selection;for(var c in b.indexes){var d=b.indexes[c],e=this.records[d],f=e?e.recid:null,g=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(f));if(g.removeClass("w2ui-selected").removeData("selected"),g.find(".w2ui-grid-select-check").prop("checked",!1),"row"!=this.selectType){var h=b.columns[d];for(var i in h)g.find(" > td[col="+h[i]+"]").removeClass("w2ui-selected")}}b.indexes=[],b.columns={},this.toolbar.disable("w2ui-edit","w2ui-delete"),this.removeRange("selection"),$("#grid_"+this.name+"_check_all").prop("checked",!1),this.trigger($.extend(a,{phase:"after"}))}},getSelection:function(a){var b=[],c=this.last.selection;if("row"==this.selectType){for(var d in c.indexes)this.records[c.indexes[d]]&&b.push(a===!0?c.indexes[d]:this.records[c.indexes[d]].recid);return b}for(var d in c.indexes){var e=c.columns[c.indexes[d]];if(this.records[c.indexes[d]])for(var f in e)b.push({recid:this.records[c.indexes[d]].recid,index:parseInt(c.indexes[d]),column:e[f]})}return b},search:function(a,b){var c="object"!=typeof this.url?this.url:this.url.get,d=[],e=this.last.multi,f=this.last.logic,g=this.last.field,h=this.last.search;if(0==arguments.length){h="";for(var i in this.searches){var j=this.searches[i],k=$("#grid_"+this.name+"_operator_"+i).val(),l=$("#grid_"+this.name+"_field_"+i),m=$("#grid_"+this.name+"_field2_"+i),n=l.val(),o=m.val(),p=null;if(-1!=["int","float","money","currency","percent"].indexOf(j.type)){var q=l.data("w2field"),r=m.data("w2field");q&&(n=q.clean(n)),r&&(o=r.clean(o))}if(-1!=["list","enum"].indexOf(j.type))if(n=l.data("selected")||{},$.isArray(n)){p=[];for(var s in n)p.push(w2utils.isFloat(n[s].id)?parseFloat(n[s].id):String(n[s].id).toLowerCase()),delete n[s].hidden}else n=n.id||"";if(""!=n&&null!=n||"undefined"!=typeof o&&""!=o){var t={field:j.field,type:j.type,operator:k};"between"==k?$.extend(t,{value:[n,o]}):"in"==k&&"string"==typeof n?$.extend(t,{value:n.split(",")}):"not in"==k&&"string"==typeof n?$.extend(t,{value:n.split(",")}):$.extend(t,{value:n}),p&&$.extend(t,{svalue:p});try{"date"==j.type&&"between"==k&&(t.value[0]=n,t.value[1]=o),"date"==j.type&&"is"==k&&(t.value=n)}catch(u){}d.push(t)}}d.length>0&&!c?(e=!0,f="AND"):(e=!0,f="AND")}if("string"==typeof a&&(g=a,h=b,e=!1,f="OR","undefined"!=typeof b))if("all"==a.toLowerCase())if(this.searches.length>0)for(var i in this.searches){var j=this.searches[i];if("text"==j.type||"alphanumeric"==j.type&&w2utils.isAlphaNumeric(b)||"int"==j.type&&w2utils.isInt(b)||"float"==j.type&&w2utils.isFloat(b)||"percent"==j.type&&w2utils.isFloat(b)||"hex"==j.type&&w2utils.isHex(b)||"currency"==j.type&&w2utils.isMoney(b)||"money"==j.type&&w2utils.isMoney(b)||"date"==j.type&&w2utils.isDate(b)){var t={field:j.field,type:j.type,operator:"text"==j.type?"contains":"is",value:b};d.push(t)}if(-1!=["int","float","money","currency","percent"].indexOf(j.type)&&-1!=String(b).indexOf("-")){var v=String(b).split("-"),t={field:j.field,type:j.type,operator:"between",value:[v[0],v[1]]};d.push(t)}}else for(var w in this.columns){var t={field:this.columns[w].field,type:"text",operator:"contains",value:b};d.push(t)}else{var x=$("#grid_"+this.name+"_search_all"),j=this.getSearch(a);if(null==j&&(j={field:a,type:"text"}),j.field==a&&(this.last.caption=j.caption),"list"==j.type){var t=x.data("selected");t&&!$.isEmptyObject(t)&&(b=t.id)}if(""!=b){var y="contains",z=b;if(-1!=["date","time","list"].indexOf(j.type)&&(y="is"),"int"==j.type&&""!=b){if(y="is",-1!=String(b).indexOf("-")){var t=b.split("-");2==t.length&&(y="between",z=[parseInt(t[0]),parseInt(t[1])])}if(-1!=String(b).indexOf(",")){var t=b.split(",");y="in",z=[];for(var v in t)z.push(t[v])}}var t={field:j.field,type:j.type,operator:y,value:z};d.push(t)}}if($.isArray(a)){var A="AND";"string"==typeof b&&(A=b.toUpperCase(),"OR"!=A&&"AND"!=A&&(A="AND")),h="",e=!0,f=A;for(var B in a){var C=a[B],j=this.getSearch(C.field);null==j&&(j={type:"text",operator:"contains"}),d.push($.extend(!0,{},j,C))}}var D=this.trigger({phase:"before",type:"search",target:this.name,searchData:d,searchField:a?a:"multi",searchValue:b?b:"multi"});D.isCancelled!==!0&&(this.searchData=D.searchData,this.last.field=g,this.last.search=h,this.last.multi=e,this.last.logic=f,this.last.scrollTop=0,this.last.scrollLeft=0,this.last.selection.indexes=[],this.last.selection.columns={},this.searchClose(),this.set({expanded:!1},!0),c?(this.last.xhr_offset=0,this.reload()):(this.localSearch(),this.refresh()),this.trigger($.extend(D,{phase:"after"})))},searchOpen:function(){if(this.box&&0!=this.searches.length){var a=this;$("#tb_"+this.name+"_toolbar_item_w2ui-search-advanced").w2overlay(this.getSearchesHTML(),{name:"searches-"+this.name,left:-10,"class":"w2ui-grid-searches",onShow:function(){"OR"==a.last.logic&&(a.searchData=[]),a.initSearches(),$("#w2ui-overlay-searches-"+this.name+" .w2ui-grid-searches").data("grid-name",a.name);var b=$("#w2ui-overlay-searches-"+this.name+" .w2ui-grid-searches *[rel=search]");b.length>0&&b[0].focus()}})}},searchClose:function(){this.box&&0!=this.searches.length&&(this.toolbar&&this.toolbar.uncheck("w2ui-search-advanced"),$("#w2ui-overlay-searches-"+this.name+" .w2ui-grid-searches").length>0&&$().w2overlay("",{name:"searches-"+this.name}))},searchShowFields:function(){for(var a=$("#grid_"+this.name+"_search_all"),b='<div class="w2ui-select-field"><table>',c=-1;c<this.searches.length;c++){var d=this.searches[c];if(-1==c){if(!this.multiSearch)continue;d={field:"all",caption:w2utils.lang("All Fields")}}else if(this.searches[c].hidden===!0)continue;b+="<tr "+(w2utils.isIOS?"onTouchStart":"onClick")+"=\"w2ui['"+this.name+"'].initAllField('"+d.field+'\')"> <td><input type="radio" tabIndex="-1" '+(d.field==this.last.field?"checked":"")+"></td> <td>"+d.caption+"</td></tr>"}b+="</table></div>",setTimeout(function(){$(a).w2overlay(b,{left:-10})},1)},initAllField:function(a,b){var c=$("#grid_"+this.name+"_search_all"),d=this.getSearch(a);if("all"==a)d={field:"all",caption:w2utils.lang("All Fields")},c.w2field("clear"),c.change().focus();else{var e=d.type;-1!=["enum","select"].indexOf(e)&&(e="list"),c.w2field(e,$.extend({},d.options,{suffix:"",autoFormat:!1,selected:b})),-1!=["list","enum"].indexOf(d.type)&&(this.last.search="",this.last.item="",c.val("")),setTimeout(function(){c.focus()},1)}""!=this.last.search?this.search(d.field,this.last.search):(this.last.field=d.field,this.last.caption=d.caption),c.attr("placeholder",d.caption),$().w2overlay()},searchReset:function(a){var b=this.trigger({phase:"before",type:"search",target:this.name,searchData:[]});b.isCancelled!==!0&&(this.searchData=[],this.last.search="",this.last.logic="OR",this.last.multi=!1,this.last.xhr_offset=0,this.last.scrollTop=0,this.last.scrollLeft=0,this.last.selection.indexes=[],this.last.selection.columns={},this.searchClose(),$("#grid_"+this.name+"_search_all").val(""),a||this.reload(),this.trigger($.extend(b,{phase:"after"})))},clear:function(a){this.records=[],this.summary=[],this.last.scrollTop=0,this.last.scrollLeft=0,this.last.range_start=null,this.last.range_end=null,a||this.refresh()},reset:function(a){this.offset=0,this.total=0,this.last.scrollTop=0,this.last.scrollLeft=0,this.last.selection.indexes=[],this.last.selection.columns={},this.last.range_start=null,this.last.range_end=null,this.last.xhr_offset=0,this.searchReset(a),null!=this.last.sortData&&(this.sortData=this.last.sortData),this.set({expanded:!1},!0),a||this.refresh()},skip:function(a){var b="object"!=typeof this.url?this.url:this.url.get;b?(this.offset=parseInt(a),this.offset>this.total&&(this.offset=this.total-this.limit),(this.offset<0||!w2utils.isInt(this.offset))&&(this.offset=0),this.records=[],this.last.xhr_offset=0,this.last.pull_more=!0,this.last.scrollTop=0,this.last.scrollLeft=0,$("#grid_"+this.name+"_records").prop("scrollTop",0),this.reload()):console.log("ERROR: grid.skip() can only be called when you have remote data source.")},load:function(a,b){return"undefined"==typeof a?void console.log('ERROR: You need to provide url argument when calling .load() method of "'+this.name+'" object.'):void this.request("get-records",{},a,b)
+},reload:function(a){var b="object"!=typeof this.url?this.url:this.url.get;b?(this.clear(!0),this.request("get-records",{},null,a)):(this.last.scrollTop=0,this.last.scrollLeft=0,this.last.range_start=null,this.last.range_end=null,this.localSearch(),this.refresh(),"function"==typeof a&&a({status:"success"}))},request:function(a,b,c,d){if("undefined"==typeof b&&(b={}),("undefined"==typeof c||""==c||null==c)&&(c=this.url),""!=c&&null!=c){var e={};if(w2utils.isInt(this.offset)||(this.offset=0),w2utils.isInt(this.last.xhr_offset)||(this.last.xhr_offset=0),e.cmd=a,e.selected=this.getSelection(),e.limit=this.limit,e.offset=parseInt(this.offset)+this.last.xhr_offset,e.search=this.searchData,e.searchLogic=this.last.logic,e.sort=this.sortData,0==this.searchData.length&&(delete e.search,delete e.searchLogic),0==this.sortData.length&&delete e.sort,$.extend(e,this.postData),$.extend(e,b),"get-records"==a){var f=this.trigger({phase:"before",type:"request",target:this.name,url:c,postData:e});if(f.isCancelled===!0)return void("function"==typeof d&&d({status:"error",message:"Request aborted."}))}else var f={url:c,postData:e};var g=this;if(0==this.last.xhr_offset)this.lock(this.msgRefresh,!0);else{var h=$("#grid_"+this.name+"_rec_more");this.autoLoad===!0?h.show().find("td").html('<div><div style="width: 20px; height: 20px;" class="w2ui-spinner"></div></div>'):h.find("td").html("<div>"+w2utils.lang("Load")+" "+g.limit+" "+w2utils.lang("More")+"...</div>")}if(this.last.xhr)try{this.last.xhr.abort()}catch(i){}var c="object"!=typeof f.url?f.url:f.url.get;if("save-records"==e.cmd&&"object"==typeof f.url&&(c=f.url.save),"delete-records"==e.cmd&&"object"==typeof f.url&&(c=f.url.remove),!$.isEmptyObject(g.routeData)){var j=w2utils.parseRoute(c);if(j.keys.length>0)for(var k=0;k<j.keys.length;k++)null!=g.routeData[j.keys[k].name]&&(c=c.replace(new RegExp(":"+j.keys[k].name,"g"),g.routeData[j.keys[k].name]))}var l={type:"POST",url:c,data:f.postData,dataType:"text"};"HTTP"==w2utils.settings.dataType&&(l.data="object"==typeof l.data?String($.param(l.data,!1)).replace(/%5B/g,"[").replace(/%5D/g,"]"):l.data),"RESTFULL"==w2utils.settings.dataType&&(l.type="GET","save-records"==e.cmd&&(l.type="PUT"),"delete-records"==e.cmd&&(l.type="DELETE"),l.data="object"==typeof l.data?String($.param(l.data,!1)).replace(/%5B/g,"[").replace(/%5D/g,"]"):l.data),"JSON"==w2utils.settings.dataType&&(l.type="POST",l.data=JSON.stringify(l.data),l.contentType="application/json"),this.method&&(l.type=this.method),this.last.xhr_cmd=e.cmd,this.last.xhr_start=(new Date).getTime(),this.last.xhr=$.ajax(l).done(function(b,c){g.requestComplete(c,a,d)}).fail(function(b,c,e){var f={status:c,error:e,rawResponseText:b.responseText},h=g.trigger({phase:"before",type:"error",error:f,xhr:b});if(h.isCancelled!==!0){if("abort"!=c){var i;try{i=$.parseJSON(b.responseText)}catch(j){}console.log("ERROR: Server communication failed.","\n EXPECTED:",{status:"success",total:5,records:[{recid:1,field:"value"}]},"\n OR:",{status:"error",message:"error message"},"\n RECEIVED:","object"==typeof i?i:b.responseText)}g.requestComplete("error",a,d),g.trigger($.extend(h,{phase:"after"}))}}),"get-records"==a&&this.trigger($.extend(f,{phase:"after"}))}},requestComplete:function(status,cmd,callBack){var obj=this;this.unlock(),setTimeout(function(){obj.status(w2utils.lang("Server Response")+" "+((new Date).getTime()-obj.last.xhr_start)/1e3+" "+w2utils.lang("sec"))},10),this.last.pull_more=!1,this.last.pull_refresh=!0;var event_name="load";"save-records"==this.last.xhr_cmd&&(event_name="save"),"delete-records"==this.last.xhr_cmd&&(event_name="deleted");var eventData=this.trigger({phase:"before",target:this.name,type:event_name,xhr:this.last.xhr,status:status});if(eventData.isCancelled===!0)return void("function"==typeof callBack&&callBack({status:"error",message:"Request aborted."}));var data,responseText=this.last.xhr.responseText;if("error"!=status){if("undefined"!=typeof responseText&&""!=responseText){if("object"==typeof responseText)data=responseText;else if("function"==typeof obj.parser)data=obj.parser(responseText),"object"!=typeof data&&console.log("ERROR: Your parser did not return proper object");else try{eval("data = "+responseText)}catch(e){}if(obj.recid)for(var r in data.records)data.records[r].recid=data.records[r][obj.recid];if("undefined"==typeof data&&(data={status:"error",message:this.msgNotJSON,responseText:responseText}),"error"==data.status)obj.error(data.message);else{if("get-records"==cmd)if(0==this.last.xhr_offset)this.records=[],this.summary=[],delete data.status,$.extend(!0,this,data);else{var records=data.records;delete data.records,delete data.status,$.extend(!0,this,data);for(var r in records)this.records.push(records[r])}if("delete-records"==cmd)return void this.reset()}}}else data={status:"error",message:this.msgAJAXerror,responseText:responseText},obj.error(this.msgAJAXerror);var url="object"!=typeof this.url?this.url:this.url.get;url||(this.localSort(),this.localSearch()),this.total=parseInt(this.total),this.trigger($.extend(eventData,{phase:"after"})),0==this.last.xhr_offset?this.refresh():this.scroll(),"function"==typeof callBack&&callBack(data)},error:function(a){var b=this.trigger({target:this.name,type:"error",message:a,xhr:this.last.xhr});return b.isCancelled===!0?void("function"==typeof callBack&&callBack({status:"error",message:"Request aborted."})):(w2alert(a,"Error"),void this.trigger($.extend(b,{phase:"after"})))},getChanges:function(){var a=[];for(var b in this.records){var c=this.records[b];"undefined"!=typeof c.changes&&a.push($.extend(!0,{recid:c.recid},c.changes))}return a},mergeChanges:function(){var changes=this.getChanges();for(var c in changes){var record=this.get(changes[c].recid);for(var s in changes[c])if("recid"!=s){try{eval("record."+s+" = changes[c][s]")}catch(e){}delete record.changes}}this.refresh()},save:function(){var a=this,b=this.getChanges(),c=this.trigger({phase:"before",target:this.name,type:"submit",changes:b});if(c.isCancelled!==!0){var d="object"!=typeof this.url?this.url:this.url.save;d?this.request("save-records",{changes:c.changes},null,function(b){"error"!==b.status&&a.mergeChanges(),a.trigger($.extend(c,{phase:"after"}))}):(this.mergeChanges(),this.trigger($.extend(c,{phase:"after"})))}},editField:function(a,b,c,d){var e=this,f=e.get(a,!0),g=e.records[f],h=e.columns[b],i=h?h.editable:null;if(g&&h&&i&&g.editable!==!1){if(-1!=["enum","file"].indexOf(i.type))return void console.log('ERROR: input types "enum" and "file" are not supported in inline editing.');var j=e.trigger({phase:"before",type:"editField",target:e.name,recid:a,column:b,value:c,index:f,originalEvent:d});if(j.isCancelled!==!0&&(c=j.value,this.selectNone(),this.select({recid:a,column:b}),this.last.edit_col=b,-1==["checkbox","check"].indexOf(i.type))){var k=$("#grid_"+e.name+"_rec_"+w2utils.escapeId(a)),l=k.find("[col="+b+"] > div");"undefined"==typeof i.inTag&&(i.inTag=""),"undefined"==typeof i.outTag&&(i.outTag=""),"undefined"==typeof i.style&&(i.style=""),"undefined"==typeof i.items&&(i.items=[]);var m=w2utils.stripTags(g.changes&&"undefined"!=typeof g.changes[h.field]?g.changes[h.field]:g[h.field]);(null==m||"undefined"==typeof m)&&(m=""),"undefined"!=typeof c&&null!=c&&(m=c);var n="undefined"!=typeof h.style?h.style+";":"";if("string"==typeof h.render&&-1!=["number","int","float","money","percent"].indexOf(h.render.split(":")[0])&&(n+="text-align: right;"),"select"==i.type){var o="";for(var p in i.items)o+='<option value="'+i.items[p].id+'" '+(i.items[p].id==m?"selected":"")+">"+i.items[p].text+"</option>";l.addClass("w2ui-editable").html('<select id="grid_'+e.name+"_edit_"+a+"_"+b+'" column="'+b+'" style="width: 100%; '+n+i.style+'" field="'+h.field+'" recid="'+a+'" '+i.inTag+">"+o+"</select>"+i.outTag),l.find("select").focus().on("change",function(){delete e.last.move}).on("blur",function(a){e.editChange.call(e,this,f,b,a)})}else{l.addClass("w2ui-editable").html('<input id="grid_'+e.name+"_edit_"+a+"_"+b+'" type="text" style="outline: none; '+n+i.style+'" field="'+h.field+'" recid="'+a+'" column="'+b+'" '+i.inTag+">"+i.outTag),null==c&&l.find("input").val("object"!=m?m:"");var q=l.find("input").get(0);$(q).w2field(i.type,$.extend(i,{selected:m})),setTimeout(function(){var a=q;"list"==i.type&&(a=$($(q).data("w2field").helpers.focus).find("input"),"object"!=m&&""!=m&&a.val(m).css({opacity:1}).prev().css({opacity:1})),$(a).on("blur",function(a){e.editChange.call(e,q,f,b,a)})},10),null!=c&&$(q).val("object"!=m?m:"")}setTimeout(function(){l.find("input, select").on("click",function(a){a.stopPropagation()}).on("keydown",function(c){var d=!1;switch(c.keyCode){case 9:d=!0;var i=a,j=c.shiftKey?e.prevCell(b,!0):e.nextCell(b,!0);if(null==j){var k=c.shiftKey?e.prevRow(f):e.nextRow(f);if(null!=k&&k!=f){i=e.records[k].recid;for(var l in e.columns){var k=e.columns[l].editable;if("undefined"!=typeof k&&-1==["checkbox","check"].indexOf(k.type)&&(j=parseInt(l),!c.shiftKey))break}}}i===!1&&(i=a),null==j&&(j=b),this.blur(),setTimeout(function(){"row"!=e.selectType?(e.selectNone(),e.select({recid:i,column:j})):e.editField(i,j,null,c)},1);break;case 13:this.blur();var m=c.shiftKey?e.prevRow(f):e.nextRow(f);null!=m&&m!=f&&setTimeout(function(){"row"!=e.selectType?(e.selectNone(),e.select({recid:e.records[m].recid,column:b})):e.editField(e.records[m].recid,b,null,c)},100);break;case 38:if(!c.shiftKey)break;d=!0;var m=e.prevRow(f);m!=f&&(this.blur(),setTimeout(function(){"row"!=e.selectType?(e.selectNone(),e.select({recid:e.records[m].recid,column:b})):e.editField(e.records[m].recid,b,null,c)},1));break;case 40:if(!c.shiftKey)break;d=!0;var m=e.nextRow(f);null!=m&&m!=f&&(this.blur(),setTimeout(function(){"row"!=e.selectType?(e.selectNone(),e.select({recid:e.records[m].recid,column:b})):e.editField(e.records[m].recid,b,null,c)},1));break;case 27:var n=e.parseField(g,h.field);g.changes&&"undefined"!=typeof g.changes[h.field]&&(n=g.changes[h.field]),this.value="undefined"!=typeof n?n:"",this.blur(),setTimeout(function(){e.select({recid:a,column:b})},1)}d&&c.preventDefault&&c.preventDefault()});var d=l.find("input").focus();null!=c?d[0].setSelectionRange(d.val().length,d.val().length):d.select()},1),e.trigger($.extend(j,{phase:"after"}))}}},editChange:function(a,b,c){var d=0>b;b=0>b?-b-1:b;var e=d?this.summary:this.records,f=e[b],g=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(f.recid)),h=this.columns[c],i=a.value,j=this.parseField(f,h.field),k=$(a).data("w2field");k&&(i=k.clean(i),"list"==k.type&&""!=i&&(i=$(a).data("selected"))),"checkbox"==a.type&&(i=a.checked);for(var l={phase:"before",type:"change",target:this.name,input_id:a.id,recid:f.recid,index:b,column:c,value_new:i,value_previous:f.changes&&f.changes.hasOwnProperty(h.field)?f.changes[h.field]:j,value_original:j};;){if(i=l.value_new,("undefined"==typeof j||null===j?"":String(j))!==String(i)){if(l=this.trigger($.extend(l,{type:"change",phase:"before"})),l.isCancelled!==!0){if(i!==l.value_new)continue;f.changes=f.changes||{},f.changes[h.field]=l.value_new,this.trigger($.extend(l,{phase:"after"}))}}else if(l=this.trigger($.extend(l,{type:"restore",phase:"before"})),l.isCancelled!==!0){if(i!==l.value_new)continue;f.changes&&delete f.changes[h.field],$.isEmptyObject(f.changes)&&delete f.changes,this.trigger($.extend(l,{phase:"after"}))}break}var m=this.getCellHTML(b,c,d);d||(f.changes&&"undefined"!=typeof f.changes[h.field]?$(g).find("[col="+c+"]").addClass("w2ui-changed").html(m):$(g).find("[col="+c+"]").removeClass("w2ui-changed").html(m))},"delete":function(a){var b=this,c=this.trigger({phase:"before",target:this.name,type:"delete",force:a});if(c.isCancelled!==!0){a=c.force;var d=this.getSelection();if(0!=d.length){if(""!=this.msgDelete&&!a)return void w2confirm({title:w2utils.lang("Delete Confirmation"),msg:b.msgDelete,btn_yes:{"class":"btn-red"},callBack:function(a){"Yes"==a&&w2ui[b.name].delete(!0)}});var e="object"!=typeof this.url?this.url:this.url.remove;if(e)this.request("delete-records");else if(this.selectNone(),"object"!=typeof d[0])this.remove.apply(this,d);else{for(var f in d){var g=this.columns[d[f].column].field,h=this.get(d[f].recid,!0);null!=h&&"recid"!=g&&(this.records[h][g]="",this.records[h].changes&&delete this.records[h].changes[g])}this.refresh()}this.trigger($.extend(c,{phase:"after"}))}}},click:function(a,b){var c=(new Date).getTime(),d=null;if(!(1==this.last.cancelClick||b&&b.altKey)){if("object"==typeof a&&(d=a.column,a=a.recid),"undefined"==typeof b&&(b={}),c-parseInt(this.last.click_time)<350&&"click"==b.type)return void this.dblClick(a,b);if(this.last.click_time=c,null==d&&b.target){var e=b.target;"TD"!=e.tagName&&(e=$(e).parents("td")[0]),"undefined"!=typeof $(e).attr("col")&&(d=parseInt($(e).attr("col")))}var f=this.trigger({phase:"before",target:this.name,type:"click",recid:a,column:d,originalEvent:b});if(f.isCancelled!==!0){var g=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(a)).parents("tr");if(g.length>0&&-1!=String(g.attr("id")).indexOf("expanded_row")){var h=g.parents(".w2ui-grid").attr("name");w2ui[h].selectNone(),g.parents(".w2ui-grid").find(".w2ui-expanded-row .w2ui-grid").each(function(a,b){var c=$(b).attr("name");w2ui[c]&&w2ui[c].selectNone()})}$(this.box).find(".w2ui-expanded-row .w2ui-grid").each(function(a,b){var c=$(b).attr("name");w2ui[c]&&w2ui[c].selectNone()});var i=this,j=this.getSelection();$("#grid_"+this.name+"_check_all").prop("checked",!1);var k=this.get(a,!0),l=(this.records[k],[]);if(i.last.sel_ind=k,i.last.sel_col=d,i.last.sel_recid=a,i.last.sel_type="click",b.shiftKey&&j.length>0&&i.multiSelect){if(j[0].recid){var m=this.get(j[0].recid,!0),n=this.get(a,!0);if(d>j[0].column)var o=j[0].column,p=d;else var o=d,p=j[0].column;for(var q=o;p>=q;q++)l.push(q)}else var m=this.get(j[0],!0),n=this.get(a,!0);var r=[];if(m>n){var e=m;m=n,n=e}for(var s="object"!=typeof this.url?this.url:this.url.get,t=m;n>=t;t++)if(!(this.searchData.length>0)||s||-1!=$.inArray(t,this.last.searchIds))if("row"==this.selectType)r.push(this.records[t].recid);else for(var u in l)r.push({recid:this.records[t].recid,column:l[u]});this.select.apply(this,r)}else{var v=this.last.selection,w=-1!=v.indexes.indexOf(k)?!0:!1;(b.ctrlKey||b.shiftKey||b.metaKey)&&this.multiSelect||this.showSelectColumn?("row"!=this.selectType&&-1==$.inArray(d,v.columns[k])&&(w=!1),w===!0?this.unselect({recid:a,column:d}):this.select({recid:a,column:d})):("row"!=this.selectType&&-1==$.inArray(d,v.columns[k])&&(w=!1),j.length>300?this.selectNone():this.unselect.apply(this,j),w===!0?this.unselect({recid:a,column:d}):this.select({recid:a,column:d}))}this.status(),i.initResize(),this.trigger($.extend(f,{phase:"after"}))}}},columnClick:function(a,b){var c=this.trigger({phase:"before",type:"columnClick",target:this.name,field:a,originalEvent:b});if(c.isCancelled!==!0){var d=this.getColumn(a);d.sortable&&this.sort(a,null,b&&(b.ctrlKey||b.metaKey)?!0:!1),this.trigger($.extend(c,{phase:"after"}))}},keydown:function(a){function b(){$("#_tmp_copy_data").remove(),$(document).off("keyup",b)}function c(){var a=Math.floor((h[0].scrollTop+h.height()/2.1)/e.recordHeight);e.records[a]||(a=0),e.select({recid:e.records[a].recid,column:0})}function d(){if("click"!=e.last.sel_type)return!1;if("row"!=e.selectType){if(e.last.sel_type="key",i.length>1){for(var a in i)if(i[a].recid==e.last.sel_recid&&i[a].column==e.last.sel_col){i.splice(a,1);break}return e.unselect.apply(e,i),!0}return!1}return e.last.sel_type="key",i.length>1?(i.splice(i.indexOf(e.records[e.last.sel_ind].recid),1),e.unselect.apply(e,i),!0):!1}var e=this;if(e.keyboard===!0){var f=e.trigger({phase:"before",type:"keydown",target:e.name,originalEvent:a});if(f.isCancelled!==!0){var g=!1,h=$("#grid_"+e.name+"_records"),i=e.getSelection();0==i.length&&(g=!0);var j=i[0]||null,k=[],l=i[i.length-1];if("object"==typeof j&&null!=j){j=i[0].recid,k=[];for(var m=0;;){if(!i[m]||i[m].recid!=j)break;k.push(i[m].column),m++}l=i[i.length-1].recid}var n=e.get(j,!0),o=e.get(l,!0),p=e.get(j),q=$("#grid_"+e.name+"_rec_"+(null!==n?w2utils.escapeId(e.records[n].recid):"none")),r=!1,s=a.keyCode,t=a.shiftKey;switch(9==s&&(s=a.shiftKey?37:39,t=!1,r=!0),s){case 8:case 46:this.show.toolbarDelete&&e["delete"](),r=!0,a.stopPropagation();break;case 27:e.selectNone(),i.length>0&&"object"==typeof i[0]&&e.select({recid:i[0].recid,column:i[0].column}),r=!0;break;case 65:if(!a.metaKey&&!a.ctrlKey)break;e.selectAll(),r=!0;break;case 70:if(!a.metaKey&&!a.ctrlKey)break;$("#grid_"+e.name+"_search_all").focus(),r=!0;break;case 13:if("row"==this.selectType&&e.show.expandColumn===!0){if(q.length<=0)break;e.toggle(j,a),r=!0}else{for(var u in this.columns)if(this.columns[u].editable){k.push(parseInt(u));break}"row"==this.selectType&&this.last.edit_col&&(k=[this.last.edit_col]),k.length>0&&(e.editField(j,k[0],null,a),r=!0)}break;case 37:if(g)break;var v=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(e.records[n].recid)).parents("tr");if(v.length>0&&-1!=String(v.attr("id")).indexOf("expanded_row")){var j=v.prev().attr("recid"),w=v.parents(".w2ui-grid").attr("name");e.selectNone(),w2utils.keyboard.active(w),w2ui[w].set(j,{expanded:!1}),w2ui[w].collapse(j),w2ui[w].click(j),r=!0;break}if("row"==this.selectType){if(q.length<=0||p.expanded!==!0)break;e.set(j,{expanded:!1},!0),e.collapse(j,a)}else{var x=e.prevCell(k[0]);if(null!=x)if(t&&e.multiSelect){if(d())return;var y=[],z=[],A=[];if(0==k.indexOf(this.last.sel_col)&&k.length>1)for(var B in i)-1==y.indexOf(i[B].recid)&&y.push(i[B].recid),A.push({recid:i[B].recid,column:k[k.length-1]});else for(var B in i)-1==y.indexOf(i[B].recid)&&y.push(i[B].recid),z.push({recid:i[B].recid,column:x});e.unselect.apply(e,A),e.select.apply(e,z)}else a.shiftKey=!1,e.click({recid:j,column:x},a);else if(!t)for(var C=1;C<i.length;C++)e.unselect(i[C])}r=!0;break;case 39:if(g)break;if("row"==this.selectType){if(q.length<=0||p.expanded===!0||e.show.expandColumn!==!0)break;e.expand(j,a)}else{var D=e.nextCell(k[k.length-1]);if(null!==D)if(t&&39==s&&e.multiSelect){if(d())return;var y=[],z=[],A=[];if(k.indexOf(this.last.sel_col)==k.length-1&&k.length>1)for(var B in i)-1==y.indexOf(i[B].recid)&&y.push(i[B].recid),A.push({recid:i[B].recid,column:k[0]});else for(var B in i)-1==y.indexOf(i[B].recid)&&y.push(i[B].recid),z.push({recid:i[B].recid,column:D});e.unselect.apply(e,A),e.select.apply(e,z)}else e.click({recid:j,column:D},a);else if(!t)for(var C=0;C<i.length-1;C++)e.unselect(i[C])}r=!0;break;case 38:if(g&&c(),q.length<=0)break;var x=e.prevRow(n);if(null!=x){if(e.records[x].expanded){var E=$("#grid_"+e.name+"_rec_"+w2utils.escapeId(e.records[x].recid)+"_expanded_row").find(".w2ui-grid");if(E.length>0&&w2ui[E.attr("name")]){e.selectNone();var w=E.attr("name"),F=w2ui[w].records;w2utils.keyboard.active(w),w2ui[w].click(F[F.length-1].recid),r=!0;break}}if(t&&e.multiSelect){if(d())return;if("row"==e.selectType)e.last.sel_ind>x&&e.last.sel_ind!=o?e.unselect(e.records[o].recid):e.select(e.records[x].recid);else if(e.last.sel_ind>x&&e.last.sel_ind!=o){x=o;var y=[];for(var u in k)y.push({recid:e.records[x].recid,column:k[u]});e.unselect.apply(e,y)}else{var y=[];for(var u in k)y.push({recid:e.records[x].recid,column:k[u]});e.select.apply(e,y)}}else e.selectNone(),e.click({recid:e.records[x].recid,column:k[0]},a);e.scrollIntoView(x),a.preventDefault&&a.preventDefault()}else{if(!t)for(var C=1;C<i.length;C++)e.unselect(i[C]);var v=$("#grid_"+e.name+"_rec_"+w2utils.escapeId(e.records[n].recid)).parents("tr");if(v.length>0&&-1!=String(v.attr("id")).indexOf("expanded_row")){var j=v.prev().attr("recid"),w=v.parents(".w2ui-grid").attr("name");e.selectNone(),w2utils.keyboard.active(w),w2ui[w].click(j),r=!0;break}}break;case 40:if(g&&c(),q.length<=0)break;if(e.records[o].expanded){var E=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(e.records[o].recid)+"_expanded_row").find(".w2ui-grid");if(E.length>0&&w2ui[E.attr("name")]){e.selectNone();var w=E.attr("name"),F=w2ui[w].records;w2utils.keyboard.active(w),w2ui[w].click(F[0].recid),r=!0;break}}var D=e.nextRow(o);if(null!=D){if(t&&e.multiSelect){if(d())return;if("row"==e.selectType)this.last.sel_ind<D&&this.last.sel_ind!=n?e.unselect(e.records[n].recid):e.select(e.records[D].recid);else if(this.last.sel_ind<D&&this.last.sel_ind!=n){D=n;var y=[];for(var u in k)y.push({recid:e.records[D].recid,column:k[u]});e.unselect.apply(e,y)}else{var y=[];for(var u in k)y.push({recid:e.records[D].recid,column:k[u]});e.select.apply(e,y)}}else e.selectNone(),e.click({recid:e.records[D].recid,column:k[0]},a);e.scrollIntoView(D),r=!0}else{if(!t)for(var C=0;C<i.length-1;C++)e.unselect(i[C]);var v=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(e.records[o].recid)).parents("tr");if(v.length>0&&-1!=String(v.attr("id")).indexOf("expanded_row")){var j=v.next().attr("recid"),w=v.parents(".w2ui-grid").attr("name");e.selectNone(),w2utils.keyboard.active(w),w2ui[w].click(j),r=!0;break}}break;case 17:case 91:if(g)break;var G=e.copy();$("body").append('<textarea id="_tmp_copy_data" onpaste="var obj = this; setTimeout(function () { w2ui[\''+e.name+"'].paste(obj.value); }, 1);\" onkeydown=\"w2ui['"+e.name+'\'].keydown(event)" style="position: absolute; top: -100px; height: 1px; width: 1px">'+G+"</textarea>"),$("#_tmp_copy_data").focus().select(),$(document).on("keyup",b);break;case 88:if(g)break;(a.ctrlKey||a.metaKey)&&setTimeout(function(){e["delete"](!0)},100)}for(var y=[187,189,32],B=48;90>=B;B++)y.push(B);if(-1!=y.indexOf(s)&&!a.ctrlKey&&!a.metaKey&&!r){0==k.length&&k.push(0);var y=String.fromCharCode(s);187==s&&(y="="),189==s&&(y="-"),t||(y=y.toLowerCase()),e.editField(j,k[0],y,a),r=!0}r&&a.preventDefault&&a.preventDefault(),e.trigger($.extend(f,{phase:"after"}))}}},scrollIntoView:function(a){var b=this.records.length;if(0==this.searchData.length||this.url||(b=this.last.searchIds.length),"undefined"==typeof a){var c=this.getSelection();if(0==c.length)return;a=this.get(c[0],!0)}var d=$("#grid_"+this.name+"_records");if(0!=b){var e=this.last.searchIds.length;if(!(d.height()>this.recordHeight*(e>0?e:b))){e>0&&(a=this.last.searchIds.indexOf(a));var f=Math.floor(d[0].scrollTop/this.recordHeight),g=f+Math.floor(d.height()/this.recordHeight);a==f&&d.animate({scrollTop:d.scrollTop()-d.height()/1.3},250,"linear"),a==g&&d.animate({scrollTop:d.scrollTop()+d.height()/1.3},250,"linear"),(f>a||a>g)&&d.animate({scrollTop:(a-1)*this.recordHeight})}}},dblClick:function(a,b){var c=null;if("object"==typeof a&&(c=a.column,a=a.recid),"undefined"==typeof b&&(b={}),null==c&&b.target){var d=b.target;"TD"!=d.tagName&&(d=$(d).parents("td")[0]),c=parseInt($(d).attr("col"))}var e=this.trigger({phase:"before",target:this.name,type:"dblClick",recid:a,column:c,originalEvent:b});if(e.isCancelled!==!0){this.selectNone();var f=this.columns[c];f&&$.isPlainObject(f.editable)?this.editField(a,c,null,b):this.select({recid:a,column:c}),this.trigger($.extend(e,{phase:"after"}))}},contextMenu:function(a,b){var c=this;"text"!=c.last.userSelect&&("undefined"==typeof b&&(b={offsetX:0,offsetY:0,target:$("#grid_"+c.name+"_rec_"+a)[0]}),"undefined"==typeof b.offsetX&&(b.offsetX=b.layerX-b.target.offsetLeft,b.offsetY=b.layerY-b.target.offsetTop),w2utils.isFloat(a)&&(a=parseFloat(a)),-1==this.getSelection().indexOf(a)&&c.click(a),setTimeout(function(){var d=c.trigger({phase:"before",type:"contextMenu",target:c.name,originalEvent:b,recid:a});d.isCancelled!==!0&&(c.menu.length>0&&$(c.box).find(b.target).w2menu(c.menu,{left:b.offsetX,onSelect:function(b){c.menuClick(a,parseInt(b.index),b.originalEvent)}}),c.trigger($.extend(d,{phase:"after"})))},150),b.preventDefault&&b.preventDefault())},menuClick:function(a,b,c){var d=this,e=d.trigger({phase:"before",type:"menuClick",target:d.name,originalEvent:c,recid:a,menuIndex:b,menuItem:d.menu[b]});e.isCancelled!==!0&&d.trigger($.extend(e,{phase:"after"}))},toggle:function(a){var b=this.get(a);return b.expanded===!0?this.collapse(a):this.expand(a)},expand:function(a){function b(){var b=$("#grid_"+d.name+"_rec_"+e+"_expanded"),c=$("#grid_"+d.name+"_rec_"+e+"_expanded_row .w2ui-expanded1 > div");b.height()<5||(b.css("opacity",1),c.show().css("opacity",1),$("#grid_"+d.name+"_cell_"+d.get(a,!0)+"_expand div").html("-"))}var c=this.get(a),d=this,e=w2utils.escapeId(a);if($("#grid_"+this.name+"_rec_"+e+"_expanded_row").length>0)return!1;if("none"==c.expanded)return!1;var f=1+(this.show.selectColumn?1:0),g="";$("#grid_"+this.name+"_rec_"+e).after('<tr id="grid_'+this.name+"_rec_"+e+'_expanded_row" class="w2ui-expanded-row '+g+'">'+(this.show.lineNumbers?'<td class="w2ui-col-number"></td>':"")+' <td class="w2ui-grid-data w2ui-expanded1" colspan="'+f+'"><div style="display: none"></div></td> <td colspan="100" class="w2ui-expanded2"> <div id="grid_'+this.name+"_rec_"+e+'_expanded" style="opacity: 0"></div> </td></tr>');var h=this.trigger({phase:"before",type:"expand",target:this.name,recid:a,box_id:"grid_"+this.name+"_rec_"+e+"_expanded",ready:b});return h.isCancelled===!0?void $("#grid_"+this.name+"_rec_"+e+"_expanded_row").remove():($("#grid_"+this.name+"_rec_"+e).attr("expanded","yes").addClass("w2ui-expanded"),$("#grid_"+this.name+"_rec_"+e+"_expanded_row").show(),$("#grid_"+this.name+"_cell_"+this.get(a,!0)+"_expand div").html('<div class="w2ui-spinner" style="width: 16px; height: 16px; margin: -2px 2px;"></div>'),c.expanded=!0,setTimeout(b,300),this.trigger($.extend(h,{phase:"after"})),this.resizeRecords(),!0)},collapse:function(a){var b=this.get(a),c=this,d=w2utils.escapeId(a);if(0==$("#grid_"+this.name+"_rec_"+d+"_expanded_row").length)return!1;var e=this.trigger({phase:"before",type:"collapse",target:this.name,recid:a,box_id:"grid_"+this.name+"_rec_"+d+"_expanded"});return e.isCancelled!==!0?($("#grid_"+this.name+"_rec_"+d).removeAttr("expanded").removeClass("w2ui-expanded"),$("#grid_"+this.name+"_rec_"+d+"_expanded").css("opacity",0),$("#grid_"+this.name+"_cell_"+this.get(a,!0)+"_expand div").html("+"),setTimeout(function(){$("#grid_"+c.name+"_rec_"+d+"_expanded").height("0px"),setTimeout(function(){$("#grid_"+c.name+"_rec_"+d+"_expanded_row").remove(),delete b.expanded,c.trigger($.extend(e,{phase:"after"})),c.resizeRecords()},300)},200),!0):void 0},sort:function(a,b,c){var d=this.trigger({phase:"before",type:"sort",target:this.name,field:a,direction:b,multiField:c});if(d.isCancelled!==!0){if("undefined"!=typeof a){var e=this.sortData.length;for(var f in this.sortData)if(this.sortData[f].field==a){e=f;break}if("undefined"==typeof b||null==b)if("undefined"==typeof this.sortData[e])b="asc";else switch(String(this.sortData[e].direction)){case"asc":b="desc";break;case"desc":b="asc";break;default:b="asc"}this.multiSort===!1&&(this.sortData=[],e=0),1!=c&&(this.sortData=[],e=0),"undefined"==typeof this.sortData[e]&&(this.sortData[e]={}),this.sortData[e].field=a,this.sortData[e].direction=b}else this.sortData=[];this.selectNone();var g="object"!=typeof this.url?this.url:this.url.get;g?(this.trigger($.extend(d,{phase:"after"})),this.last.xhr_offset=0,this.reload()):(this.localSort(),this.searchData.length>0&&this.localSearch(!0),this.trigger($.extend(d,{phase:"after"})),this.refresh())}},copy:function(){var a=this.getSelection();if(0==a.length)return"";var b="";if("object"==typeof a[0]){var c=a[0].column,d=a[0].column,e=[];for(var f in a)a[f].column<c&&(c=a[f].column),a[f].column>d&&(d=a[f].column),-1==e.indexOf(a[f].index)&&e.push(a[f].index);e.sort();for(var g in e){for(var h=e[g],i=c;d>=i;i++){var j=this.columns[i];j.hidden!==!0&&(b+=w2utils.stripTags(this.getCellHTML(h,i))+" ")}b=b.substr(0,b.length-1),b+="\n"}}else{for(var i in this.columns){var j=this.columns[i];j.hidden!==!0&&(b+='"'+w2utils.stripTags(j.caption?j.caption:j.field)+'" ')}b=b.substr(0,b.length-1),b+="\n";for(var f in a){var h=this.get(a[f],!0);for(var i in this.columns){var j=this.columns[i];j.hidden!==!0&&(b+='"'+w2utils.stripTags(this.getCellHTML(h,i))+'" ')}b=b.substr(0,b.length-1),b+="\n"}}b=b.substr(0,b.length-1);var k=this.trigger({phase:"before",type:"copy",target:this.name,text:b});return k.isCancelled===!0?"":(b=k.text,this.trigger($.extend(k,{phase:"after"})),b)},paste:function(a){var b=this.getSelection(),c=this.get(b[0].recid,!0),d=b[0].column,e=this.trigger({phase:"before",type:"paste",target:this.name,text:a,index:c,column:d});if(e.isCancelled!==!0){if(a=e.text,"row"==this.selectType||0==b.length)return console.log("ERROR: You can paste only if grid.selectType = 'cell' and when at least one cell selected."),void this.trigger($.extend(e,{phase:"after"}));var f=[],a=a.split("\n");for(var g in a){var h=a[g].split(" "),i=0,j=this.records[c],k=[];for(var l in h)if(this.columns[d+i]){var m=this.columns[d+i].field;j.changes=j.changes||{},j.changes[m]=h[l],k.push(d+i),i++}for(var n in k)f.push({recid:j.recid,column:k[n]});c++}this.selectNone(),this.select.apply(this,f),this.refresh(),this.trigger($.extend(e,{phase:"after"}))}},resize:function(){var a=this,b=(new Date).getTime();if(this.box&&$(this.box).attr("name")==this.name){$(this.box).find("> div").css("width",$(this.box).width()).css("height",$(this.box).height());var c=this.trigger({phase:"before",type:"resize",target:this.name});if(c.isCancelled!==!0)return a.resizeBoxes(),a.resizeRecords(),this.trigger($.extend(c,{phase:"after"})),(new Date).getTime()-b}},refreshCell:function(a,b){var c=this.get(a,!0),d=this.getColumn(b,!0),e=this.records[c],f=this.columns[d],g=$("#grid_"+this.name+"_rec_"+a+" [col="+d+"]");g.html(this.getCellHTML(c,d)),e.changes&&"undefined"!=typeof e.changes[f.field]?g.addClass("w2ui-changed"):g.removeClass("w2ui-changed")},refreshRow:function(a){var b=$("#grid_"+this.name+"_rec_"+w2utils.escapeId(a));if(0!=b.length){var c=this.get(a,!0),d=b.attr("line"),e="object"!=typeof this.url?this.url:this.url.get;if(this.searchData.length>0&&!e)for(var f in this.last.searchIds)this.last.searchIds[f]==c&&(c=f);$(b).replaceWith(this.getRecordHTML(c,d))}},refresh:function(){var a=this,b=(new Date).getTime(),c="object"!=typeof this.url?this.url:this.url.get;if(this.total<=0&&!c&&0==this.searchData.length&&(this.total=this.records.length),this.toolbar.disable("w2ui-edit","w2ui-delete"),this.box){var d=this.trigger({phase:"before",target:this.name,type:"refresh"});if(d.isCancelled!==!0){if(this.show.header?$("#grid_"+this.name+"_header").html(this.header+"&nbsp;").show():$("#grid_"+this.name+"_header").hide(),this.show.toolbar){if(this.toolbar&&this.toolbar.get("w2ui-column-on-off")&&this.toolbar.get("w2ui-column-on-off").checked);else if($("#grid_"+this.name+"_toolbar").show(),"object"==typeof this.toolbar){var e=this.toolbar.items;for(var f in e)"w2ui-search"!=e[f].id&&"break"!=e[f].type&&this.toolbar.refresh(e[f].id)}}else $("#grid_"+this.name+"_toolbar").hide();this.searchClose();var g=$("#grid_"+a.name+"_search_all");!this.multiSearch&&"all"==this.last.field&&this.searches.length>0&&(this.last.field=this.searches[0].field,this.last.caption=this.searches[0].caption);for(var h in this.searches)this.searches[h].field==this.last.field&&(this.last.caption=this.searches[h].caption);if(this.last.multi?g.attr("placeholder","["+w2utils.lang("Multiple Fields")+"]"):g.attr("placeholder",this.last.caption),g.val()!=this.last.search){var i=this.last.search,e=g.data("w2field");e&&(i=e.format(i)),g.val(i)}var e=this.find({summary:!0},!0);if(e.length>0){for(var f in e)this.summary.push(this.records[e[f]]);for(var f=e.length-1;f>=0;f--)this.records.splice(e[f],1);this.total=this.total-e.length}var j="";j+='<div id="grid_'+this.name+'_records" class="w2ui-grid-records" onscroll="var obj = w2ui[\''+this.name+"']; obj.last.scrollTop = this.scrollTop; obj.last.scrollLeft = this.scrollLeft; $('#grid_"+this.name+"_columns')[0].scrollLeft = this.scrollLeft; $('#grid_"+this.name+"_summary')[0].scrollLeft = this.scrollLeft; obj.scroll(event);\">"+this.getRecordsHTML()+'</div><div id="grid_'+this.name+'_columns" class="w2ui-grid-columns"> <table>'+this.getColumnsHTML()+"</table></div>",$("#grid_"+this.name+"_body").html(j),this.summary.length>0?$("#grid_"+this.name+"_summary").html(this.getSummaryHTML()).show():$("#grid_"+this.name+"_summary").hide(),this.show.footer?$("#grid_"+this.name+"_footer").html(this.getFooterHTML()).show():$("#grid_"+this.name+"_footer").hide(),this.searchData.length>0?$("#grid_"+this.name+"_searchClear").show():$("#grid_"+this.name+"_searchClear").hide();
+var k=this.last.selection;k.indexes.length==this.records.length||0!==this.searchData.length&&k.indexes.length==this.last.searchIds.length?$("#grid_"+this.name+"_check_all").prop("checked",!0):$("#grid_"+this.name+"_check_all").prop("checked",!1),this.status();var l=a.find({expanded:!0},!0);for(var m in l)a.records[l[m]].expanded=!1;return setTimeout(function(){var b=$.trim($("#grid_"+a.name+"_search_all").val());""!=b&&$(a.box).find(".w2ui-grid-data > div").w2marker(b)},50),this.trigger($.extend(d,{phase:"after"})),a.resize(),a.addRange("selection"),setTimeout(function(){a.resize(),a.scroll()},1),a.reorderColumns&&!a.last.columnDrag?a.last.columnDrag=a.initColumnDrag():!a.reorderColumns&&a.last.columnDrag&&a.last.columnDrag.remove(),(new Date).getTime()-b}}},render:function(a){function b(a){if(1==a.which&&("text"==e.last.userSelect&&(delete e.last.userSelect,$(e.box).find(".w2ui-grid-body").css("user-select","none").css("-webkit-user-select","none").css("-moz-user-select","none").css("-ms-user-select","none"),$(this.box).on("selectstart",function(){return!1})),!($(a.target).parents().hasClass("w2ui-head")||$(a.target).hasClass("w2ui-head")||e.last.move&&"expand"==e.last.move.type))){if(a.altKey)$(e.box).off("selectstart"),$(e.box).find(".w2ui-grid-body").css("user-select","text").css("-webkit-user-select","text").css("-moz-user-select","text").css("-ms-user-select","text"),e.selectNone(),e.last.move={type:"text-select"},e.last.userSelect="text";else{if(!e.multiSelect)return;e.last.move={x:a.screenX,y:a.screenY,divX:0,divY:0,recid:$(a.target).parents("tr").attr("recid"),column:"TD"==a.target.tagName?$(a.target).attr("col"):$(a.target).parents("td").attr("col"),type:"select",ghost:!1,start:!0}}$(document).on("mousemove",c),$(document).on("mouseup",d)}}function c(a){var b=e.last.move;if(b&&"select"==b.type&&(b.divX=a.screenX-b.x,b.divY=a.screenY-b.y,!(Math.abs(b.divX)<=1&&Math.abs(b.divY)<=1))){if(e.last.cancelClick=!0,1==e.reorderRows){if(!b.ghost){var c=$("#grid_"+e.name+"_rec_"+b.recid),d=c.parents("table").find("tr:first-child").clone();b.offsetY=a.offsetY,b.from=b.recid,b.pos=c.position(),b.ghost=$(c).clone(!0),b.ghost.removeAttr("id"),c.find("td:first-child").replaceWith('<td colspan="1000" style="height: '+e.recordHeight+'px; background-color: #ddd"></td>');var f=$(e.box).find(".w2ui-grid-records");f.append('<table id="grid_'+e.name+'_ghost" style="position: absolute; z-index: 999999; opacity: 0.8; border-bottom: 2px dashed #aaa; border-top: 2px dashed #aaa; pointer-events: none;"></table>'),$("#grid_"+e.name+"_ghost").append(d).append(b.ghost)}var g=$(a.target).parents("tr").attr("recid");if(g!=b.from){var h=$("#grid_"+e.name+"_rec_"+b.recid),i=$("#grid_"+e.name+"_rec_"+g);a.screenY-b.lastY<0?h.after(i):i.after(h),b.lastY=a.screenY,b.to=g}var j=$("#grid_"+e.name+"_ghost"),f=$(e.box).find(".w2ui-grid-records");return void j.css({top:b.pos.top+b.divY+f.scrollTop(),left:b.pos.left})}b.start&&b.recid&&(e.selectNone(),b.start=!1);var k=[],g="TR"==a.target.tagName?$(a.target).attr("recid"):$(a.target).parents("tr").attr("recid");if("undefined"!=typeof g){var l=e.get(b.recid,!0);if(null!==l){var m=e.get(g,!0);if(null!==m){var n=parseInt(b.column),o=parseInt("TD"==a.target.tagName?$(a.target).attr("col"):$(a.target).parents("td").attr("col"));if(l>m){var d=l;l=m,m=d}var d="ind1:"+l+",ind2;"+m+",col1:"+n+",col2:"+o;if(b.range!=d){b.range=d;for(var p=l;m>=p;p++)if(!(e.last.searchIds.length>0&&-1==e.last.searchIds.indexOf(p)))if("row"!=e.selectType){if(n>o){var d=n;n=o,o=d}for(var d=[],q=n;o>=q;q++)e.columns[q].hidden||k.push({recid:e.records[p].recid,column:parseInt(q)})}else k.push(e.records[p].recid);if("row"!=e.selectType){var r=e.getSelection(),d=[];for(var s in k){var t=!1;for(var u in r)k[s].recid==r[u].recid&&k[s].column==r[u].column&&(t=!0);t||d.push({recid:k[s].recid,column:k[s].column})}e.select.apply(e,d);var d=[];for(var u in r){var t=!1;for(var s in k)k[s].recid==r[u].recid&&k[s].column==r[u].column&&(t=!0);t||d.push({recid:r[u].recid,column:r[u].column})}e.unselect.apply(e,d)}else if(e.multiSelect){var r=e.getSelection();for(var s in k)-1==r.indexOf(k[s])&&e.select(k[s]);for(var u in r)-1==k.indexOf(r[u])&&e.unselect(r[u])}}}}}}}function d(a){var b=e.last.move;if(setTimeout(function(){delete e.last.cancelClick},1),!$(a.target).parents().hasClass(".w2ui-head")&&!$(a.target).hasClass(".w2ui-head")){if(b&&"select"==b.type&&1==e.reorderRows){var f=e.get(b.from,!0),g=e.records[f];e.records.splice(f,1);var h=e.get(b.to,!0);f>h?e.records.splice(h,0,g):e.records.splice(h+1,0,g),$("#grid_"+e.name+"_ghost").remove(),e.refresh()}delete e.last.move,$(document).off("mousemove",c),$(document).off("mouseup",d)}}var e=this,f=(new Date).getTime();if("undefined"!=typeof a&&null!=a&&($(this.box).find("#grid_"+this.name+"_body").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-grid").html(""),this.box=a),this.box){null==this.last.sortData&&(this.last.sortData=this.sortData);var g=this.trigger({phase:"before",target:this.name,type:"render",box:a});if(g.isCancelled!==!0){if($(this.box).attr("name",this.name).addClass("w2ui-reset w2ui-grid").html('<div> <div id="grid_'+this.name+'_header" class="w2ui-grid-header"></div> <div id="grid_'+this.name+'_toolbar" class="w2ui-grid-toolbar"></div> <div id="grid_'+this.name+'_body" class="w2ui-grid-body"></div> <div id="grid_'+this.name+'_summary" class="w2ui-grid-body w2ui-grid-summary"></div> <div id="grid_'+this.name+'_footer" class="w2ui-grid-footer"></div></div>'),"row"!=this.selectType&&$(this.box).addClass("w2ui-ss"),$(this.box).length>0&&($(this.box)[0].style.cssText+=this.style),this.initToolbar(),null!=this.toolbar&&this.toolbar.render($("#grid_"+this.name+"_toolbar")[0]),this.last.field&&"all"!=this.last.field){var h=this.searchData;this.initAllField(this.last.field,1==h.length?h[0].value:null)}return $("#grid_"+this.name+"_footer").html(this.getFooterHTML()),this.last.state||(this.last.state=this.stateSave(!0)),this.stateRestore(),this.url&&this.refresh(),this.reload(),$(this.box).on("mousedown",b),$(this.box).on("selectstart",function(){return!1}),this.trigger($.extend(g,{phase:"after"})),0==$(".w2ui-layout").length&&(this.tmp_resize=function(){w2ui[e.name].resize()},$(window).off("resize",this.tmp_resize).on("resize",this.tmp_resize)),(new Date).getTime()-f}}},destroy:function(){var a=this.trigger({phase:"before",target:this.name,type:"destroy"});a.isCancelled!==!0&&($(window).off("resize",this.tmp_resize),"object"==typeof this.toolbar&&this.toolbar.destroy&&this.toolbar.destroy(),$(this.box).find("#grid_"+this.name+"_body").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-grid").html(""),delete w2ui[this.name],this.trigger($.extend(a,{phase:"after"})))},initColumnOnOff:function(){if(this.show.toolbarColumns){var a=this,b='<div class="w2ui-col-on-off"><table><tr><td style="width: 30px"> <input id="grid_'+this.name+'_column_ln_check" type="checkbox" tabIndex="-1" '+(a.show.lineNumbers?"checked":"")+" onclick=\"w2ui['"+a.name+"'].columnOnOff(this, event, 'line-numbers');\"></td><td onclick=\"w2ui['"+a.name+"'].columnOnOff(this, event, 'line-numbers'); $('#w2ui-overlay')[0].hide();\"> <label for=\"grid_"+this.name+'_column_ln_check">'+w2utils.lang("Line #")+"</label></td></tr>";for(var c in this.columns){var d=this.columns[c],e=this.columns[c].caption;d.hideable!==!1&&(!e&&this.columns[c].hint&&(e=this.columns[c].hint),e||(e="- column "+(parseInt(c)+1)+" -"),b+='<tr><td style="width: 30px"> <input id="grid_'+this.name+"_column_"+c+'_check" type="checkbox" tabIndex="-1" '+(d.hidden?"":"checked")+" onclick=\"w2ui['"+a.name+"'].columnOnOff(this, event, '"+d.field+'\');"></td><td> <label for="grid_'+this.name+"_column_"+c+'_check">'+e+"</label></td></tr>")}b+='<tr><td colspan="2"><div style="border-top: 1px solid #ddd;"></div></td></tr>';var f="object"!=typeof this.url?this.url:this.url.get;f&&a.show.skipRecords&&(b+='<tr><td colspan="2" style="padding: 0px"> <div style="cursor: pointer; padding: 2px 8px; cursor: default">'+w2utils.lang("Skip")+' <input type="text" style="width: 45px" value="'+this.offset+'" onkeypress="if (event.keyCode == 13) { w2ui[\''+a.name+"'].skip(this.value); $('#w2ui-overlay')[0].hide(); }\"> "+w2utils.lang("Records")+" </div></td></tr>"),b+='<tr><td colspan="2" onclick="w2ui[\''+a.name+"'].stateSave(); $('#w2ui-overlay')[0].hide();\"> <div style=\"cursor: pointer; padding: 4px 8px; cursor: default\">"+w2utils.lang("Save Grid State")+'</div></td></tr><tr><td colspan="2" onclick="w2ui[\''+a.name+"'].stateReset(); $('#w2ui-overlay')[0].hide();\"> <div style=\"cursor: pointer; padding: 4px 8px; cursor: default\">"+w2utils.lang("Restore Default State")+"</div></td></tr>",b+="</table></div>",this.toolbar.get("w2ui-column-on-off").html=b}},initColumnDrag:function(){function a(){i.pressed=!1,clearTimeout(i.timeout)}function b(a){i.timeout&&clearTimeout(i.timeout);var b=this;i.pressed=!0,i.timeout=setTimeout(function(){if(i.pressed){var e,f,g,j,k,l=["w2ui-col-number","w2ui-col-expand","w2ui-col-select"],m=["w2ui-head-last"],n=l.concat(m),o=".w2ui-col-number, .w2ui-col-expand, .w2ui-col-select",p=".w2ui-head.w2ui-col-number, .w2ui-head.w2ui-col-expand, .w2ui-head.w2ui-col-select";if($(a.originalEvent.target).parents().hasClass("w2ui-head")){for(var q=0,r=n.length;r>q;q++)if($(a.originalEvent.target).parents().hasClass(n[q]))return;if(i.numberPreColumnsPresent=$(h.box).find(p).length,i.columnHead=j=$(a.originalEvent.target).parents(".w2ui-head"),k=parseInt(j.attr("col"),10),e=h.trigger({type:"columnDragStart",phase:"before",originalEvent:a,origColumnNumber:k,target:j[0]}),e.isCancelled===!0)return!1;f=i.columns=$(h.box).find(".w2ui-head:not(.w2ui-head-last)"),$(document).on("mouseup",d),$(document).on("mousemove",c),i.originalPos=parseInt($(a.originalEvent.target).parent(".w2ui-head").attr("col"),10),i.ghost=$(b).clone(!0),$(i.ghost).find('[col]:not([col="'+i.originalPos+'"]), .w2ui-toolbar, .w2ui-grid-header').remove(),$(i.ghost).find(o).remove(),$(i.ghost).find(".w2ui-grid-body").css({top:0}),g=$(i.ghost).find('[col="'+i.originalPos+'"]'),$(document.body).append(i.ghost),$(i.ghost).css({width:0,height:0,margin:0,position:"fixed",zIndex:999999,opacity:0}).addClass(".w2ui-grid-ghost").animate({width:g.width(),height:$(h.box).find(".w2ui-grid-body:first").height(),left:a.pageX,top:a.pageY,opacity:.8},0),i.offsets=[];for(var q=0,r=f.length;r>q;q++)i.offsets.push($(f[q]).offset().left);h.trigger($.extend(e,{phase:"after"}))}}},150)}function c(a){if(i.pressed){var b=a.originalEvent.pageX,c=a.originalEvent.pageY,d=i.offsets,h=$(".w2ui-head:not(.w2ui-head-last)").width();i.targetInt=Math.max(i.numberPreColumnsPresent,f(b,d,h)),e(i.targetInt),g(b,c)}}function d(a){i.pressed=!1;var b,e,f,g,j,k=$(".w2ui-grid-ghost");return b=h.trigger({type:"columnDragEnd",phase:"before",originalEvent:a,target:i.columnHead[0]}),b.isCancelled===!0?!1:(f=h.columns[i.originalPos],g=h.columns,j=$(i.columns[Math.min(i.lastInt,i.columns.length-1)]),e=i.lastInt<i.columns.length?parseInt(j.attr("col")):g.length,e!==i.originalPos+1&&e!==i.originalPos&&j&&j.length?($(i.ghost).animate({top:$(h.box).offset().top,left:j.offset().left,width:0,height:0,opacity:.2},300,function(){$(this).remove(),k.remove()}),g.splice(e,0,$.extend({},f)),g.splice(g.indexOf(f),1)):($(i.ghost).remove(),k.remove()),$(document).off("mouseup",d),$(document).off("mousemove",c),i.marker&&i.marker.remove(),i={},h.refresh(),void h.trigger($.extend(b,{phase:"after",targetColumnNumber:e-1})))}function e(a){i.marker||i.markerLeft||(i.marker=$('<div class="col-intersection-marker"><div class="top-marker"></div><div class="bottom-marker"></div></div>'),i.markerLeft=$('<div class="col-intersection-marker"><div class="top-marker"></div><div class="bottom-marker"></div></div>')),i.lastInt&&i.lastInt===a||(i.lastInt=a,i.marker.remove(),i.markerLeft.remove(),$(".w2ui-head").removeClass("w2ui-col-intersection"),a>=i.columns.length?($(i.columns[i.columns.length-1]).children("div:last").append(i.marker.addClass("right").removeClass("left")),$(i.columns[i.columns.length-1]).addClass("w2ui-col-intersection")):a<=i.numberPreColumnsPresent?($(i.columns[i.numberPreColumnsPresent]).prepend(i.marker.addClass("left").removeClass("right")).css({position:"relative"}),$(i.columns[i.numberPreColumnsPresent]).prev().addClass("w2ui-col-intersection")):($(i.columns[a]).children("div:last").prepend(i.marker.addClass("left").removeClass("right")),$(i.columns[a]).prev().children("div:last").append(i.markerLeft.addClass("right").removeClass("left")).css({position:"relative"}),$(i.columns[a-1]).addClass("w2ui-col-intersection")))}function f(a,b,c){if(a<=b[0])return 0;if(a>=b[b.length-1]+c)return b.length;for(var d=0,e=b.length;e>d;d++){var f=b[d],g=b[d+1]||b[d]+c,h=(g-b[d])/2+b[d];if(a>f&&h>=a)return d;if(a>h&&g>=a)return d+1}return intersection}function g(a,b){$(i.ghost).css({left:a-10,top:b-10})}if(this.columnGroups&&this.columnGroups.length)throw"Draggable columns are not currently supported with column groups.";var h=this,i={};return i.lastInt=null,i.pressed=!1,i.timeout=null,i.columnHead=null,$(h.box).on("mousedown",b),$(h.box).on("mouseup",a),{remove:function(){$(h.box).off("mousedown",b),$(h.box).off("mouseup",a),$(h.box).find(".w2ui-head").removeAttr("draggable"),h.last.columnDrag=!1}}},columnOnOff:function(a,b,c){var d=this.trigger({phase:"before",target:this.name,type:"columnOnOff",checkbox:a,field:c,originalEvent:b});if(d.isCancelled!==!0){var e=this;for(var f in this.records)this.records[f].expanded===!0&&(this.records[f].expanded=!1);var g=!0;if("line-numbers"==c)this.show.lineNumbers=!this.show.lineNumbers,this.refresh();else{var h=this.getColumn(c);h.hidden?($(a).prop("checked",!0),this.showColumn(h.field)):($(a).prop("checked",!1),this.hideColumn(h.field)),g=!1}g&&setTimeout(function(){$().w2overlay("",{name:"searches-"+this.name}),e.toolbar.uncheck("column-on-off")},100),this.trigger($.extend(d,{phase:"after"}))}},initToolbar:function(){if("undefined"==typeof this.toolbar.render){var a=this.toolbar.items;if(this.toolbar.items=[],this.toolbar=$().w2toolbar($.extend(!0,{},this.toolbar,{name:this.name+"_toolbar",owner:this})),this.show.toolbarReload&&this.toolbar.items.push($.extend(!0,{},this.buttons.reload)),this.show.toolbarColumns&&this.toolbar.items.push($.extend(!0,{},this.buttons.columns)),(this.show.toolbarReload||this.show.toolbarColumn)&&this.toolbar.items.push({type:"break",id:"w2ui-break0"}),this.show.toolbarSearch){var b='<div class="w2ui-toolbar-search"><table cellpadding="0" cellspacing="0"><tr> <td>'+this.buttons.search.html+'</td> <td> <input id="grid_'+this.name+'_search_all" class="w2ui-search-all" placeholder="'+this.last.caption+'" value="'+this.last.search+'" onkeydown="if (event.keyCode == 13 && w2utils.isIE) this.onchange();" onchange=" var val = this.value; var fld = $(this).data(\'w2field\'); if (fld) val = fld.clean(val); w2ui[\''+this.name+"'].search(w2ui['"+this.name+'\'].last.field, val); "> </td> <td> <div title="'+w2utils.lang("Clear Search")+'" class="w2ui-search-clear" id="grid_'+this.name+'_searchClear" onclick="var obj = w2ui[\''+this.name+"']; obj.searchReset();\" >&nbsp;&nbsp;</div> </td></tr></table></div>";this.toolbar.items.push({type:"html",id:"w2ui-search",html:b}),this.multiSearch&&this.searches.length>0&&this.toolbar.items.push($.extend(!0,{},this.buttons["search-go"]))}this.show.toolbarSearch&&(this.show.toolbarAdd||this.show.toolbarEdit||this.show.toolbarDelete||this.show.toolbarSave)&&this.toolbar.items.push({type:"break",id:"w2ui-break1"}),this.show.toolbarAdd&&this.toolbar.items.push($.extend(!0,{},this.buttons.add)),this.show.toolbarEdit&&this.toolbar.items.push($.extend(!0,{},this.buttons.edit)),this.show.toolbarDelete&&this.toolbar.items.push($.extend(!0,{},this.buttons["delete"])),this.show.toolbarSave&&((this.show.toolbarAdd||this.show.toolbarDelete||this.show.toolbarEdit)&&this.toolbar.items.push({type:"break",id:"w2ui-break2"}),this.toolbar.items.push($.extend(!0,{},this.buttons.save)));for(var c in a)this.toolbar.items.push(a[c]);var d=this;this.toolbar.on("click",function(a){function b(){$("#w2ui-overlay-searches-"+d.name).data("keepOpen")!==!0&&(g.uncheck(e),$(document).off("click","body",b))}var c=d.trigger({phase:"before",type:"toolbar",target:a.target,originalEvent:a});if(c.isCancelled!==!0){var e=a.target;switch(e){case"w2ui-reload":var f=d.trigger({phase:"before",type:"reload",target:d.name});if(f.isCancelled===!0)return!1;d.reload(),d.trigger($.extend(f,{phase:"after"}));break;case"w2ui-column-on-off":d.initColumnOnOff(),d.initResize(),d.resize();break;case"w2ui-search-advanced":var g=this,h=this.get(e);h.checked?(d.searchClose(),setTimeout(function(){g.uncheck(e)},1)):(d.searchOpen(),a.originalEvent.stopPropagation(),$(document).on("click","body",b));break;case"w2ui-add":var c=d.trigger({phase:"before",target:d.name,type:"add",recid:null});d.trigger($.extend(c,{phase:"after"}));break;case"w2ui-edit":var i=d.getSelection(),j=null;1==i.length&&(j=i[0]);var c=d.trigger({phase:"before",target:d.name,type:"edit",recid:j});d.trigger($.extend(c,{phase:"after"}));break;case"w2ui-delete":d["delete"]();break;case"w2ui-save":d.save()}d.trigger($.extend(c,{phase:"after"}))}})}},initResize:function(){var a=this;$(this.box).find(".w2ui-resizer").off("click").on("click",function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0,a.preventDefault&&a.preventDefault()}).off("mousedown").on("mousedown",function(b){b||(b=window.event),window.addEventListener||window.document.attachEvent("onselectstart",function(){return!1}),a.resizing=!0,a.last.tmp={x:b.screenX,y:b.screenY,gx:b.screenX,gy:b.screenY,col:parseInt($(this).attr("name"))},b.stopPropagation?b.stopPropagation():b.cancelBubble=!0,b.preventDefault&&b.preventDefault();for(var c in a.columns)"undefined"==typeof a.columns[c].sizeOriginal&&(a.columns[c].sizeOriginal=a.columns[c].size),a.columns[c].size=a.columns[c].sizeCalculated;var d={phase:"before",type:"columnResize",target:a.name,column:a.last.tmp.col,field:a.columns[a.last.tmp.col].field};d=a.trigger($.extend(d,{resizeBy:0,originalEvent:b}));var e=function(b){if(1==a.resizing){if(b||(b=window.event),d=a.trigger($.extend(d,{resizeBy:b.screenX-a.last.tmp.gx,originalEvent:b})),d.isCancelled===!0)return void(d.isCancelled=!1);a.last.tmp.x=b.screenX-a.last.tmp.x,a.last.tmp.y=b.screenY-a.last.tmp.y,a.columns[a.last.tmp.col].size=parseInt(a.columns[a.last.tmp.col].size)+a.last.tmp.x+"px",a.resizeRecords(),a.last.tmp.x=b.screenX,a.last.tmp.y=b.screenY}},f=function(b){delete a.resizing,$(document).off("mousemove","body"),$(document).off("mouseup","body"),a.resizeRecords(),a.trigger($.extend(d,{phase:"after",originalEvent:b}))};$(document).on("mousemove","body",e),$(document).on("mouseup","body",f)}).each(function(a,b){var c=$(b).parent();$(b).css({height:"25px","margin-left":c.width()-3+"px"})})},resizeBoxes:function(){{var a=($(this.box).find("> div"),$("#grid_"+this.name+"_header")),b=$("#grid_"+this.name+"_toolbar"),c=$("#grid_"+this.name+"_summary"),d=$("#grid_"+this.name+"_footer"),e=$("#grid_"+this.name+"_body");$("#grid_"+this.name+"_columns"),$("#grid_"+this.name+"_records")}this.show.header&&a.css({top:"0px",left:"0px",right:"0px"}),this.show.toolbar&&b.css({top:0+(this.show.header?w2utils.getSize(a,"height"):0)+"px",left:"0px",right:"0px"}),this.show.footer&&d.css({bottom:"0px",left:"0px",right:"0px"}),this.summary.length>0&&c.css({bottom:0+(this.show.footer?w2utils.getSize(d,"height"):0)+"px",left:"0px",right:"0px"}),e.css({top:0+(this.show.header?w2utils.getSize(a,"height"):0)+(this.show.toolbar?w2utils.getSize(b,"height"):0)+"px",bottom:0+(this.show.footer?w2utils.getSize(d,"height"):0)+(this.summary.length>0?w2utils.getSize(c,"height"):0)+"px",left:"0px",right:"0px"})},resizeRecords:function(){var a=this;$(this.box).find(".w2ui-empty-record").remove();var b=$(this.box),c=$(this.box).find("> div"),d=$("#grid_"+this.name+"_header"),e=$("#grid_"+this.name+"_toolbar"),f=$("#grid_"+this.name+"_summary"),g=$("#grid_"+this.name+"_footer"),h=$("#grid_"+this.name+"_body"),i=$("#grid_"+this.name+"_columns"),j=$("#grid_"+this.name+"_records");if(this.fixedBody){var k=c.height()-(this.show.header?w2utils.getSize(d,"height"):0)-(this.show.toolbar?w2utils.getSize(e,"height"):0)-("none"!=f.css("display")?w2utils.getSize(f,"height"):0)-(this.show.footer?w2utils.getSize(g,"height"):0);h.css("height",k)}else{var k=w2utils.getSize(i,"height")+w2utils.getSize($("#grid_"+a.name+"_records table"),"height");a.height=k+w2utils.getSize(c,"+height")+(a.show.header?w2utils.getSize(d,"height"):0)+(a.show.toolbar?w2utils.getSize(e,"height"):0)+("none"!=f.css("display")?w2utils.getSize(f,"height"):0)+(a.show.footer?w2utils.getSize(g,"height"):0),c.css("height",a.height),h.css("height",k),b.css("height",w2utils.getSize(c,"height")+w2utils.getSize(b,"+height"))}var l=this.records.length;0==this.searchData.length||this.url||(l=this.last.searchIds.length);var m=!1,n=!1;if(h.width()<$(j).find(">table").width()&&(m=!0),h.height()-i.height()<$(j).find(">table").height()+(m?w2utils.scrollBarSize():0)&&(n=!0),this.fixedBody||(n=!1,m=!1),m||n?(i.find("> table > tbody > tr:nth-child(1) td.w2ui-head-last").css("width",w2utils.scrollBarSize()).show(),j.css({top:(this.columnGroups.length>0&&this.show.columns?1:0)+w2utils.getSize(i,"height")+"px","-webkit-overflow-scrolling":"touch","overflow-x":m?"auto":"hidden","overflow-y":n?"auto":"hidden"})):(i.find("> table > tbody > tr:nth-child(1) td.w2ui-head-last").hide(),j.css({top:(this.columnGroups.length>0&&this.show.columns?1:0)+w2utils.getSize(i,"height")+"px",overflow:"hidden"}),j.length>0&&(this.last.scrollTop=0,this.last.scrollLeft=0)),this.show.emptyRecords&&!n){var o=Math.floor(j.height()/this.recordHeight)+1;if(this.fixedBody)for(var p=l;o>=p;p++){var q="";q+='<tr class="'+(p%2?"w2ui-even":"w2ui-odd")+' w2ui-empty-record" style="height: '+this.recordHeight+'px">',this.show.lineNumbers&&(q+='<td class="w2ui-col-number"></td>'),this.show.selectColumn&&(q+='<td class="w2ui-grid-data w2ui-col-select"></td>'),this.show.expandColumn&&(q+='<td class="w2ui-grid-data w2ui-col-expand"></td>');for(var r=0;this.columns.length>0;){var s=this.columns[r];if(s.hidden){if(r++,"undefined"==typeof this.columns[r])break}else if(q+='<td class="w2ui-grid-data" '+("undefined"!=typeof s.attr?s.attr:"")+' col="'+r+'"></td>',r++,"undefined"==typeof this.columns[r])break}q+='<td class="w2ui-grid-data-last"></td>',q+="</tr>",$("#grid_"+this.name+"_records > table").append(q)}}if(h.length>0){for(var t=parseInt(h.width())-(n?w2utils.scrollBarSize():0)-(this.show.lineNumbers?34:0)-(this.show.selectColumn?26:0)-(this.show.expandColumn?26:0),u=t,v=0,w=!1,x=0;x<this.columns.length;x++){var s=this.columns[x];s.gridMinWidth>0&&(s.gridMinWidth>u&&s.hidden!==!0&&(s.hidden=!0,w=!0),s.gridMinWidth<u&&s.hidden===!0&&(s.hidden=!1,w=!0))}if(w===!0)return void this.refresh();for(var x=0;x<this.columns.length;x++){var s=this.columns[x];s.hidden||("px"==String(s.size).substr(String(s.size).length-2).toLowerCase()?(t-=parseFloat(s.size),this.columns[x].sizeCalculated=s.size,this.columns[x].sizeType="px"):(v+=parseFloat(s.size),this.columns[x].sizeType="%",delete s.sizeCorrected))}if(100!=v&&v>0)for(var x=0;x<this.columns.length;x++){var s=this.columns[x];s.hidden||"%"==s.sizeType&&(s.sizeCorrected=Math.round(100*parseFloat(s.size)*100/v)/100+"%")}for(var x=0;x<this.columns.length;x++){var s=this.columns[x];s.hidden||"%"==s.sizeType&&(this.columns[x].sizeCalculated="undefined"!=typeof this.columns[x].sizeCorrected?Math.floor(t*parseFloat(s.sizeCorrected)/100)-1+"px":Math.floor(t*parseFloat(s.size)/100)-1+"px")}}for(var y=0,x=0;x<this.columns.length;x++){var s=this.columns[x];s.hidden||("undefined"==typeof s.min&&(s.min=20),parseInt(s.sizeCalculated)<parseInt(s.min)&&(s.sizeCalculated=s.min+"px"),parseInt(s.sizeCalculated)>parseInt(s.max)&&(s.sizeCalculated=s.max+"px"),y+=parseInt(s.sizeCalculated))}var z=parseInt(u)-parseInt(y);if(z>0&&v>0)for(var x=0;;){var s=this.columns[x];if("undefined"!=typeof s)if(s.hidden||"px"==s.sizeType)x++;else{if(s.sizeCalculated=parseInt(s.sizeCalculated)+1+"px",z--,0==z)break;x++}else x=0}else z>0&&i.find("> table > tbody > tr:nth-child(1) td.w2ui-head-last").css("width",w2utils.scrollBarSize()).show();i.find("> table > tbody > tr:nth-child(1) td").each(function(b,c){var d=$(c).attr("col");"undefined"!=typeof d&&a.columns[d]&&$(c).css("width",a.columns[d].sizeCalculated),$(c).hasClass("w2ui-head-last")&&$(c).css("width",w2utils.scrollBarSize()+(z>0&&0==v?z:0)+"px")}),3==i.find("> table > tbody > tr").length&&i.find("> table > tbody > tr:nth-child(1) td").html("").css({height:"0px",border:"0px",padding:"0px",margin:"0px"}),j.find("> table > tbody > tr:nth-child(1) td").each(function(b,c){var d=$(c).attr("col");"undefined"!=typeof d&&a.columns[d]&&$(c).css("width",a.columns[d].sizeCalculated),$(c).hasClass("w2ui-grid-data-last")&&$(c).css("width",(z>0&&0==v?z:0)+"px")}),f.find("> table > tbody > tr:nth-child(1) td").each(function(b,c){var d=$(c).attr("col");"undefined"!=typeof d&&a.columns[d]&&$(c).css("width",a.columns[d].sizeCalculated),$(c).hasClass("w2ui-grid-data-last")&&$(c).css("width",w2utils.scrollBarSize()+(z>0&&0==v?z:0)+"px")}),this.initResize(),this.refreshRanges(),""!=this.last.scrollTop&&j.length>0&&(i.prop("scrollLeft",this.last.scrollLeft),j.prop("scrollTop",this.last.scrollTop),j.prop("scrollLeft",this.last.scrollLeft))},getSearchesHTML:function(){for(var a='<table cellspacing="0">',b=!1,c=0;c<this.searches.length;c++){var d=this.searches[c];if(d.type=String(d.type).toLowerCase(),!d.hidden){var e="";if(0==b&&(e='<button class="btn close-btn" onclick="obj = w2ui[\''+this.name+"']; if (obj) { obj.searchClose(); }\">X</button",b=!0),"undefined"==typeof d.inTag&&(d.inTag=""),"undefined"==typeof d.outTag&&(d.outTag=""),"undefined"==typeof d.type&&(d.type="text"),-1!=["text","alphanumeric","combo"].indexOf(d.type))var f='<select id="grid_'+this.name+"_operator_"+c+'" onclick="event.stopPropagation();"> <option value="is">'+w2utils.lang("is")+'</option> <option value="begins">'+w2utils.lang("begins")+'</option> <option value="contains">'+w2utils.lang("contains")+'</option> <option value="ends">'+w2utils.lang("ends")+"</option></select>";if(-1!=["int","float","money","currency","percent","date","time"].indexOf(d.type))var f='<select id="grid_'+this.name+"_operator_"+c+'" onchange="w2ui[\''+this.name+"'].initOperator(this, "+c+');" onclick="event.stopPropagation();"> <option value="is">'+w2utils.lang("is")+"</option>"+(-1!=["int"].indexOf(d.type)?'<option value="in">'+w2utils.lang("in")+"</option>":"")+(-1!=["int"].indexOf(d.type)?'<option value="not in">'+w2utils.lang("not in")+"</option>":"")+'<option value="between">'+w2utils.lang("between")+"</option></select>";if(-1!=["select","list","hex"].indexOf(d.type))var f='<select id="grid_'+this.name+"_operator_"+c+'" onclick="event.stopPropagation();"> <option value="is">'+w2utils.lang("is")+"</option></select>";if(-1!=["enum"].indexOf(d.type))var f='<select id="grid_'+this.name+"_operator_"+c+'" onclick="event.stopPropagation();"> <option value="in">'+w2utils.lang("in")+'</option> <option value="in">'+w2utils.lang("not in")+"</option></select>";switch(a+='<tr> <td class="close-btn">'+e+'</td> <td class="caption">'+d.caption+'</td> <td class="operator">'+f+'</td> <td class="value">',d.type){case"text":case"alphanumeric":case"hex":case"list":case"combo":case"enum":a+='<input rel="search" type="text" style="width: 300px;" id="grid_'+this.name+"_field_"+c+'" name="'+d.field+'" '+d.inTag+">";break;case"int":case"float":case"money":case"currency":case"percent":case"date":case"time":a+='<input rel="search" type="text" size="12" id="grid_'+this.name+"_field_"+c+'" name="'+d.field+'" '+d.inTag+'><span id="grid_'+this.name+"_range_"+c+'" style="display: none">&nbsp;-&nbsp;&nbsp;<input rel="search" type="text" style="width: 90px" id="grid_'+this.name+"_field2_"+c+'" name="'+d.field+'" '+d.inTag+"></span>";break;case"select":a+='<select rel="search" id="grid_'+this.name+"_field_"+c+'" name="'+d.field+'" '+d.inTag+' onclick="event.stopPropagation();"></select>'}a+=d.outTag+" </td></tr>"}}return a+='<tr> <td colspan="4" class="actions"> <div> <button class="btn" onclick="obj = w2ui[\''+this.name+"']; if (obj) { obj.searchReset(); }\">"+w2utils.lang("Reset")+'</button> <button class="btn btn-blue" onclick="obj = w2ui[\''+this.name+"']; if (obj) { obj.search(); }\">"+w2utils.lang("Search")+"</button> </div> </td></tr></table>"},initOperator:function(a,b){var c=this,d=c.searches[b],e=$("#grid_"+c.name+"_range_"+b),f=$("#grid_"+c.name+"_field_"+b),g=f.parent().find("span input");f.w2field("in"==$(a).val()||"not in"==$(a).val()?"clear":d.type),"between"==$(a).val()?(e.show(),g.w2field(d.type)):e.hide()},initSearches:function(){var a=this;for(var b in this.searches){var c=this.searches[b],d=this.getSearchData(c.field);switch(c.type=String(c.type).toLowerCase(),"object"!=typeof c.options&&(c.options={}),c.type){case"text":case"alphanumeric":$("#grid_"+this.name+"_operator_"+b).val("begins"),-1!=["alphanumeric","hex"].indexOf(c.type)&&$("#grid_"+this.name+"_field_"+b).w2field(c.type,c.options);break;case"int":case"float":case"money":case"currency":case"percent":case"date":case"time":if(d&&"int"==d.type&&-1!=["in","not in"].indexOf(d.operator))break;$("#grid_"+this.name+"_field_"+b).w2field(c.type,c.options),$("#grid_"+this.name+"_field2_"+b).w2field(c.type,c.options),setTimeout(function(){$("#grid_"+a.name+"_field_"+b).keydown(),$("#grid_"+a.name+"_field2_"+b).keydown()},1);break;case"hex":break;case"list":case"combo":case"enum":var e=c.options;"list"==c.type&&(e.selected={}),"enum"==c.type&&(e.selected=[]),d&&(e.selected=d.value),$("#grid_"+this.name+"_field_"+b).w2field(c.type,e),"combo"==c.type&&$("#grid_"+this.name+"_operator_"+b).val("begins");break;case"select":var e='<option value="">--</option>';for(var f in c.options.items){var g=c.options.items[f];if($.isPlainObject(c.options.items[f])){var h=g.id,i=g.text;"undefined"==typeof h&&"undefined"!=typeof g.value&&(h=g.value),"undefined"==typeof i&&"undefined"!=typeof g.caption&&(i=g.caption),null==h&&(h=""),e+='<option value="'+h+'">'+i+"</option>"}else e+='<option value="'+g+'">'+g+"</option>"}$("#grid_"+this.name+"_field_"+b).html(e)}null!=d&&("int"==d.type&&-1!=["in","not in"].indexOf(d.operator)&&$("#grid_"+this.name+"_field_"+b).w2field("clear").val(d.value),$("#grid_"+this.name+"_operator_"+b).val(d.operator).trigger("change"),$.isArray(d.value)?-1!=["in","not in"].indexOf(d.operator)?$("#grid_"+this.name+"_field_"+b).val(d.value).trigger("change"):($("#grid_"+this.name+"_field_"+b).val(d.value[0]).trigger("change"),$("#grid_"+this.name+"_field2_"+b).val(d.value[1]).trigger("change")):"udefined"!=typeof d.value&&$("#grid_"+this.name+"_field_"+b).val(d.value).trigger("change"))}$("#w2ui-overlay-searches-"+this.name+" .w2ui-grid-searches *[rel=search]").on("keypress",function(b){13==b.keyCode&&(a.search(),$().w2overlay())})},getColumnsHTML:function(){function a(){var a="<tr>";""!=c.columnGroups[c.columnGroups.length-1].caption&&c.columnGroups.push({caption:""}),c.show.lineNumbers&&(a+='<td class="w2ui-head w2ui-col-number"> <div>&nbsp;</div></td>'),c.show.selectColumn&&(a+='<td class="w2ui-head w2ui-col-select"> <div>&nbsp;</div></td>'),c.show.expandColumn&&(a+='<td class="w2ui-head w2ui-col-expand"> <div>&nbsp;</div></td>');for(var b=0,d=0;d<c.columnGroups.length;d++){var e=c.columnGroups[d],f=c.columns[b];if(("undefined"==typeof e.span||e.span!=parseInt(e.span))&&(e.span=1),"undefined"!=typeof e.colspan&&(e.span=e.colspan),e.master===!0){var g="";
+for(var h in c.sortData)c.sortData[h].field==f.field&&(RegExp("asc","i").test(c.sortData[h].direction)&&(g="w2ui-sort-up"),RegExp("desc","i").test(c.sortData[h].direction)&&(g="w2ui-sort-down"));var i="";f.resizable!==!1&&(i='<div class="w2ui-resizer" name="'+b+'"></div>'),a+='<td class="w2ui-head '+g+'" col="'+b+'" rowspan="2" colspan="'+(e.span+(d==c.columnGroups.length-1?1:0))+'" onclick="w2ui[\''+c.name+"'].columnClick('"+f.field+"', event);\">"+i+' <div class="w2ui-col-group w2ui-col-header '+(g?"w2ui-col-sorted":"")+'"> <div class="'+g+'"></div>'+(f.caption?f.caption:"&nbsp;")+" </div></td>"}else a+='<td class="w2ui-head" col="'+b+'" colspan="'+(e.span+(d==c.columnGroups.length-1?1:0))+'"> <div class="w2ui-col-group">'+(e.caption?e.caption:"&nbsp;")+" </div></td>";b+=e.span}return a+="</tr>"}function b(a){var b="<tr>",d=!c.reorderColumns||c.columnGroups&&c.columnGroups.length?"":" w2ui-reorder-cols-head ";c.show.lineNumbers&&(b+='<td class="w2ui-head w2ui-col-number" onclick="w2ui[\''+c.name+"'].columnClick('line-number', event);\"> <div>#</div></td>"),c.show.selectColumn&&(b+='<td class="w2ui-head w2ui-col-select" onclick="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;"> <div> <input type="checkbox" id="grid_'+c.name+'_check_all" tabIndex="-1" style="'+(0==c.multiSelect?"display: none;":"")+'" onclick="if (this.checked) w2ui[\''+c.name+"'].selectAll(); else w2ui['"+c.name+"'].selectNone(); if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;\"> </div></td>"),c.show.expandColumn&&(b+='<td class="w2ui-head w2ui-col-expand"> <div>&nbsp;</div></td>');for(var e=0,f=0,g=0;g<c.columns.length;g++){var h=c.columns[g],i={};if(g==f&&(f+="undefined"!=typeof c.columnGroups[e]?parseInt(c.columnGroups[e].span):0,e++),"undefined"!=typeof c.columnGroups[e-1])var i=c.columnGroups[e-1];if(!h.hidden){var j="";for(var k in c.sortData)c.sortData[k].field==h.field&&(RegExp("asc","i").test(c.sortData[k].direction)&&(j="w2ui-sort-up"),RegExp("desc","i").test(c.sortData[k].direction)&&(j="w2ui-sort-down"));if(i.master!==!0||a){var l="";h.resizable!==!1&&(l='<div class="w2ui-resizer" name="'+g+'"></div>'),b+='<td col="'+g+'" class="w2ui-head '+j+d+'" onclick="w2ui[\''+c.name+"'].columnClick('"+h.field+"', event);\">"+l+' <div class="w2ui-col-header '+(j?"w2ui-col-sorted":"")+'"> <div class="'+j+'"></div>'+(h.caption?h.caption:"&nbsp;")+" </div></td>"}}}return b+='<td class="w2ui-head w2ui-head-last"><div>&nbsp;</div></td>',b+="</tr>"}var c=this,d="";return this.show.columnHeaders&&(d=this.columnGroups.length>0?b(!0)+a()+b(!1):b(!0)),d},getRecordsHTML:function(){var a=this.records.length;0==this.searchData.length||this.url||(a=this.last.searchIds.length),this.show_extra=a>300?30:300;var b=$("#grid_"+this.name+"_records"),c=Math.floor(b.height()/this.recordHeight)+this.show_extra+1;(!this.fixedBody||c>a)&&(c=a);var d="<table>"+this.getRecordHTML(-1,0);d+='<tr id="grid_'+this.name+'_rec_top" line="top" style="height: 0px"> <td colspan="200"></td></tr>';for(var e=0;c>e;e++)d+=this.getRecordHTML(e,e+1);return d+='<tr id="grid_'+this.name+'_rec_bottom" line="bottom" style="height: '+(a-c)*this.recordHeight+'px"> <td colspan="200"></td></tr><tr id="grid_'+this.name+'_rec_more" style="display: none"> <td colspan="200" class="w2ui-load-more"></td></tr></table>',this.last.range_start=0,this.last.range_end=c,d},getSummaryHTML:function(){if(0!=this.summary.length){for(var a="<table>",b=0;b<this.summary.length;b++)a+=this.getRecordHTML(b,b+1,!0);return a+="</table>"}},scroll:function(){function a(){b.markSearch!==!1&&(clearTimeout(b.last.marker_timer),b.last.marker_timer=setTimeout(function(){var a=[];for(var c in b.searchData){var d=b.searchData[c];-1==$.inArray(d.value,a)&&a.push(d.value)}a.length>0&&$(b.box).find(".w2ui-grid-data > div").w2marker(a)},50))}var b=((new Date).getTime(),this),c=$("#grid_"+this.name+"_records"),d=this.records.length;if(0==this.searchData.length||this.url||(d=this.last.searchIds.length),0!=d&&0!=c.length&&0!=c.height()){if(this.show_extra=d>300?30:300,c.height()<d*this.recordHeight&&"hidden"==c.css("overflow-y"))return void(this.total>0&&this.refresh());var e=Math.round(c[0].scrollTop/this.recordHeight+1),f=e+(Math.round(c.height()/this.recordHeight)-1);e>d&&(e=d),f>d&&(f=d);var g="object"!=typeof this.url?this.url:this.url.get;if($("#grid_"+this.name+"_footer .w2ui-footer-right").html(w2utils.formatNumber(this.offset+e)+"-"+w2utils.formatNumber(this.offset+f)+" "+w2utils.lang("of")+" "+w2utils.formatNumber(this.total)+(g?" ("+w2utils.lang("buffered")+" "+w2utils.formatNumber(d)+(this.offset>0?", skip "+w2utils.formatNumber(this.offset):"")+")":"")),g||this.fixedBody&&!(this.total<=300)){var h=Math.floor(c[0].scrollTop/this.recordHeight)-this.show_extra,i=h+Math.floor(c.height()/this.recordHeight)+2*this.show_extra+1;1>h&&(h=1),i>this.total&&(i=this.total);var j=c.find("#grid_"+this.name+"_rec_top"),k=c.find("#grid_"+this.name+"_rec_bottom");-1!=String(j.next().prop("id")).indexOf("_expanded_row")&&j.next().remove(),this.total>i&&-1!=String(k.prev().prop("id")).indexOf("_expanded_row")&&k.prev().remove();var l=parseInt(j.next().attr("line")),m=parseInt(k.prev().attr("line"));if(h>l||1==l||this.last.pull_refresh){if(i<=m+this.show_extra-2&&i!=this.total)return;for(this.last.pull_refresh=!1;;){var n=c.find("#grid_"+this.name+"_rec_top").next();if("bottom"==n.attr("line"))break;if(!(parseInt(n.attr("line"))<h))break;n.remove()}var n=c.find("#grid_"+this.name+"_rec_bottom").prev(),o=n.attr("line");"top"==o&&(o=h);for(var p=parseInt(o)+1;i>=p;p++)this.records[p-1]&&(this.records[p-1].expanded===!0&&(this.records[p-1].expanded=!1),k.before(this.getRecordHTML(p-1,p)));a(),setTimeout(function(){b.refreshRanges()},0)}else{if(h>=l-this.show_extra+2&&h>1)return;for(;;){var n=c.find("#grid_"+this.name+"_rec_bottom").prev();if("top"==n.attr("line"))break;if(!(parseInt(n.attr("line"))>i))break;n.remove()}var n=c.find("#grid_"+this.name+"_rec_top").next(),o=n.attr("line");"bottom"==o&&(o=i);for(var p=parseInt(o)-1;p>=h;p--)this.records[p-1]&&(this.records[p-1].expanded===!0&&(this.records[p-1].expanded=!1),j.after(this.getRecordHTML(p-1,p)));a(),setTimeout(function(){b.refreshRanges()},0)}var q=(h-1)*b.recordHeight,r=(d-i)*b.recordHeight;0>r&&(r=0),j.css("height",q+"px"),k.css("height",r+"px"),b.last.range_start=h,b.last.range_end=i;var s=Math.floor(c[0].scrollTop/this.recordHeight),t=s+Math.floor(c.height()/this.recordHeight);if(t+10>d&&this.last.pull_more!==!0&&d<this.total-this.offset)if(this.autoLoad===!0)this.last.pull_more=!0,this.last.xhr_offset+=this.limit,this.request("get-records");else{var u=$("#grid_"+this.name+"_rec_more");"none"==u.css("display")&&u.show().on("click",function(){b.last.pull_more=!0,b.last.xhr_offset+=b.limit,b.request("get-records"),$(this).find("td").html('<div><div style="width: 20px; height: 20px;" class="w2ui-spinner"></div></div>')}),-1==u.find("td").text().indexOf("Load")&&u.find("td").html("<div>"+w2utils.lang("Load")+" "+b.limit+" "+w2utils.lang("More")+"...</div>")}d>=this.total-this.offset&&$("#grid_"+this.name+"_rec_more").hide()}}},getRecordHTML:function(a,b,c){var d,e="",f=this.last.selection;if(-1==a){e+='<tr line="0">',this.show.lineNumbers&&(e+='<td class="w2ui-col-number" style="height: 0px;"></td>'),this.show.selectColumn&&(e+='<td class="w2ui-col-select" style="height: 0px;"></td>'),this.show.expandColumn&&(e+='<td class="w2ui-col-expand" style="height: 0px;"></td>');for(var g in this.columns)this.columns[g].hidden||(e+='<td class="w2ui-grid-data" col="'+g+'" style="height: 0px;"></td>');return e+='<td class="w2ui-grid-data-last" style="height: 0px;"></td>',e+="</tr>"}var h="object"!=typeof this.url?this.url:this.url.get;if(c!==!0)if(this.searchData.length>0&&!h){if(a>=this.last.searchIds.length)return"";a=this.last.searchIds[a],d=this.records[a]}else{if(a>=this.records.length)return"";d=this.records[a]}else{if(a>=this.summary.length)return"";d=this.summary[a]}if(!d)return"";var i=(w2utils.escapeId(d.recid),!1);if(-1!=f.indexes.indexOf(a)&&(i=!0),e+='<tr id="grid_'+this.name+"_rec_"+d.recid+'" recid="'+d.recid+'" line="'+b+'" class="'+(b%2==0?"w2ui-even":"w2ui-odd")+(i&&"row"==this.selectType?" w2ui-selected":"")+(d.expanded===!0?" w2ui-expanded":"")+'" '+(c!==!0?w2utils.isIOS?" onclick = \"w2ui['"+this.name+"'].dblClick('"+d.recid+"', event);\"":" onclick = \"w2ui['"+this.name+"'].click('"+d.recid+"', event);\" oncontextmenu = \"w2ui['"+this.name+"'].contextMenu('"+d.recid+"', event);\"":"")+' style="height: '+this.recordHeight+"px; "+(i||"string"!=typeof d.style?"":d.style)+'" '+("string"==typeof d.style?'custom_style="'+d.style+'"':"")+">",this.show.lineNumbers&&(e+='<td id="grid_'+this.name+"_cell_"+a+"_number"+(c?"_s":"")+'" class="w2ui-col-number">'+(c!==!0?"<div>"+b+"</div>":"")+"</td>"),this.show.selectColumn&&(e+='<td id="grid_'+this.name+"_cell_"+a+"_select"+(c?"_s":"")+'" class="w2ui-grid-data w2ui-col-select" onclick="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;">'+(c!==!0?' <div> <input class="w2ui-grid-select-check" type="checkbox" tabIndex="-1" '+(i?'checked="checked"':"")+" onclick=\"var obj = w2ui['"+this.name+"']; if (!obj.multiSelect) { obj.selectNone(); } if (this.checked) obj.select('"+d.recid+"'); else obj.unselect('"+d.recid+"'); if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;\"> </div>":"")+"</td>"),this.show.expandColumn){var j="";j=d.expanded===!0?"-":"+","none"==d.expanded&&(j=""),"spinner"==d.expanded&&(j='<div class="w2ui-spinner" style="width: 16px; margin: -2px 2px;"></div>'),e+='<td id="grid_'+this.name+"_cell_"+a+"_expand"+(c?"_s":"")+'" class="w2ui-grid-data w2ui-col-expand">'+(c!==!0?' <div ondblclick="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;" onclick="w2ui[\''+this.name+"'].toggle('"+d.recid+"', event); if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;\"> "+j+" </div>":"")+"</td>"}for(var k=0;;){var l=this.columns[k];if(l.hidden){if(k++,"undefined"==typeof this.columns[k])break}else{var m=!c&&d.changes&&"undefined"!=typeof d.changes[l.field],n=this.getCellHTML(a,k,c),o="";if("string"==typeof l.render){var p=l.render.toLowerCase().split(":");-1!=["number","int","float","money","currency","percent"].indexOf(p[0])&&(o+="text-align: right;")}"object"==typeof d.style&&"string"==typeof d.style[k]&&(o+=d.style[k]+";");var q=!1;if(i&&-1!=$.inArray(k,f.columns[a])&&(q=!0),e+='<td class="w2ui-grid-data'+(q?" w2ui-selected":"")+(m?" w2ui-changed":"")+'" col="'+k+'" style="'+o+("undefined"!=typeof l.style?l.style:"")+'" '+("undefined"!=typeof l.attr?l.attr:"")+">"+n+"</td>",k++,"undefined"==typeof this.columns[k])break}}return e+='<td class="w2ui-grid-data-last"></td>',e+="</tr>"},getCellHTML:function(a,b,c){var d=this.columns[b],e=c!==!0?this.records[a]:this.summary[a],f=this.getCellValue(a,b,c),g=d.editable;if("undefined"!=typeof d.render){if("function"==typeof d.render&&(f=$.trim(d.render.call(this,e,a,b)),(f.length<4||"<div"!=f.substr(0,4).toLowerCase())&&(f="<div>"+f+"</div>")),"object"==typeof d.render&&(f="<div>"+(d.render[f]||"")+"</div>"),"string"==typeof d.render){var h=d.render.toLowerCase().split(":"),i="",j="";-1!=["number","int","float","money","currency","percent"].indexOf(h[0])&&("undefined"!=typeof h[1]&&w2utils.isInt(h[1])||(h[1]=0),h[1]>20&&(h[1]=20),h[1]<0&&(h[1]=0),-1!=["money","currency"].indexOf(h[0])&&(h[1]=w2utils.settings.currencyPrecision,i=w2utils.settings.currencyPrefix,j=w2utils.settings.currencySuffix),"percent"==h[0]&&(j="%","0"!==h[1]&&(h[1]=1)),"int"==h[0]&&(h[1]=0),f="<div>"+(""!==f?i+w2utils.formatNumber(Number(f).toFixed(h[1]))+j:"")+"</div>"),"time"==h[0]&&(("undefined"==typeof h[1]||""==h[1])&&(h[1]=w2utils.settings.time_format),f="<div>"+i+w2utils.formatTime(f,"h12"==h[1]?"hh:mi pm":"h24:min")+j+"</div>"),"date"==h[0]&&(("undefined"==typeof h[1]||""==h[1])&&(h[1]=w2utils.settings.date_display),f="<div>"+i+w2utils.formatDate(f,h[1])+j+"</div>"),"age"==h[0]&&(f="<div>"+i+w2utils.age(f)+j+"</div>"),"toggle"==h[0]&&(f="<div>"+i+(f?"Yes":"")+j+"</div>")}}else{var k="";if(g&&-1!=["checkbox","check"].indexOf(g.type)){var l=c?-(a+1):a;k="text-align: center",f='<input type="checkbox" '+(f?"checked":"")+" onclick=\" var obj = w2ui['"+this.name+"']; obj.editChange.call(obj, this, "+l+", "+b+', event); ">'}if(this.show.recordTitles){var m=String(f).replace(/"/g,"''");"undefined"!=typeof d.title&&("function"==typeof d.title&&(m=d.title.call(this,e,a,b)),"string"==typeof d.title&&(m=d.title));var f='<div title="'+w2utils.stripTags(m)+'" style="'+k+'">'+f+"</div>"}else var f='<div style="'+k+'">'+f+"</div>"}return(null==f||"undefined"==typeof f)&&(f=""),f},getCellValue:function(a,b,c){var d=this.columns[b],e=c!==!0?this.records[a]:this.summary[a],f=this.parseField(e,d.field);return e.changes&&"undefined"!=typeof e.changes[d.field]&&(f=e.changes[d.field]),(null==f||"undefined"==typeof f)&&(f=""),f},getFooterHTML:function(){return'<div> <div class="w2ui-footer-left"></div> <div class="w2ui-footer-right"></div> <div class="w2ui-footer-center"></div></div>'},status:function(a){if("undefined"!=typeof a)$("#grid_"+this.name+"_footer").find(".w2ui-footer-left").html(a);else{var b="",c=this.getSelection();if(c.length>0){b=String(c.length).replace(/(\d)(?=(\d\d\d)+(?!\d))/g,"$1,")+" "+w2utils.lang("selected");var d=c[0];"object"==typeof d&&(d=d.recid+", "+w2utils.lang("Column")+": "+d.column),1==c.length&&(b=w2utils.lang("Record ID")+": "+d+" ")}$("#grid_"+this.name+"_footer .w2ui-footer-left").html(b),1==c.length?this.toolbar.enable("w2ui-edit"):this.toolbar.disable("w2ui-edit"),c.length>=1?this.toolbar.enable("w2ui-delete"):this.toolbar.disable("w2ui-delete")}},lock:function(){var a=$(this.box).find("> div:first-child"),b=Array.prototype.slice.call(arguments,0);b.unshift(a),setTimeout(function(){w2utils.lock.apply(window,b)},10)},unlock:function(){var a=this.box;setTimeout(function(){w2utils.unlock(a)},25)},stateSave:function(a){if(!localStorage)return null;var b={columns:[],show:$.extend({},this.show),last:{search:this.last.search,multi:this.last.multi,logic:this.last.logic,caption:this.last.caption,field:this.last.field,scrollTop:this.last.scrollTop,scrollLeft:this.last.scrollLeft},sortData:[],searchData:[]};for(var c in this.columns){var d=this.columns[c];b.columns.push({field:d.field,hidden:d.hidden,size:d.size,sizeCalculated:d.sizeCalculated,sizeOriginal:d.sizeOriginal,sizeType:d.sizeType})}for(var c in this.sortData)b.sortData.push($.extend({},this.sortData[c]));for(var c in this.searchData)b.searchData.push($.extend({},this.searchData[c]));if(a!==!0){var e=this.trigger({phase:"before",type:"stateSave",target:this.name,state:b});if(e.isCancelled===!0)return void("function"==typeof callBack&&callBack({status:"error",message:"Request aborted."}));try{var f=$.parseJSON(localStorage.w2ui||"{}");f||(f={}),f.states||(f.states={}),f.states[this.name]=b,localStorage.w2ui=JSON.stringify(f)}catch(g){return delete localStorage.w2ui,null}this.trigger($.extend(e,{phase:"after"}))}return b},stateRestore:function(a){var b=this;if(!a)try{if(!localStorage)return!1;var c=$.parseJSON(localStorage.w2ui||"{}");c||(c={}),c.states||(c.states={}),a=c.states[this.name]}catch(d){return delete localStorage.w2ui,null}var e=this.trigger({phase:"before",type:"stateRestore",target:this.name,state:a});if(e.isCancelled===!0)return void("function"==typeof callBack&&callBack({status:"error",message:"Request aborted."}));if($.isPlainObject(a)){$.extend(this.show,a.show),$.extend(this.last,a.last);var f=this.last.scrollTop,g=this.last.scrollLeft;for(var h in a.columns){var c=a.columns[h],i=this.getColumn(c.field);i&&$.extend(i,c)}this.sortData.splice(0,this.sortData.length);for(var h in a.sortData)this.sortData.push(a.sortData[h]);this.searchData.splice(0,this.searchData.length);for(var h in a.searchData)this.searchData.push(a.searchData[h]);setTimeout(function(){b.sortData.length>0&&b.localSort(),b.searchData.length>0&&b.localSearch(),b.last.scrollTop=f,b.last.scrollLeft=g,b.refresh()},1)}return this.trigger($.extend(e,{phase:"after"})),!0},stateReset:function(){if(this.stateRestore(this.last.state),localStorage)try{var a=$.parseJSON(localStorage.w2ui||"{}");a.states&&a.states[this.name]&&delete a.states[this.name],localStorage.w2ui=JSON.stringify(a)}catch(b){return delete localStorage.w2ui,null}},parseField:function(a,b){var c="";try{c=a;var d=String(b).split(".");for(var e in d)c=c[d[e]]}catch(f){c=""}return c},prepareData:function(){for(var a in this.records){var b=this.records[a];for(var c in this.columns){var d=this.columns[c];if(null!=b[d.field]&&"string"==typeof d.render){if(-1!=["number","int","float","money","currency","percent"].indexOf(d.render.split(":")[0])&&"number"!=typeof b[d.field]&&(b[d.field]=parseFloat(b[d.field])),-1!=["date","age"].indexOf(d.render.split(":")[0])&&!b[d.field+"_"]){var e=b[d.field];w2utils.isInt(e)&&(e=parseInt(e)),b[d.field+"_"]=new Date(e)}if(-1!=["time"].indexOf(d.render))if(w2utils.isTime(b[d.field])){var f=w2utils.isTime(b[d.field],!0),e=new Date;e.setHours(f.hours,f.minutes,f.seconds?f.seconds:0,0),b[d.field+"_"]||(b[d.field+"_"]=e)}else{var f=b[d.field];w2utils.isInt(f)&&(f=parseInt(f));var f=null!=f?new Date(f):new Date,e=new Date;e.setHours(f.getHours(),f.getMinutes(),f.getSeconds(),0),b[d.field+"_"]||(b[d.field+"_"]=e)}}}}},nextCell:function(a,b){var c=a+1;if(this.columns.length==c)return null;if(b===!0){var d=this.columns[c].editable;if(this.columns[c].hidden||"undefined"==typeof d||d&&-1!=["checkbox","check"].indexOf(d.type))return this.nextCell(c,b)}return c},prevCell:function(a,b){var c=a-1;if(0>c)return null;if(b===!0){var d=this.columns[c].editable;if(this.columns[c].hidden||"undefined"==typeof d||d&&-1!=["checkbox","check"].indexOf(d.type))return this.prevCell(c,b)}return c},nextRow:function(a){if(a+1<this.records.length&&0==this.last.searchIds.length||this.last.searchIds.length>0&&a<this.last.searchIds[this.last.searchIds.length-1]){if(a++,this.last.searchIds.length>0)for(;;){if(-1!=$.inArray(a,this.last.searchIds)||a>this.records.length)break;a++}return a}return null},prevRow:function(a){if(a>0&&0==this.last.searchIds.length||this.last.searchIds.length>0&&a>this.last.searchIds[0]){if(a--,this.last.searchIds.length>0)for(;;){if(-1!=$.inArray(a,this.last.searchIds)||0>a)break;a--}return a}return null}},$.extend(w2grid.prototype,w2utils.event),w2obj.grid=w2grid}(),function(){var a=function(a){this.box=null,this.name=null,this.panels=[],this.tmp={},this.padding=1,this.resizer=4,this.style="",this.onShow=null,this.onHide=null,this.onResizing=null,this.onResizerClick=null,this.onRender=null,this.onRefresh=null,this.onResize=null,this.onDestroy=null,$.extend(!0,this,w2obj.layout,a)},b=["top","left","main","preview","right","bottom"];$.fn.w2layout=function(c){function d(a,b,c){var d=a.get(b);return null!==d&&"undefined"==typeof c&&(c=d.tabs),null===d||null===c?!1:($.isArray(c)&&(c={tabs:c}),$().w2destroy(a.name+"_"+b+"_tabs"),d.tabs=$().w2tabs($.extend({},c,{owner:a,name:a.name+"_"+b+"_tabs"})),d.show.tabs=!0,!0)}function e(a,b,c){var d=a.get(b);return null!==d&&"undefined"==typeof c&&(c=d.toolbar),null===d||null===c?!1:($.isArray(c)&&(c={items:c}),$().w2destroy(a.name+"_"+b+"_toolbar"),d.toolbar=$().w2toolbar($.extend({},c,{owner:a,name:a.name+"_"+b+"_toolbar"})),d.show.toolbar=!0,!0)}if("object"==typeof c||!c){if(!w2utils.checkName(c,"w2layout"))return;var f=c.panels||[],g=new a(c);$.extend(g,{handlers:[],panels:[]});for(var h=0,i=f.length;i>h;h++)g.panels[h]=$.extend(!0,{},a.prototype.panel,f[h]),($.isPlainObject(g.panels[h].tabs)||$.isArray(g.panels[h].tabs))&&d(g,f[h].type),($.isPlainObject(g.panels[h].toolbar)||$.isArray(g.panels[h].toolbar))&&e(g,f[h].type);for(var j in b)j=b[j],null===g.get(j)&&g.panels.push($.extend(!0,{},a.prototype.panel,{type:j,hidden:"main"!==j,size:50}));return $(this).length>0&&g.render($(this)[0]),w2ui[g.name]=g,g}if(w2ui[$(this).attr("name")]){var k=w2ui[$(this).attr("name")];return k[c].apply(k,Array.prototype.slice.call(arguments,1)),this}console.log("ERROR: Method "+c+" does not exist on jQuery.w2layout")},a.prototype={panel:{type:null,title:"",size:100,minSize:20,maxSize:!1,hidden:!1,resizable:!1,overflow:"auto",style:"",content:"",tabs:null,toolbar:null,width:null,height:null,show:{toolbar:!1,tabs:!1},onRefresh:null,onShow:null,onHide:null},html:function(a,b,c){return this.content(a,b,c)},content:function(a,b,c){var d=this,e=this.get(a);if("css"==a)return $("#layout_"+d.name+"_panel_css").html("<style>"+b+"</style>"),!0;if(null===e)return!1;if("undefined"==typeof b||null===b)return e.content;if(b instanceof jQuery)return console.log("ERROR: You can not pass jQuery object to w2layout.content() method"),!1;var f="#layout_"+this.name+"_panel_"+e.type,g=$(f+"> .w2ui-panel-content"),h=0;if(g.length>0&&($(f).scrollTop(0),h=$(g).position().top),""===e.content)e.content=b,this.refresh(a);else{if(e.content=b,!e.hidden&&null!==c&&""!==c&&"undefined"!=typeof c){var i=$(f+"> .w2ui-panel-content");i.after('<div class="w2ui-panel-content new-panel" style="'+i[0].style.cssText+'"></div>');var j=$(f+"> .w2ui-panel-content.new-panel");i.css("top",h),j.css("top",h),"object"==typeof b?(b.box=j[0],b.render()):j.html(b),w2utils.transition(i[0],j[0],c,function(){i.remove(),j.removeClass("new-panel"),j.css("overflow",e.overflow),d.resize(),-1!=window.navigator.userAgent.indexOf("MSIE")&&setTimeout(function(){d.resize()},100)})}this.refresh(a)}return d.resize(),-1!=window.navigator.userAgent.indexOf("MSIE")&&setTimeout(function(){d.resize()},100),!0},load:function(a,b,c,d){var e=this;return"css"==a?($.get(b,function(b,c,f){e.content(a,f.responseText),d&&d()}),!0):null!==this.get(a)?($.get(b,function(b,f,g){e.content(a,g.responseText,c),d&&d(),e.resize(),-1!=window.navigator.userAgent.indexOf("MSIE")&&setTimeout(function(){e.resize()},100)}),!0):!1},sizeTo:function(a,b){var c=this,d=c.get(a);return null===d?!1:($(c.box).find(" > div > .w2ui-panel").css({"-webkit-transition":".2s","-moz-transition":".2s","-ms-transition":".2s","-o-transition":".2s"}),setTimeout(function(){c.set(a,{size:b})},1),setTimeout(function(){$(c.box).find(" > div > .w2ui-panel").css({"-webkit-transition":"0s","-moz-transition":"0s","-ms-transition":"0s","-o-transition":"0s"}),c.resize()},500),!0)},show:function(a,b){var c=this,d=this.trigger({phase:"before",type:"show",target:a,object:this.get(a),immediate:b});if(d.isCancelled!==!0){var e=c.get(a);return null===e?!1:(e.hidden=!1,b===!0?($("#layout_"+c.name+"_panel_"+a).css({opacity:"1"}),e.resizable&&$("#layout_"+c.name+"_resizer_"+a).show(),c.trigger($.extend(d,{phase:"after"})),c.resize()):(e.resizable&&$("#layout_"+c.name+"_resizer_"+a).show(),$("#layout_"+c.name+"_panel_"+a).css({opacity:"0"}),$(c.box).find(" > div > .w2ui-panel").css({"-webkit-transition":".2s","-moz-transition":".2s","-ms-transition":".2s","-o-transition":".2s"}),setTimeout(function(){c.resize()},1),setTimeout(function(){$("#layout_"+c.name+"_panel_"+a).css({opacity:"1"})},250),setTimeout(function(){$(c.box).find(" > div > .w2ui-panel").css({"-webkit-transition":"0s","-moz-transition":"0s","-ms-transition":"0s","-o-transition":"0s"}),c.trigger($.extend(d,{phase:"after"})),c.resize()},500)),!0)}},hide:function(a,b){var c=this,d=this.trigger({phase:"before",type:"hide",target:a,object:this.get(a),immediate:b});if(d.isCancelled!==!0){var e=c.get(a);return null===e?!1:(e.hidden=!0,b===!0?($("#layout_"+c.name+"_panel_"+a).css({opacity:"0"}),$("#layout_"+c.name+"_resizer_"+a).hide(),c.trigger($.extend(d,{phase:"after"})),c.resize()):($("#layout_"+c.name+"_resizer_"+a).hide(),$(c.box).find(" > div > .w2ui-panel").css({"-webkit-transition":".2s","-moz-transition":".2s","-ms-transition":".2s","-o-transition":".2s"}),$("#layout_"+c.name+"_panel_"+a).css({opacity:"0"}),setTimeout(function(){c.resize()},1),setTimeout(function(){$(c.box).find(" > div > .w2ui-panel").css({"-webkit-transition":"0s","-moz-transition":"0s","-ms-transition":"0s","-o-transition":"0s"}),c.trigger($.extend(d,{phase:"after"})),c.resize()},500)),!0)}},toggle:function(a,b){var c=this.get(a);return null===c?!1:c.hidden?this.show(a,b):this.hide(a,b)},set:function(a,b){var c=this.get(a,!0);return null===c?!1:($.extend(this.panels[c],b),"undefined"!=typeof b.content&&this.refresh(a),this.resize(),!0)},get:function(a,b){for(var c in this.panels)if(this.panels[c].type==a)return b===!0?c:this.panels[c];return null},el:function(a){var b=$("#layout_"+this.name+"_panel_"+a+"> .w2ui-panel-content");return 1!=b.length?null:b[0]},hideToolbar:function(a){var b=this.get(a);b&&(b.show.toolbar=!1,$("#layout_"+this.name+"_panel_"+a+"> .w2ui-panel-toolbar").hide(),this.resize())},showToolbar:function(a){var b=this.get(a);b&&(b.show.toolbar=!0,$("#layout_"+this.name+"_panel_"+a+"> .w2ui-panel-toolbar").show(),this.resize())},toggleToolbar:function(a){var b=this.get(a);b&&(b.show.toolbar?this.hideToolbar(a):this.showToolbar(a))},hideTabs:function(a){var b=this.get(a);b&&(b.show.tabs=!1,$("#layout_"+this.name+"_panel_"+a+"> .w2ui-panel-tabs").hide(),this.resize())},showTabs:function(a){var b=this.get(a);b&&(b.show.tabs=!0,$("#layout_"+this.name+"_panel_"+a+"> .w2ui-panel-tabs").show(),this.resize())},toggleTabs:function(a){var b=this.get(a);b&&(b.show.tabs?this.hideTabs(a):this.showTabs(a))},render:function(a){function c(){g.tmp.events={resize:function(){w2ui[g.name].resize()},resizeStart:d,mouseMove:f,mouseUp:e},$(window).on("resize",g.tmp.events.resize)}function d(a,c){if(g.box){c||(c=window.event),window.addEventListener||window.document.attachEvent("onselectstart",function(){return!1}),$(document).off("mousemove",g.tmp.events.mouseMove).on("mousemove",g.tmp.events.mouseMove),$(document).off("mouseup",g.tmp.events.mouseUp).on("mouseup",g.tmp.events.mouseUp),g.tmp.resize={type:a,x:c.screenX,y:c.screenY,diff_x:0,diff_y:0,value:0};for(var d in b)d=b[d],g.lock(d,{opacity:0});("left"==a||"right"==a)&&(g.tmp.resize.value=parseInt($("#layout_"+g.name+"_resizer_"+a)[0].style.left)),("top"==a||"preview"==a||"bottom"==a)&&(g.tmp.resize.value=parseInt($("#layout_"+g.name+"_resizer_"+a)[0].style.top))}}function e(a){if(g.box&&(a||(a=window.event),window.addEventListener||window.document.attachEvent("onselectstart",function(){return!1}),$(document).off("mousemove",g.tmp.events.mouseMove),$(document).off("mouseup",g.tmp.events.mouseUp),"undefined"!=typeof g.tmp.resize)){for(var c in b)g.unlock(b[c]);if(0!==g.tmp.diff_x||0!==g.tmp.resize.diff_y){var d,e,f=g.get("top"),h=g.get("bottom"),i=g.get(g.tmp.resize.type),j=parseInt($(g.box).height()),k=parseInt($(g.box).width()),l=String(i.size);switch(g.tmp.resize.type){case"top":d=parseInt(i.sizeCalculated)+g.tmp.resize.diff_y,e=0;break;case"bottom":d=parseInt(i.sizeCalculated)-g.tmp.resize.diff_y,e=0;break;case"preview":d=parseInt(i.sizeCalculated)-g.tmp.resize.diff_y,e=(f&&!f.hidden?f.sizeCalculated:0)+(h&&!h.hidden?h.sizeCalculated:0);break;case"left":d=parseInt(i.sizeCalculated)+g.tmp.resize.diff_x,e=0;break;case"right":d=parseInt(i.sizeCalculated)-g.tmp.resize.diff_x,e=0}i.size="%"==l.substr(l.length-1)?Math.floor(100*d/("left"==i.type||"right"==i.type?k:j-e)*100)/100+"%":d,g.resize()}$("#layout_"+g.name+"_resizer_"+g.tmp.resize.type).removeClass("active"),delete g.tmp.resize}}function f(a){if(g.box&&(a||(a=window.event),"undefined"!=typeof g.tmp.resize)){var b=g.get(g.tmp.resize.type),c=g.tmp.resize,d=g.trigger({phase:"before",type:"resizing",target:g.name,object:b,originalEvent:a,panel:c?c.type:"all",diff_x:c?c.diff_x:0,diff_y:c?c.diff_y:0});if(d.isCancelled!==!0){var e=$("#layout_"+g.name+"_resizer_"+c.type),f=a.screenX-c.x,h=a.screenY-c.y,i=g.get("main");switch(e.hasClass("active")||e.addClass("active"),c.type){case"left":b.minSize-f>b.width&&(f=b.minSize-b.width),b.maxSize&&b.width+f>b.maxSize&&(f=b.maxSize-b.width),i.minSize+f>i.width&&(f=i.width-i.minSize);break;case"right":b.minSize+f>b.width&&(f=b.width-b.minSize),b.maxSize&&b.width-f>b.maxSize&&(f=b.width-b.maxSize),i.minSize-f>i.width&&(f=i.minSize-i.width);break;case"top":b.minSize-h>b.height&&(h=b.minSize-b.height),b.maxSize&&b.height+h>b.maxSize&&(h=b.maxSize-b.height),i.minSize+h>i.height&&(h=i.height-i.minSize);break;case"preview":case"bottom":b.minSize+h>b.height&&(h=b.height-b.minSize),b.maxSize&&b.height-h>b.maxSize&&(h=b.height-b.maxSize),i.minSize-h>i.height&&(h=i.minSize-i.height)}switch(c.diff_x=f,c.diff_y=h,c.type){case"top":case"preview":case"bottom":c.diff_x=0,e.length>0&&(e[0].style.top=c.value+c.diff_y+"px");break;case"left":case"right":c.diff_y=0,e.length>0&&(e[0].style.left=c.value+c.diff_x+"px")}g.trigger($.extend(d,{phase:"after"}))}}}var g=this,h=(new Date).getTime(),i=g.trigger({phase:"before",type:"render",target:g.name,box:a});if(i.isCancelled!==!0){if("undefined"!=typeof a&&null!==a&&($(g.box).find("#layout_"+g.name+"_panel_main").length>0&&$(g.box).removeAttr("name").removeClass("w2ui-layout").html(""),g.box=a),!g.box)return!1;$(g.box).attr("name",g.name).addClass("w2ui-layout").html("<div></div>"),$(g.box).length>0&&($(g.box)[0].style.cssText+=g.style);for(var j in b){j=b[j];var k=(g.get(j),'<div id="layout_'+g.name+"_panel_"+j+'" class="w2ui-panel"> <div class="w2ui-panel-title"></div> <div class="w2ui-panel-tabs"></div> <div class="w2ui-panel-toolbar"></div> <div class="w2ui-panel-content"></div></div><div id="layout_'+g.name+"_resizer_"+j+'" class="w2ui-resizer"></div>');$(g.box).find(" > div").append(k)}return $(g.box).find(" > div").append('<div id="layout_'+g.name+'_panel_css" style="position: absolute; top: 10000px;"></div'),g.refresh(),g.trigger($.extend(i,{phase:"after"})),setTimeout(function(){c(),g.resize()},0),(new Date).getTime()-h}},refresh:function(a){var b=this;"undefined"==typeof a&&(a=null);var c=(new Date).getTime(),d=b.trigger({phase:"before",type:"refresh",target:"undefined"!=typeof a?a:b.name,object:b.get(a)});if(d.isCancelled!==!0){if("string"==typeof a){var e=b.get(a);if(null===e)return;var f="#layout_"+b.name+"_panel_"+e.type,g="#layout_"+b.name+"_resizer_"+e.type;$(f).css({display:e.hidden?"none":"block"}),e.resizable?$(g).show():$(g).hide(),"object"==typeof e.content&&"function"==typeof e.content.render?(e.content.box=$(f+"> .w2ui-panel-content")[0],setTimeout(function(){$(f+"> .w2ui-panel-content").length>0&&($(f+"> .w2ui-panel-content").removeClass().addClass("w2ui-panel-content").css("overflow",e.overflow)[0].style.cssText+=";"+e.style),e.content.render()},1)):$(f+"> .w2ui-panel-content").length>0&&($(f+"> .w2ui-panel-content").removeClass().addClass("w2ui-panel-content").html(e.content).css("overflow",e.overflow)[0].style.cssText+=";"+e.style);var h=$(b.box).find(f+"> .w2ui-panel-tabs");e.show.tabs?0===h.find("[name="+e.tabs.name+"]").length&&null!==e.tabs?h.w2render(e.tabs):e.tabs.refresh():h.html("").removeClass("w2ui-tabs").hide(),h=$(b.box).find(f+"> .w2ui-panel-toolbar"),e.show.toolbar?0===h.find("[name="+e.toolbar.name+"]").length&&null!==e.toolbar?h.w2render(e.toolbar):e.toolbar.refresh():h.html("").removeClass("w2ui-toolbar").hide(),h=$(b.box).find(f+"> .w2ui-panel-title"),e.title?h.html(e.title).show():h.html("").hide()}else{if(0==$("#layout_"+b.name+"_panel_main").length)return void b.render();b.resize();for(var i in this.panels)b.refresh(this.panels[i].type)}return b.trigger($.extend(d,{phase:"after"})),(new Date).getTime()-c}},resize:function(){if(!this.box)return!1;var a=(new Date).getTime(),c=this.tmp.resize,d=this.trigger({phase:"before",type:"resize",target:this.name,panel:c?c.type:"all",diff_x:c?c.diff_x:0,diff_y:c?c.diff_y:0});if(d.isCancelled!==!0){this.padding<0&&(this.padding=0);
+var e=parseInt($(this.box).width()),f=parseInt($(this.box).height());$(this.box).find(" > div").css({width:e+"px",height:f+"px"});var g,h,i,j,k,l=this,m=this.get("main"),n=this.get("preview"),o=this.get("left"),p=this.get("right"),q=this.get("top"),r=this.get("bottom"),s=null!==n&&n.hidden!==!0?!0:!1,t=null!==o&&o.hidden!==!0?!0:!1,u=null!==p&&p.hidden!==!0?!0:!1,v=null!==q&&q.hidden!==!0?!0:!1,w=null!==r&&r.hidden!==!0?!0:!1;for(var x in b)if(x=b[x],"main"!==x){var c=this.get(x);if(c){var y=String(c.size||0);if("%"==y.substr(y.length-1)){var z=f;"preview"==c.type&&(z=z-(q&&!q.hidden?q.sizeCalculated:0)-(r&&!r.hidden?r.sizeCalculated:0)),c.sizeCalculated=parseInt(("left"==c.type||"right"==c.type?e:z)*parseFloat(c.size)/100)}else c.sizeCalculated=parseInt(c.size);c.sizeCalculated=Math.max(c.sizeCalculated,parseInt(c.minSize))}}null!==q&&q.hidden!==!0?(g=0,h=0,i=e,j=q.sizeCalculated,$("#layout_"+this.name+"_panel_top").css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px"}).show(),q.width=i,q.height=j,q.resizable&&(h=q.sizeCalculated-(0===this.padding?this.resizer:0),j=this.resizer>this.padding?this.resizer:this.padding,$("#layout_"+this.name+"_resizer_top").show().css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px",cursor:"ns-resize"}).off("mousedown").on("mousedown",function(a){var b=l.trigger({phase:"before",type:"resizerClick",target:"top",originalEvent:a});if(b.isCancelled!==!0)return w2ui[l.name].tmp.events.resizeStart("top",a),l.trigger($.extend(b,{phase:"after"})),!1}))):$("#layout_"+this.name+"_panel_top").hide(),null!==o&&o.hidden!==!0?(g=0,h=0+(v?q.sizeCalculated+this.padding:0),i=o.sizeCalculated,j=f-(v?q.sizeCalculated+this.padding:0)-(w?r.sizeCalculated+this.padding:0),k=$("#layout_"+this.name+"_panel_left"),-1!=window.navigator.userAgent.indexOf("MSIE")&&k.length>0&&k[0].clientHeight<k[0].scrollHeight&&(i+=17),k.css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px"}).show(),o.width=i,o.height=j,o.resizable&&(g=o.sizeCalculated-(0===this.padding?this.resizer:0),i=this.resizer>this.padding?this.resizer:this.padding,$("#layout_"+this.name+"_resizer_left").show().css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px",cursor:"ew-resize"}).off("mousedown").on("mousedown",function(a){var b=l.trigger({phase:"before",type:"resizerClick",target:"left",originalEvent:a});if(b.isCancelled!==!0)return w2ui[l.name].tmp.events.resizeStart("left",a),l.trigger($.extend(b,{phase:"after"})),!1}))):($("#layout_"+this.name+"_panel_left").hide(),$("#layout_"+this.name+"_resizer_left").hide()),null!==p&&p.hidden!==!0?(g=e-p.sizeCalculated,h=0+(v?q.sizeCalculated+this.padding:0),i=p.sizeCalculated,j=f-(v?q.sizeCalculated+this.padding:0)-(w?r.sizeCalculated+this.padding:0),$("#layout_"+this.name+"_panel_right").css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px"}).show(),p.width=i,p.height=j,p.resizable&&(g-=this.padding,i=this.resizer>this.padding?this.resizer:this.padding,$("#layout_"+this.name+"_resizer_right").show().css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px",cursor:"ew-resize"}).off("mousedown").on("mousedown",function(a){var b=l.trigger({phase:"before",type:"resizerClick",target:"right",originalEvent:a});if(b.isCancelled!==!0)return w2ui[l.name].tmp.events.resizeStart("right",a),l.trigger($.extend(b,{phase:"after"})),!1}))):$("#layout_"+this.name+"_panel_right").hide(),null!==r&&r.hidden!==!0?(g=0,h=f-r.sizeCalculated,i=e,j=r.sizeCalculated,$("#layout_"+this.name+"_panel_bottom").css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px"}).show(),r.width=i,r.height=j,r.resizable&&(h-=0===this.padding?0:this.padding,j=this.resizer>this.padding?this.resizer:this.padding,$("#layout_"+this.name+"_resizer_bottom").show().css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px",cursor:"ns-resize"}).off("mousedown").on("mousedown",function(a){var b=l.trigger({phase:"before",type:"resizerClick",target:"bottom",originalEvent:a});if(b.isCancelled!==!0)return w2ui[l.name].tmp.events.resizeStart("bottom",a),l.trigger($.extend(b,{phase:"after"})),!1}))):$("#layout_"+this.name+"_panel_bottom").hide(),g=0+(t?o.sizeCalculated+this.padding:0),h=0+(v?q.sizeCalculated+this.padding:0),i=e-(t?o.sizeCalculated+this.padding:0)-(u?p.sizeCalculated+this.padding:0),j=f-(v?q.sizeCalculated+this.padding:0)-(w?r.sizeCalculated+this.padding:0)-(s?n.sizeCalculated+this.padding:0),k=$("#layout_"+this.name+"_panel_main"),-1!=window.navigator.userAgent.indexOf("MSIE")&&k.length>0&&k[0].clientHeight<k[0].scrollHeight&&(i+=17),k.css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px"}),m.width=i,m.height=j,null!==n&&n.hidden!==!0?(g=0+(t?o.sizeCalculated+this.padding:0),h=f-(w?r.sizeCalculated+this.padding:0)-n.sizeCalculated,i=e-(t?o.sizeCalculated+this.padding:0)-(u?p.sizeCalculated+this.padding:0),j=n.sizeCalculated,k=$("#layout_"+this.name+"_panel_preview"),-1!=window.navigator.userAgent.indexOf("MSIE")&&k.length>0&&k[0].clientHeight<k[0].scrollHeight&&(i+=17),k.css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px"}).show(),n.width=i,n.height=j,n.resizable&&(h-=0===this.padding?0:this.padding,j=this.resizer>this.padding?this.resizer:this.padding,$("#layout_"+this.name+"_resizer_preview").show().css({display:"block",left:g+"px",top:h+"px",width:i+"px",height:j+"px",cursor:"ns-resize"}).off("mousedown").on("mousedown",function(a){var b=l.trigger({phase:"before",type:"resizerClick",target:"preview",originalEvent:a});if(b.isCancelled!==!0)return w2ui[l.name].tmp.events.resizeStart("preview",a),l.trigger($.extend(b,{phase:"after"})),!1}))):$("#layout_"+this.name+"_panel_preview").hide();for(var A in b){A=b[A];var B=this.get(A),C="#layout_"+this.name+"_panel_"+A+" > .w2ui-panel-",D=0;B&&(B.title&&(D+=w2utils.getSize($(C+"title").css({top:D+"px",display:"block"}),"height")),B.show.tabs&&(null!==B.tabs&&w2ui[this.name+"_"+A+"_tabs"]&&w2ui[this.name+"_"+A+"_tabs"].resize(),D+=w2utils.getSize($(C+"tabs").css({top:D+"px",display:"block"}),"height")),B.show.toolbar&&(null!==B.toolbar&&w2ui[this.name+"_"+A+"_toolbar"]&&w2ui[this.name+"_"+A+"_toolbar"].resize(),D+=w2utils.getSize($(C+"toolbar").css({top:D+"px",display:"block"}),"height"))),$(C+"content").css({display:"block"}).css({top:D+"px"})}return clearTimeout(this._resize_timer),this._resize_timer=setTimeout(function(){for(var a in w2ui)if("function"==typeof w2ui[a].resize){"undefined"==w2ui[a].panels&&w2ui[a].resize();var b=$(w2ui[a].box).parents(".w2ui-layout");b.length>0&&b.attr("name")==l.name&&w2ui[a].resize()}},100),this.trigger($.extend(d,{phase:"after"})),(new Date).getTime()-a}},destroy:function(){var a=this.trigger({phase:"before",type:"destroy",target:this.name});if(a.isCancelled!==!0)return"undefined"==typeof w2ui[this.name]?!1:($(this.box).find("#layout_"+this.name+"_panel_main").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-layout").html(""),delete w2ui[this.name],this.trigger($.extend(a,{phase:"after"})),this.tmp.events&&this.tmp.events.resize&&$(window).off("resize",this.tmp.events.resize),!0)},lock:function(a){if(-1==b.indexOf(a))return void console.log("ERROR: First parameter needs to be the a valid panel name.");var c=Array.prototype.slice.call(arguments,0);c[0]="#layout_"+this.name+"_panel_"+a,w2utils.lock.apply(window,c)},unlock:function(a){if(-1==b.indexOf(a))return void console.log("ERROR: First parameter needs to be the a valid panel name.");var c="#layout_"+this.name+"_panel_"+a;w2utils.unlock(c)}},$.extend(a.prototype,w2utils.event),w2obj.layout=a}();var w2popup={};!function(){$.fn.w2popup=function(a,b){"undefined"==typeof a&&(b={},a="open"),$.isPlainObject(a)&&(b=a,a="open"),a=a.toLowerCase(),"load"===a&&"string"==typeof b&&(b=$.extend({url:b},arguments.length>2?arguments[2]:{})),"open"===a&&null!=b.url&&(a="load"),b=b||{};var c={};return $(this).length>0&&($(this).find("div[rel=title], div[rel=body], div[rel=buttons]").length>0?($(this).find("div[rel=title]").length>0&&(c.title=$(this).find("div[rel=title]").html()),$(this).find("div[rel=body]").length>0&&(c.body=$(this).find("div[rel=body]").html(),c.style=$(this).find("div[rel=body]")[0].style.cssText),$(this).find("div[rel=buttons]").length>0&&(c.buttons=$(this).find("div[rel=buttons]").html())):(c.title="&nbsp;",c.body=$(this).html()),0!=parseInt($(this).css("width"))&&(c.width=parseInt($(this).css("width"))),0!=parseInt($(this).css("height"))&&(c.height=parseInt($(this).css("height")))),w2popup[a]($.extend({},c,b))},w2popup={defaults:{title:"",body:"",buttons:"",style:"",color:"#000",opacity:.4,speed:.3,modal:!1,maximized:!1,keyboard:!0,width:500,height:300,showClose:!0,showMax:!1,transition:null},status:"closed",handlers:[],onOpen:null,onClose:null,onMax:null,onMin:null,onToggle:null,onKeydown:null,open:function(a){function b(a){return a||(a=window.event),window.addEventListener||window.document.attachEvent("onselectstart",function(){return!1}),w2popup.status="moving",q.resizing=!0,q.x=a.screenX,q.y=a.screenY,q.pos_x=$("#w2ui-popup").position().left,q.pos_y=$("#w2ui-popup").position().top,w2popup.lock({opacity:0}),$(document).on("mousemove",q.mvMove),$(document).on("mouseup",q.mvStop),a.stopPropagation?a.stopPropagation():a.cancelBubble=!0,a.preventDefault?void a.preventDefault():!1}function c(a){1==q.resizing&&(a||(a=window.event),q.div_x=a.screenX-q.x,q.div_y=a.screenY-q.y,$("#w2ui-popup").css({"-webkit-transition":"none","-webkit-transform":"translate3d("+q.div_x+"px, "+q.div_y+"px, 0px)","-moz-transition":"none","-moz-transform":"translate("+q.div_x+"px, "+q.div_y+"px)","-ms-transition":"none","-ms-transform":"translate("+q.div_x+"px, "+q.div_y+"px)","-o-transition":"none","-o-transform":"translate("+q.div_x+"px, "+q.div_y+"px)"}))}function d(a){1==q.resizing&&(a||(a=window.event),w2popup.status="open",q.div_x=a.screenX-q.x,q.div_y=a.screenY-q.y,$("#w2ui-popup").css({left:q.pos_x+q.div_x+"px",top:q.pos_y+q.div_y+"px","-webkit-transition":"none","-webkit-transform":"translate3d(0px, 0px, 0px)","-moz-transition":"none","-moz-transform":"translate(0px, 0px)","-ms-transition":"none","-ms-transform":"translate(0px, 0px)","-o-transition":"none","-o-transform":"translate(0px, 0px)"}),q.resizing=!1,$(document).off("mousemove",q.mvMove),$(document).off("mouseup",q.mvStop),w2popup.unlock())}var e=this;if("closing"==w2popup.status)return void setTimeout(function(){e.open.call(e,a)},100);var f=$("#w2ui-popup").data("options"),a=$.extend({},this.defaults,f,{title:"",body:"",buttons:""},a,{maximized:!1});if(setTimeout(function(){$("#w2ui-popup").data("options",a)},100),0==$("#w2ui-popup").length&&(w2popup.handlers=[],w2popup.onMax=null,w2popup.onMin=null,w2popup.onToggle=null,w2popup.onOpen=null,w2popup.onClose=null,w2popup.onKeydown=null),a.onOpen&&(w2popup.onOpen=a.onOpen),a.onClose&&(w2popup.onClose=a.onClose),a.onMax&&(w2popup.onMax=a.onMax),a.onMin&&(w2popup.onMin=a.onMin),a.onToggle&&(w2popup.onToggle=a.onToggle),a.onKeydown&&(w2popup.onKeydown=a.onKeydown),void 0==window.innerHeight){var g=document.documentElement.offsetWidth,h=document.documentElement.offsetHeight;"IE7"===w2utils.engine&&(g+=21,h+=4)}else var g=window.innerWidth,h=window.innerHeight;parseInt(g)-10<parseInt(a.width)&&(a.width=parseInt(g)-10),parseInt(h)-10<parseInt(a.height)&&(a.height=parseInt(h)-10);var i=parseInt((parseInt(h)-parseInt(a.height))/2*.6),j=parseInt((parseInt(g)-parseInt(a.width))/2);if(0==$("#w2ui-popup").length){var k=this.trigger({phase:"before",type:"open",target:"popup",options:a,present:!1});if(k.isCancelled===!0)return;w2popup.status="opening",w2popup.lockScreen(a);var l="";a.showClose&&(l+='<div class="w2ui-msg-button w2ui-msg-close" onmousedown="event.stopPropagation()" onclick="w2popup.close()">Close</div>'),a.showMax&&(l+='<div class="w2ui-msg-button w2ui-msg-max" onmousedown="event.stopPropagation()" onclick="w2popup.toggle()">Max</div>');var m='<div id="w2ui-popup" class="w2ui-popup" style="opacity: 0; left: '+j+"px; top: "+i+"px; width: "+parseInt(a.width)+"px; height: "+parseInt(a.height)+'px; -webkit-transform: scale(0.8); -moz-transform: scale(0.8); -ms-transform: scale(0.8); -o-transform: scale(0.8); "> <div class="w2ui-msg-title" style="'+(""==a.title?"display: none":"")+'">'+l+a.title+'</div> <div class="w2ui-box1" style="'+(""==a.title?"top: 0px !important;":"")+(""==a.buttons?"bottom: 0px !important;":"")+'"> <div class="w2ui-msg-body'+(""!=!a.title?" w2ui-msg-no-title":"")+(""!=!a.buttons?" w2ui-msg-no-buttons":"")+'" style="'+a.style+'">'+a.body+'</div> </div> <div class="w2ui-box2" style="'+(""==a.title?"top: 0px !important;":"")+(""==a.buttons?"bottom: 0px !important;":"")+'"> <div class="w2ui-msg-body'+(""!=!a.title?" w2ui-msg-no-title":"")+(""!=!a.buttons?" w2ui-msg-no-buttons":"")+'" style="'+a.style+'"></div> </div> <div class="w2ui-msg-buttons" style="'+(""==a.buttons?"display: none":"")+'">'+a.buttons+"</div></div>";$("body").append(m),setTimeout(function(){$("#w2ui-popup .w2ui-box2").hide(),$("#w2ui-popup").css({"-webkit-transition":a.speed+"s opacity, "+a.speed+"s -webkit-transform","-webkit-transform":"scale(1)","-moz-transition":a.speed+"s opacity, "+a.speed+"s -moz-transform","-moz-transform":"scale(1)","-ms-transition":a.speed+"s opacity, "+a.speed+"s -ms-transform","-ms-transform":"scale(1)","-o-transition":a.speed+"s opacity, "+a.speed+"s -o-transform","-o-transform":"scale(1)",opacity:"1"})},1),setTimeout(function(){$("#w2ui-popup").css({"-webkit-transform":"","-moz-transform":"","-ms-transform":"","-o-transform":""}),w2popup.status="open",setTimeout(function(){e.trigger($.extend(k,{phase:"after"}))},100)},1e3*a.speed)}else{var k=this.trigger({phase:"before",type:"open",target:"popup",options:a,present:!0});if(k.isCancelled===!0)return;w2popup.status="opening",("undefined"==typeof f||f.width!=a.width||f.height!=a.height)&&w2popup.resize(a.width,a.height),"undefined"!=typeof f&&(a.prevSize=a.width+":"+a.height,a.maximized=f.maximized);var n=$("#w2ui-popup .w2ui-box2 > .w2ui-msg-body").html(a.body);n.length>0&&(n[0].style.cssText=a.style),""!=a.buttons?($("#w2ui-popup .w2ui-msg-buttons").show().html(a.buttons),$("#w2ui-popup .w2ui-msg-body").removeClass("w2ui-msg-no-buttons"),$("#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2").css("bottom","")):($("#w2ui-popup .w2ui-msg-buttons").hide().html(""),$("#w2ui-popup .w2ui-msg-body").addClass("w2ui-msg-no-buttons"),$("#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2").css("bottom","0px")),""!=a.title?($("#w2ui-popup .w2ui-msg-title").show().html((a.showClose?'<div class="w2ui-msg-button w2ui-msg-close" onmousedown="event.stopPropagation()" onclick="w2popup.close()">Close</div>':"")+(a.showMax?'<div class="w2ui-msg-button w2ui-msg-max" onmousedown="event.stopPropagation()" onclick="w2popup.toggle()">Max</div>':"")+a.title),$("#w2ui-popup .w2ui-msg-body").removeClass("w2ui-msg-no-title"),$("#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2").css("top","")):($("#w2ui-popup .w2ui-msg-title").hide().html(""),$("#w2ui-popup .w2ui-msg-body").addClass("w2ui-msg-no-title"),$("#w2ui-popup .w2ui-box1, #w2ui-popup .w2ui-box2").css("top","0px"));var o=$("#w2ui-popup .w2ui-box1")[0],p=$("#w2ui-popup .w2ui-box2")[0];w2utils.transition(o,p,a.transition),p.className="w2ui-box1",o.className="w2ui-box2",$(p).addClass("w2ui-current-box"),$("#w2ui-popup").data("prev-size",null),setTimeout(function(){w2popup.status="open",e.trigger($.extend(k,{phase:"after"}))},100)}a._last_w2ui_name=w2utils.keyboard.active(),w2utils.keyboard.active(null),a.keyboard&&$(document).on("keydown",this.keydown);var q={resizing:!1,mvMove:c,mvStop:d};return $("#w2ui-popup .w2ui-msg-title").on("mousedown",function(a){b(a)}),this},keydown:function(a){var b=$("#w2ui-popup").data("options");if(b.keyboard){var c=w2popup.trigger({phase:"before",type:"keydown",target:"popup",options:b,originalEvent:a});if(c.isCancelled!==!0){switch(a.keyCode){case 27:a.preventDefault(),$("#w2ui-popup .w2ui-popup-message").length>0?w2popup.message():w2popup.close()}w2popup.trigger($.extend(c,{phase:"after"}))}}},close:function(a){var b=this,a=$.extend({},$("#w2ui-popup").data("options"),a);if(0!=$("#w2ui-popup").length){var c=this.trigger({phase:"before",type:"close",target:"popup",options:a});c.isCancelled!==!0&&(w2popup.status="closing",$("#w2ui-popup").css({"-webkit-transition":a.speed+"s opacity, "+a.speed+"s -webkit-transform","-webkit-transform":"scale(0.9)","-moz-transition":a.speed+"s opacity, "+a.speed+"s -moz-transform","-moz-transform":"scale(0.9)","-ms-transition":a.speed+"s opacity, "+a.speed+"s -ms-transform","-ms-transform":"scale(0.9)","-o-transition":a.speed+"s opacity, "+a.speed+"s -o-transform","-o-transform":"scale(0.9)",opacity:"0"}),w2popup.unlockScreen(a),setTimeout(function(){$("#w2ui-popup").remove(),w2popup.status="closed",b.trigger($.extend(c,{phase:"after"}))},1e3*a.speed),w2utils.keyboard.active(a._last_w2ui_name),a.keyboard&&$(document).off("keydown",this.keydown))}},toggle:function(){var a=this,b=$("#w2ui-popup").data("options"),c=this.trigger({phase:"before",type:"toggle",target:"popup",options:b});c.isCancelled!==!0&&(b.maximized===!0?w2popup.min():w2popup.max(),setTimeout(function(){a.trigger($.extend(c,{phase:"after"}))},1e3*b.speed+50))},max:function(){var a=this,b=$("#w2ui-popup").data("options");if(b.maximized!==!0){var c=this.trigger({phase:"before",type:"max",target:"popup",options:b});c.isCancelled!==!0&&(w2popup.status="resizing",b.prevSize=$("#w2ui-popup").css("width")+":"+$("#w2ui-popup").css("height"),w2popup.resize(1e4,1e4,function(){w2popup.status="open",b.maximized=!0,a.trigger($.extend(c,{phase:"after"}))}))}},min:function(){var a=this,b=$("#w2ui-popup").data("options");if(b.maximized===!0){var c=b.prevSize.split(":"),d=this.trigger({phase:"before",type:"min",target:"popup",options:b});d.isCancelled!==!0&&(w2popup.status="resizing",w2popup.resize(c[0],c[1],function(){w2popup.status="open",b.maximized=!1,b.prevSize=null,a.trigger($.extend(d,{phase:"after"}))}))}},get:function(){return $("#w2ui-popup").data("options")},set:function(a){w2popup.open(a)},clear:function(){$("#w2ui-popup .w2ui-msg-title").html(""),$("#w2ui-popup .w2ui-msg-body").html(""),$("#w2ui-popup .w2ui-msg-buttons").html("")},reset:function(){w2popup.open(w2popup.defaults)},load:function(a){function b(b,c){if(delete a.url,$("body").append('<div id="w2ui-tmp" style="display: none">'+b+"</div>"),"undefined"!=typeof c&&$("#w2ui-tmp #"+c).length>0?$("#w2ui-tmp #"+c).w2popup(a):$("#w2ui-tmp > div").w2popup(a),$("#w2ui-tmp > style").length>0){var d=$("<div>").append($("#w2ui-tmp > style").clone()).html();0==$("#w2ui-popup #div-style").length&&$("#w2ui-popup").append('<div id="div-style" style="position: absolute; left: -100; width: 1px"></div>'),$("#w2ui-popup #div-style").html(d)}$("#w2ui-tmp").remove()}if(w2popup.status="loading","undefined"==String(a.url))return void console.log("ERROR: The url parameter is empty.");var c=String(a.url).split("#"),d=c[0],e=c[1];"undefined"==String(a)&&(a={});var f=$("#w2ui-popup").data(d);"undefined"!=typeof f&&null!=f?b(f,e):$.get(d,function(a,c,f){b(f.responseText,e),$("#w2ui-popup").data(d,f.responseText)})},message:function(a){$().w2tag(),a||(a={width:200,height:100}),parseInt(a.width)<10&&(a.width=10),parseInt(a.height)<10&&(a.height=10),"undefined"==typeof a.hideOnClick&&(a.hideOnClick=!1);var b=$("#w2ui-popup").data("options")||{};("undefined"==typeof a.width||a.width>b.width-10)&&(a.width=b.width-10),("undefined"==typeof a.height||a.height>b.height-40)&&(a.height=b.height-40);var c=$("#w2ui-popup .w2ui-msg-title"),d=parseInt($("#w2ui-popup").width()),e=$("#w2ui-popup .w2ui-popup-message").length;if(""==$.trim(a.html)){$("#w2ui-popup #w2ui-message"+(e-1)).css("z-Index",250);var a=$("#w2ui-popup #w2ui-message"+(e-1)).data("options")||{};$("#w2ui-popup #w2ui-message"+(e-1)).remove(),"function"==typeof a.onClose&&a.onClose(),1==e?w2popup.unlock():$("#w2ui-popup #w2ui-message"+(e-2)).show()}else{$("#w2ui-popup .w2ui-popup-message").hide(),$("#w2ui-popup .w2ui-box1").before('<div id="w2ui-message'+e+'" class="w2ui-popup-message" style="display: none; '+(0==c.length?"top: 0px;":"top: "+w2utils.getSize(c,"height")+"px;")+("undefined"!=typeof a.width?"width: "+a.width+"px; left: "+(d-a.width)/2+"px;":"left: 10px; right: 10px;")+("undefined"!=typeof a.height?"height: "+a.height+"px;":"bottom: 6px;")+'-webkit-transition: .3s; -moz-transition: .3s; -ms-transition: .3s; -o-transition: .3s;"'+(a.hideOnClick===!0?'onclick="w2popup.message();"':"")+"></div>"),$("#w2ui-popup #w2ui-message"+e).data("options",a);var f=$("#w2ui-popup #w2ui-message"+e).css("display");$("#w2ui-popup #w2ui-message"+e).css({"-webkit-transform":"none"==f?"translateY(-"+a.height+"px)":"translateY(0px)","-moz-transform":"none"==f?"translateY(-"+a.height+"px)":"translateY(0px)","-ms-transform":"none"==f?"translateY(-"+a.height+"px)":"translateY(0px)","-o-transform":"none"==f?"translateY(-"+a.height+"px)":"translateY(0px)"}),"none"==f&&($("#w2ui-popup #w2ui-message"+e).show().html(a.html),setTimeout(function(){$("#w2ui-popup #w2ui-message"+e).css({"-webkit-transform":"none"==f?"translateY(0px)":"translateY(-"+a.height+"px)","-moz-transform":"none"==f?"translateY(0px)":"translateY(-"+a.height+"px)","-ms-transform":"none"==f?"translateY(0px)":"translateY(-"+a.height+"px)","-o-transform":"none"==f?"translateY(0px)":"translateY(-"+a.height+"px)"})},1),setTimeout(function(){$("#w2ui-popup #w2ui-message"+e).css({"-webkit-transition":"0s","-moz-transition":"0s","-ms-transition":"0s","-o-transition":"0s","z-Index":1500}),0==e&&w2popup.lock(),"function"==typeof a.onOpen&&a.onOpen()},300))}},lock:function(){var a=Array.prototype.slice.call(arguments,0);a.unshift($("#w2ui-popup")),w2utils.lock.apply(window,a)},unlock:function(){w2utils.unlock($("#w2ui-popup"))},lockScreen:function(a){return $("#w2ui-lock").length>0?!1:("undefined"==typeof a&&(a=$("#w2ui-popup").data("options")),"undefined"==typeof a&&(a={}),a=$.extend({},w2popup.defaults,a),$("body").append('<div id="w2ui-lock" onmousewheel="if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true; if (event.preventDefault) event.preventDefault(); else return false;" style="position: '+("IE5"==w2utils.engine?"absolute":"fixed")+"; z-Index: 1199; left: 0px; top: 0px; padding: 0px; margin: 0px; background-color: "+a.color+'; width: 100%; height: 100%; opacity: 0;"></div>'),setTimeout(function(){$("#w2ui-lock").css({"-webkit-transition":a.speed+"s opacity","-moz-transition":a.speed+"s opacity","-ms-transition":a.speed+"s opacity","-o-transition":a.speed+"s opacity",opacity:a.opacity})},1),1==a.modal?($("#w2ui-lock").on("mousedown",function(){$("#w2ui-lock").css({"-webkit-transition":".1s","-moz-transition":".1s","-ms-transition":".1s","-o-transition":".1s",opacity:"0.6"})}),$("#w2ui-lock").on("mouseup",function(){setTimeout(function(){$("#w2ui-lock").css({"-webkit-transition":".1s","-moz-transition":".1s","-ms-transition":".1s","-o-transition":".1s",opacity:a.opacity})},100)})):$("#w2ui-lock").on("mouseup",function(){w2popup.close()}),!0)},unlockScreen:function(a){return 0==$("#w2ui-lock").length?!1:("undefined"==typeof a&&(a=$("#w2ui-popup").data("options")),"undefined"==typeof a&&(a={}),a=$.extend({},w2popup.defaults,a),$("#w2ui-lock").css({"-webkit-transition":a.speed+"s opacity","-moz-transition":a.speed+"s opacity","-ms-transition":a.speed+"s opacity","-o-transition":a.speed+"s opacity",opacity:0}),setTimeout(function(){$("#w2ui-lock").remove()},1e3*a.speed),!0)},resize:function(a,b,c){var d=$("#w2ui-popup").data("options");parseInt($(window).width())-10<parseInt(a)&&(a=parseInt($(window).width())-10),parseInt($(window).height())-10<parseInt(b)&&(b=parseInt($(window).height())-10);var e=(parseInt($(window).height())-parseInt(b))/2*.8,f=(parseInt($(window).width())-parseInt(a))/2;$("#w2ui-popup").css({"-webkit-transition":d.speed+"s width, "+d.speed+"s height, "+d.speed+"s left, "+d.speed+"s top","-moz-transition":d.speed+"s width, "+d.speed+"s height, "+d.speed+"s left, "+d.speed+"s top","-ms-transition":d.speed+"s width, "+d.speed+"s height, "+d.speed+"s left, "+d.speed+"s top","-o-transition":d.speed+"s width, "+d.speed+"s height, "+d.speed+"s left, "+d.speed+"s top",top:e,left:f,width:a,height:b}),setTimeout(function(){d.width=a,d.height=b,"function"==typeof c&&c()},1e3*d.speed+50)}},$.extend(w2popup,w2utils.event)}();var w2alert=function(a,b,c){null==b&&(b=w2utils.lang("Notification")),$("#w2ui-popup").length>0&&"closing"!=w2popup.status?w2popup.message({width:400,height:170,html:'<div style="position: absolute; top: 0px; left: 0px; right: 0px; bottom: 45px; overflow: auto"> <div class="w2ui-centered" style="font-size: 13px;">'+a+'</div></div><div style="position: absolute; bottom: 7px; left: 0px; right: 0px; text-align: center; padding: 5px"> <button onclick="w2popup.message();" class="w2ui-popup-btn btn">'+w2utils.lang("Ok")+"</button></div>",onClose:function(){"function"==typeof c&&c()}}):w2popup.open({width:450,height:220,showMax:!1,showClose:!1,title:b,body:'<div class="w2ui-centered" style="font-size: 13px;">'+a+"</div>",buttons:'<button onclick="w2popup.close();" class="w2ui-popup-btn btn">'+w2utils.lang("Ok")+"</button>",onClose:function(){"function"==typeof c&&c()}})},w2confirm=function(a,b,c){var d={},e={msg:"",title:w2utils.lang("Confirmation"),width:$("#w2ui-popup").length>0?400:450,height:$("#w2ui-popup").length>0?170:220,yes_text:"Yes",yes_class:"",yes_style:"",yes_callBack:null,no_text:"No",no_class:"",no_style:"",no_callBack:null,callBack:null};return 1==arguments.length&&"object"==typeof a?$.extend(d,e,a):"function"==typeof b?$.extend(d,e,{msg:a,callBack:b}):$.extend(d,e,{msg:a,title:b,callBack:c}),$("#w2ui-popup").length>0&&"closing"!=w2popup.status?(d.width>w2popup.get().width&&(d.width=w2popup.get().width),d.height>w2popup.get().height-50&&(d.height=w2popup.get().height-50),w2popup.message({width:d.width,height:d.height,html:'<div style="position: absolute; top: 0px; left: 0px; right: 0px; bottom: 40px; overflow: auto"> <div class="w2ui-centered" style="font-size: 13px;">'+d.msg+'</div></div><div style="position: absolute; bottom: 7px; left: 0px; right: 0px; text-align: center; padding: 5px"> <button id="Yes" class="w2ui-popup-btn btn '+d.yes_class+'" style="'+d.yes_style+'">'+w2utils.lang(d.yes_text)+'</button> <button id="No" class="w2ui-popup-btn btn '+d.no_class+'" style="'+d.no_style+'">'+w2utils.lang(d.no_text)+"</button></div>",onOpen:function(){$("#w2ui-popup .w2ui-popup-message .btn").on("click",function(a){w2popup.message(),"function"==typeof d.callBack&&d.callBack(a.target.id),"Yes"==a.target.id&&"function"==typeof d.yes_callBack&&d.yes_callBack(),"No"==a.target.id&&"function"==typeof d.no_callBack&&d.no_callBack()})},onKeydown:function(a){switch(a.originalEvent.keyCode){case 13:"function"==typeof d.callBack&&d.callBack("Yes"),"function"==typeof d.yes_callBack&&d.yes_callBack(),w2popup.message();break;case 27:"function"==typeof d.callBack&&d.callBack("No"),"function"==typeof d.no_callBack&&d.no_callBack(),w2popup.message()}}})):(w2utils.isInt(d.height)||(d.height=d.height+50),w2popup.open({width:d.width,height:d.height,title:d.title,modal:!0,showClose:!1,body:'<div class="w2ui-centered" style="font-size: 13px;">'+d.msg+"</div>",buttons:'<button id="Yes" class="w2ui-popup-btn btn '+d.yes_class+'" style="'+d.yes_style+'">'+w2utils.lang(d.yes_text)+'</button><button id="No" class="w2ui-popup-btn btn '+d.no_class+'" style="'+d.no_style+'">'+w2utils.lang(d.no_text)+"</button>",onOpen:function(a){a.onComplete=function(){$("#w2ui-popup .w2ui-popup-btn").on("click",function(a){w2popup.close(),"function"==typeof d.callBack&&d.callBack(a.target.id),"Yes"==a.target.id&&"function"==typeof d.yes_callBack&&d.yes_callBack(),"No"==a.target.id&&"function"==typeof d.no_callBack&&d.no_callBack()})}},onKeydown:function(a){switch(a.originalEvent.keyCode){case 13:"function"==typeof d.callBack&&d.callBack("Yes"),"function"==typeof d.yes_callBack&&d.yes_callBack(),w2popup.close();break;case 27:"function"==typeof d.callBack&&d.callBack("No"),"function"==typeof d.no_callBack&&d.no_callBack(),w2popup.close()}}})),{yes:function(a){return d.yes_callBack=a,this},no:function(a){return d.no_callBack=a,this}}};!function(){var a=function(a){this.box=null,this.name=null,this.active=null,this.tabs=[],this.routeData={},this.right="",this.style="",this.onClick=null,this.onClose=null,this.onRender=null,this.onRefresh=null,this.onResize=null,this.onDestroy=null,$.extend(this,{handlers:[]}),$.extend(!0,this,w2obj.tabs,a)};$.fn.w2tabs=function(b){if("object"!=typeof b&&b){if(w2ui[$(this).attr("name")]){var c=w2ui[$(this).attr("name")];return c[b].apply(c,Array.prototype.slice.call(arguments,1)),this}return void console.log("ERROR: Method "+b+" does not exist on jQuery.w2tabs")}if(w2utils.checkName(b,"w2tabs")){for(var d=b.tabs||[],e=new a(b),f=0;f<d.length;f++)e.tabs[f]=$.extend({},a.prototype.tab,d[f]);return 0!==$(this).length&&e.render($(this)[0]),w2ui[e.name]=e,e}},a.prototype={tab:{id:null,text:"",route:null,hidden:!1,disabled:!1,closable:!1,hint:"",onClick:null,onRefresh:null,onClose:null},add:function(a){return this.insert(null,a)},insert:function(b,c){$.isArray(c)||(c=[c]);for(var d=0;d<c.length;d++){if("undefined"==typeof c[d].id)return void console.log('ERROR: The parameter "id" is required but not supplied. (obj: '+this.name+")");if(!w2utils.checkUniqueId(c[d].id,this.tabs,"tabs",this.name))return;var e=$.extend({},a.prototype.tab,c[d]);if(null===b||"undefined"==typeof b)this.tabs.push(e);else{var f=this.get(b,!0);this.tabs=this.tabs.slice(0,f).concat([e],this.tabs.slice(f))}this.refresh(c[d].id)}},remove:function(){for(var a=0,b=0;b<arguments.length;b++){var c=this.get(arguments[b]);if(!c)return!1;a++,this.tabs.splice(this.get(c.id,!0),1),$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(c.id)).remove()}return a},select:function(a){return this.active==a||null===this.get(a)?!1:(this.active=a,this.refresh(),!0)},set:function(a,b){var c=this.get(a,!0);return null===c?!1:($.extend(this.tabs[c],b),this.refresh(a),!0)},get:function(a,b){if(0===arguments.length){for(var c=[],d=0;d<this.tabs.length;d++)null!=this.tabs[d].id&&c.push(this.tabs[d].id);return c}for(var e=0;e<this.tabs.length;e++)if(this.tabs[e].id==a)return b===!0?e:this.tabs[e];return null},show:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&e.hidden!==!1&&(b++,e.hidden=!1,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},hide:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&e.hidden!==!0&&(b++,e.hidden=!0,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},enable:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&e.disabled!==!1&&(b++,e.disabled=!1,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},disable:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&e.disabled!==!0&&(b++,e.disabled=!0,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},refresh:function(a){var b=(new Date).getTime(),c=this.trigger({phase:"before",type:"refresh",target:"undefined"!=typeof a?a:this.name,object:this.get(a)});if(c.isCancelled!==!0){if("undefined"==typeof a)for(var d=0;d<this.tabs.length;d++)this.refresh(this.tabs[d].id);else{var e=this.get(a);if(null===e)return!1;"undefined"!=typeof e.caption&&(e.text=e.caption);var f=$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(e.id)),g=(e.closable?'<div class="w2ui-tab-close" onclick="w2ui[\''+this.name+"'].animateClose('"+e.id+"', event);\"></div>":"")+' <div class="w2ui-tab'+(this.active===e.id?" active":"")+(e.closable?" closable":"")+'" title="'+("undefined"!=typeof e.hint?e.hint:"")+'" onclick="w2ui[\''+this.name+"'].click('"+e.id+"', event);\">"+e.text+"</div>";
+if(0===f.length){var h="";e.hidden&&(h+="display: none;"),e.disabled&&(h+="opacity: 0.2; -moz-opacity: 0.2; -webkit-opacity: 0.2; -o-opacity: 0.2; filter:alpha(opacity=20);");var i='<td id="tabs_'+this.name+"_tab_"+e.id+'" style="'+h+'" valign="middle">'+g+"</td>";this.get(a,!0)!==this.tabs.length-1&&$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(this.tabs[parseInt(this.get(a,!0))+1].id)).length>0?$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(this.tabs[parseInt(this.get(a,!0))+1].id)).before(i):$(this.box).find("#tabs_"+this.name+"_right").before(i)}else f.html(g),e.hidden?f.css("display","none"):f.css("display",""),f.css(e.disabled?{opacity:"0.2","-moz-opacity":"0.2","-webkit-opacity":"0.2","-o-opacity":"0.2",filter:"alpha(opacity=20)"}:{opacity:"1","-moz-opacity":"1","-webkit-opacity":"1","-o-opacity":"1",filter:"alpha(opacity=100)"})}return $("#tabs_"+this.name+"_right").html(this.right),this.trigger($.extend(c,{phase:"after"})),(new Date).getTime()-b}},render:function(a){var b=(new Date).getTime(),c=this.trigger({phase:"before",type:"render",target:this.name,box:a});if(c.isCancelled!==!0){if("undefined"!=typeof a&&null!==a&&($(this.box).find("> table #tabs_"+this.name+"_right").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-tabs").html(""),this.box=a),!this.box)return!1;var d='<table cellspacing="0" cellpadding="1" width="100%"> <tr><td width="100%" id="tabs_'+this.name+'_right" align="right">'+this.right+"</td></tr></table>";return $(this.box).attr("name",this.name).addClass("w2ui-reset w2ui-tabs").html(d),$(this.box).length>0&&($(this.box)[0].style.cssText+=this.style),this.trigger($.extend(c,{phase:"after"})),this.refresh(),(new Date).getTime()-b}},resize:function(){var a=(new Date).getTime(),b=this.trigger({phase:"before",type:"resize",target:this.name});return b.isCancelled!==!0?(this.trigger($.extend(b,{phase:"after"})),(new Date).getTime()-a):void 0},destroy:function(){var a=this.trigger({phase:"before",type:"destroy",target:this.name});a.isCancelled!==!0&&($(this.box).find("> table #tabs_"+this.name+"_right").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-tabs").html(""),delete w2ui[this.name],this.trigger($.extend(a,{phase:"after"})))},click:function(a,b){var c=this.get(a);if(null===c||c.disabled)return!1;var d=this.trigger({phase:"before",type:"click",target:a,tab:c,object:c,originalEvent:b});if(d.isCancelled!==!0){if($(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(this.active)+" .w2ui-tab").removeClass("active"),this.active=c.id,c.route){var e=String("/"+c.route).replace(/\/{2,}/g,"/"),f=w2utils.parseRoute(e);if(f.keys.length>0)for(var g=0;g<f.keys.length;g++)null!=this.routeData[f.keys[g].name]&&(e=e.replace(new RegExp(":"+f.keys[g].name,"g"),this.routeData[f.keys[g].name]));setTimeout(function(){window.location.hash=e},1)}this.trigger($.extend(d,{phase:"after"})),this.refresh(a)}},animateClose:function(a,b){var c=this.get(a);if(null===c||c.disabled)return!1;var d=this.trigger({phase:"before",type:"close",target:a,object:this.get(a),originalEvent:b});if(d.isCancelled!==!0){var e=this;$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(c.id)).css({"-webkit-transition":".2s","-moz-transition":"2s","-ms-transition":".2s","-o-transition":".2s",opacity:"0"}),setTimeout(function(){var a=$(e.box).find("#tabs_"+e.name+"_tab_"+w2utils.escapeId(c.id)).width();$(e.box).find("#tabs_"+e.name+"_tab_"+w2utils.escapeId(c.id)).html('<div style="width: '+a+'px; -webkit-transition: .2s; -moz-transition: .2s; -ms-transition: .2s; -o-transition: .2s"></div>'),setTimeout(function(){$(e.box).find("#tabs_"+e.name+"_tab_"+w2utils.escapeId(c.id)).find(":first-child").css({width:"0px"})},50)},200),setTimeout(function(){e.remove(a)},450),this.trigger($.extend(d,{phase:"after"})),this.refresh()}},animateInsert:function(a,b){if(null!==this.get(a)&&$.isPlainObject(b)&&w2utils.checkUniqueId(b.id,this.tabs,"tabs",this.name)){var c=$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(b.id));if(0===c.length){"undefined"!=typeof b.caption&&(b.text=b.caption);var d='<div id="_tmp_tabs" class="w2ui-reset w2ui-tabs" style="position: absolute; top: -1000px;"><table cellspacing="0" cellpadding="1" width="100%"><tr><td id="_tmp_simple_tab" style="" valign="middle">'+(b.closable?'<div class="w2ui-tab-close"></div>':"")+' <div class="w2ui-tab '+(this.active===b.id?"active":"")+'">'+b.text+"</div></td></tr></table></div>";$("body").append(d);var e='<div style="width: 1px; -webkit-transition: 0.2s; -moz-transition: 0.2s; -ms-transition: 0.2s; -o-transition: 0.2s;">&nbsp;</div>',f="";b.hidden&&(f+="display: none;"),b.disabled&&(f+="opacity: 0.2; -moz-opacity: 0.2; -webkit-opacity: 0.2; -o-opacity: 0.2; filter:alpha(opacity=20);");var g='<td id="tabs_'+this.name+"_tab_"+b.id+'" style="'+f+'" valign="middle">'+e+"</td>";this.get(a,!0)!==this.tabs.length&&$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(this.tabs[parseInt(this.get(a,!0))].id)).length>0?$(this.box).find("#tabs_"+this.name+"_tab_"+w2utils.escapeId(this.tabs[parseInt(this.get(a,!0))].id)).before(g):$(this.box).find("#tabs_"+this.name+"_right").before(g);var h=this;setTimeout(function(){var a=$("#_tmp_simple_tab").width();$("#_tmp_tabs").remove(),$("#tabs_"+h.name+"_tab_"+w2utils.escapeId(b.id)+" > div").css("width",a+"px")},1),setTimeout(function(){h.insert(a,b)},200)}}}},$.extend(a.prototype,w2utils.event),w2obj.tabs=a}(),function(){var a=function(a){this.box=null,this.name=null,this.routeData={},this.items=[],this.right="",this.onClick=null,this.onRender=null,this.onRefresh=null,this.onResize=null,this.onDestroy=null,$.extend(!0,this,w2obj.toolbar,a)};$.fn.w2toolbar=function(b){if("object"==typeof b||!b){if(!w2utils.checkName(b,"w2toolbar"))return;var c=b.items||[],d=new a(b);$.extend(d,{items:[],handlers:[]});for(var e=0;e<c.length;e++)d.items[e]=$.extend({},a.prototype.item,c[e]);return 0!==$(this).length&&d.render($(this)[0]),w2ui[d.name]=d,d}if(w2ui[$(this).attr("name")]){var f=w2ui[$(this).attr("name")];return f[b].apply(f,Array.prototype.slice.call(arguments,1)),this}console.log("ERROR: Method "+b+" does not exist on jQuery.w2toolbar")},a.prototype={item:{id:null,type:"button",text:"",route:null,html:"",img:null,icon:null,count:null,hidden:!1,disabled:!1,checked:!1,arrow:!0,hint:"",group:null,items:null,overlay:{},onClick:null},add:function(a){this.insert(null,a)},insert:function(b,c){$.isArray(c)||(c=[c]);for(var d=0;d<c.length;d++){if("undefined"==typeof c[d].type)return void console.log('ERROR: The parameter "type" is required but not supplied in w2toolbar.add() method.');if(-1===$.inArray(String(c[d].type),["button","check","radio","drop","menu","break","html","spacer"]))return void console.log('ERROR: The parameter "type" should be one of the following [button, check, radio, drop, menu, break, html, spacer] in w2toolbar.add() method.');if("undefined"==typeof c[d].id)return void console.log('ERROR: The parameter "id" is required but not supplied in w2toolbar.add() method.');if(!w2utils.checkUniqueId(c[d].id,this.items,"toolbar items",this.name))return;var e=$.extend({},a.prototype.item,c[d]);if(null==b)this.items.push(e);else{var f=this.get(b,!0);this.items=this.items.slice(0,f).concat([e],this.items.slice(f))}this.refresh(e.id)}},remove:function(){for(var a=0,b=0;b<arguments.length;b++){var c=this.get(arguments[b]);if(c){a++,$(this.box).find("#tb_"+this.name+"_item_"+w2utils.escapeId(c.id)).remove();var d=this.get(c.id,!0);d&&this.items.splice(d,1)}}return a},set:function(a,b){var c=this.get(a,!0);return null===c?!1:($.extend(this.items[c],b),this.refresh(a),!0)},get:function(a,b){if(0===arguments.length){for(var c=[],d=0;d<this.items.length;d++)null!==this.items[d].id&&c.push(this.items[d].id);return c}for(var e=0;e<this.items.length;e++)if(this.items[e].id===a)return b===!0?e:this.items[e];return null},show:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&(b++,e.hidden=!1,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},hide:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&(b++,e.hidden=!0,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},enable:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&(b++,e.disabled=!1,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},disable:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&(b++,e.disabled=!0,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},check:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&(b++,e.checked=!0,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},uncheck:function(){for(var a=this,b=0,c=[],d=0;d<arguments.length;d++){var e=this.get(arguments[d]);e&&(b++,e.checked=!1,c.push(e.id))}return setTimeout(function(){for(var b in c)a.refresh(c[b])},15),b},render:function(a){var b=(new Date).getTime(),c=this.trigger({phase:"before",type:"render",target:this.name,box:a});if(c.isCancelled!==!0&&(null!=a&&($(this.box).find("> table #tb_"+this.name+"_right").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-toolbar").html(""),this.box=a),this.box)){for(var d='<table cellspacing="0" cellpadding="0" width="100%"><tr>',e=0;e<this.items.length;e++){var f=this.items[e];null==f.id&&(f.id="item_"+e),null!==f&&(d+="spacer"===f.type?'<td width="100%" id="tb_'+this.name+"_item_"+f.id+'" align="right"></td>':'<td id="tb_'+this.name+"_item_"+f.id+'" style="'+(f.hidden?"display: none":"")+'" class="'+(f.disabled?"disabled":"")+'" valign="middle">'+this.getItemHTML(f)+"</td>")}return d+='<td width="100%" id="tb_'+this.name+'_right" align="right">'+this.right+"</td>",d+="</tr></table>",$(this.box).attr("name",this.name).addClass("w2ui-reset w2ui-toolbar").html(d),$(this.box).length>0&&($(this.box)[0].style.cssText+=this.style),this.trigger($.extend(c,{phase:"after"})),(new Date).getTime()-b}},refresh:function(a){var b=(new Date).getTime(),c=this.trigger({phase:"before",type:"refresh",target:"undefined"!=typeof a?a:this.name,item:this.get(a)});if(c.isCancelled!==!0){if(null==a)for(var d=0;d<this.items.length;d++){var e=this.items[d];null==e.id&&(e.id="item_"+d),this.refresh(e.id)}var f=this.get(a);if(null===f)return!1;var g=$(this.box).find("#tb_"+this.name+"_item_"+w2utils.escapeId(f.id)),h=this.getItemHTML(f);return 0===g.length?(h="spacer"===f.type?'<td width="100%" id="tb_'+this.name+"_item_"+f.id+'" align="right"></td>':'<td id="tb_'+this.name+"_item_"+f.id+'" style="'+(f.hidden?"display: none":"")+'" class="'+(f.disabled?"disabled":"")+'" valign="middle">'+h+"</td>",this.get(a,!0)===this.items.length-1?$(this.box).find("#tb_"+this.name+"_right").before(h):$(this.box).find("#tb_"+this.name+"_item_"+w2utils.escapeId(this.items[parseInt(this.get(a,!0))+1].id)).before(h)):(g.html(h),f.hidden?g.css("display","none"):g.css("display",""),f.disabled?g.addClass("disabled"):g.removeClass("disabled")),this.trigger($.extend(c,{phase:"after"})),(new Date).getTime()-b}},resize:function(){var a=(new Date).getTime(),b=this.trigger({phase:"before",type:"resize",target:this.name});return b.isCancelled!==!0?(this.trigger($.extend(b,{phase:"after"})),(new Date).getTime()-a):void 0},destroy:function(){var a=this.trigger({phase:"before",type:"destroy",target:this.name});a.isCancelled!==!0&&($(this.box).find("> table #tb_"+this.name+"_right").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-toolbar").html(""),$(this.box).html(""),delete w2ui[this.name],this.trigger($.extend(a,{phase:"after"})))},getItemHTML:function(a){var b="";switch("undefined"!=typeof a.caption&&(a.text=a.caption),"undefined"==typeof a.hint&&(a.hint=""),"undefined"==typeof a.text&&(a.text=""),a.type){case"menu":case"button":case"check":case"radio":case"drop":var c="<td>&nbsp;</td>";a.img&&(c='<td><div class="w2ui-tb-image w2ui-icon '+a.img+'"></div></td>'),a.icon&&(c='<td><div class="w2ui-tb-image"><span class="'+a.icon+'"></span></div></td>'),b+='<table cellpadding="0" cellspacing="0" title="'+a.hint+'" class="w2ui-button '+(a.checked?"checked":"")+'" onclick = "var el=w2ui[\''+this.name+"']; if (el) el.click('"+a.id+'\', event);" onmouseover = "'+(a.disabled?"":"$(this).addClass('over');")+'" onmouseout = "'+(a.disabled?"":"$(this).removeClass('over').removeClass('down');")+'" onmousedown = "'+(a.disabled?"":"$(this).addClass('down');")+'" onmouseup = "'+(a.disabled?"":"$(this).removeClass('down');")+'"><tr><td> <table cellpadding="1" cellspacing="0"> <tr>'+c+(""!==a.text?'<td class="w2ui-tb-caption" nowrap>'+a.text+"</td>":"")+(null!=a.count?'<td class="w2ui-tb-count" nowrap><span>'+a.count+"</span></td>":"")+("drop"!==a.type&&"menu"!==a.type||a.arrow===!1?"":'<td class="w2ui-tb-down" nowrap><div></div></td>')+" </tr></table></td></tr></table>";break;case"break":b+='<table cellpadding="0" cellspacing="0"><tr> <td><div class="w2ui-break">&nbsp;</div></td></tr></table>';break;case"html":b+='<table cellpadding="0" cellspacing="0"><tr> <td nowrap>'+a.html+"</td></tr></table>"}var d="";return"function"==typeof a.onRender&&(d=a.onRender.call(this,a.id,b)),"function"==typeof this.onRender&&(d=this.onRender(a.id,b)),""!==d&&null!=d&&(b=d),b},menuClick:function(a){var b=this;if(a.item&&!a.item.disabled){var c=this.trigger({phase:"before",type:"click",target:a.item.id+":"+a.subItem.id,item:a.item,subItem:a.subItem,originalEvent:a.originalEvent});if(c.isCancelled===!0)return;var d=a.subItem;if(d.route){var e=String("/"+d.route).replace(/\/{2,}/g,"/"),f=w2utils.parseRoute(e);if(f.keys.length>0)for(var g=0;g<f.keys.length;g++)null!=b.routeData[f.keys[g].name]&&(e=e.replace(new RegExp(":"+f.keys[g].name,"g"),this.routeData[f.keys[g].name]));setTimeout(function(){window.location.hash=e},1)}this.trigger($.extend(c,{phase:"after"}))}},click:function(a,b){var c=this,d=this.get(a);if(d&&!d.disabled){var e=this.trigger({phase:"before",type:"click",target:"undefined"!=typeof a?a:this.name,item:d,object:d,originalEvent:b});if(e.isCancelled===!0)return;var f=$("#tb_"+this.name+"_item_"+w2utils.escapeId(d.id)+" table.w2ui-button");if(f.removeClass("down"),"radio"===d.type){for(var g=0;g<this.items.length;g++){var h=this.items[g];null!=h&&h.id!==d.id&&"radio"===h.type&&h.group===d.group&&h.checked&&(h.checked=!1,this.refresh(h.id))}d.checked=!0,f.addClass("checked")}if(("drop"===d.type||"menu"===d.type)&&(d.checked?d.checked=!1:setTimeout(function(){function a(){$(document).off("click",a),d.checked=!1,f.removeClass("checked")}var b=$("#tb_"+c.name+"_item_"+w2utils.escapeId(d.id));$.isPlainObject(d.overlay)||(d.overlay={});var e=(b.width()-50)/2;e>19&&(e=19),"drop"===d.type&&b.w2overlay(d.html,$.extend({left:e,top:3},d.overlay)),"menu"===d.type&&b.w2menu(d.items,$.extend({left:e,top:3},d.overlay,{select:function(b){c.menuClick({item:d,subItem:b.item,originalEvent:b.originalEvent}),a()}})),$(document).on("click",a)},1)),("check"===d.type||"drop"===d.type||"menu"===d.type)&&(d.checked=!d.checked,d.checked?f.addClass("checked"):f.removeClass("checked")),d.route){var i=String("/"+d.route).replace(/\/{2,}/g,"/"),j=w2utils.parseRoute(i);if(j.keys.length>0)for(var k=0;k<j.keys.length;k++)i=i.replace(new RegExp(":"+j.keys[k].name,"g"),this.routeData[j.keys[k].name]);setTimeout(function(){window.location.hash=i},1)}this.trigger($.extend(e,{phase:"after"}))}}},$.extend(a.prototype,w2utils.event),w2obj.toolbar=a}(),function(){var a=function(a){this.name=null,this.box=null,this.sidebar=null,this.parent=null,this.nodes=[],this.menu=[],this.routeData={},this.selected=null,this.img=null,this.icon=null,this.style="",this.topHTML="",this.bottomHTML="",this.keyboard=!0,this.onClick=null,this.onDblClick=null,this.onContextMenu=null,this.onMenuClick=null,this.onExpand=null,this.onCollapse=null,this.onKeydown=null,this.onRender=null,this.onRefresh=null,this.onResize=null,this.onDestroy=null,$.extend(!0,this,w2obj.sidebar,a)};$.fn.w2sidebar=function(b){if("object"==typeof b||!b){if(!w2utils.checkName(b,"w2sidebar"))return;var c=b.nodes,d=new a(b);return $.extend(d,{handlers:[],nodes:[]}),"undefined"!=typeof c&&d.add(d,c),0!==$(this).length&&d.render($(this)[0]),d.sidebar=d,w2ui[d.name]=d,d}if(w2ui[$(this).attr("name")]){var e=w2ui[$(this).attr("name")];return e[b].apply(e,Array.prototype.slice.call(arguments,1)),this}console.log("ERROR: Method "+b+" does not exist on jQuery.w2sidebar")},a.prototype={node:{id:null,text:"",count:null,img:null,icon:null,nodes:[],style:"",route:null,selected:!1,expanded:!1,hidden:!1,disabled:!1,group:!1,groupShowHide:!0,plus:!1,onClick:null,onDblClick:null,onContextMenu:null,onExpand:null,onCollapse:null,parent:null,sidebar:null},add:function(a,b){return 1==arguments.length&&(b=arguments[0],a=this),"string"==typeof a&&(a=this.get(a)),this.insert(a,null,b)},insert:function(b,c,d){var e,f,g,h,i;if(2==arguments.length){if(d=arguments[1],c=arguments[0],f=this.get(c),null===f)return $.isArray(d)||(d=[d]),e=null!=d[0].caption?d[0].caption:d[0].text,console.log('ERROR: Cannot insert node "'+e+'" because cannot find node "'+c+'" to insert before.'),null;b=this.get(c).parent}"string"==typeof b&&(b=this.get(b)),$.isArray(d)||(d=[d]);for(var j in d)if(h=d[j],null!=typeof h.id)if(null===this.get(this,h.id)){if(g=$.extend({},a.prototype.node,h),g.sidebar=this,g.parent=b,i=g.nodes||[],g.nodes=[],null===c)b.nodes.push(g);else{if(f=this.get(b,c,!0),null===f)return e=null!=h.caption?h.caption:h.text,console.log('ERROR: Cannot insert node "'+e+'" because cannot find node "'+c+'" to insert before.'),null;b.nodes.splice(f,0,g)}i.length>0&&this.insert(g,null,i)}else e=null!=h.caption?h.caption:h.text,console.log("ERROR: Cannot insert node with id="+h.id+" (text: "+e+") because another node with the same id already exists.");else e=null!=h.caption?h.caption:h.text,console.log('ERROR: Cannot insert node "'+e+'" because it has no id.');return this.refresh(b.id),g},remove:function(){for(var a,b=0,c=0;c<arguments.length;c++)if(a=this.get(arguments[c]),null!==a){null!==this.selected&&this.selected===a.id&&(this.selected=null);var d=this.get(a.parent,arguments[c],!0);null!==d&&(a.parent.nodes[d].selected&&a.sidebar.unselect(a.id),a.parent.nodes.splice(d,1),b++)}return b>0&&1==arguments.length?this.refresh(a.parent.id):this.refresh(),b},set:function(a,b,c){if(2==arguments.length&&(c=b,b=a,a=this),"string"==typeof a&&(a=this.get(a)),null==a.nodes)return null;for(var d=0;d<a.nodes.length;d++){if(a.nodes[d].id===b){var e=c.nodes;return $.extend(a.nodes[d],c,{nodes:[]}),null!=e&&this.add(a.nodes[d],e),this.refresh(b),!0}var f=this.set(a.nodes[d],b,c);if(f)return!0}return!1},get:function(a,b,c){if(0===arguments.length){for(var d=[],e=this.find({}),f=0;f<e.length;f++)null!=e[f].id&&d.push(e[f].id);return d}if((1==arguments.length||2==arguments.length&&b===!0)&&(c=b,b=a,a=this),"string"==typeof a&&(a=this.get(a)),null==a.nodes)return null;for(var g=0;g<a.nodes.length;g++){if(a.nodes[g].id==b)return c===!0?g:a.nodes[g];var h=this.get(a.nodes[g],b,c);if(h||0===h)return h}return null},find:function(a,b,c){if(1==arguments.length&&(b=a,a=this),c||(c=[]),"string"==typeof a&&(a=this.get(a)),null==a.nodes)return c;for(var d=0;d<a.nodes.length;d++){var e=!0;for(var f in b)a.nodes[d][f]!=b[f]&&(e=!1);e&&c.push(a.nodes[d]),a.nodes[d].nodes.length>0&&(c=this.find(a.nodes[d],b,c))}return c},hide:function(){for(var a=0,b=0;b<arguments.length;b++){var c=this.get(arguments[b]);null!==c&&(c.hidden=!0,a++)}return 1==arguments.length?this.refresh(arguments[0]):this.refresh(),a},show:function(){for(var a=0,b=0;b<arguments.length;b++){var c=this.get(arguments[b]);null!==c&&(c.hidden=!1,a++)}return 1==arguments.length?this.refresh(arguments[0]):this.refresh(),a},disable:function(){for(var a=0,b=0;b<arguments.length;b++){var c=this.get(arguments[b]);null!==c&&(c.disabled=!0,c.selected&&this.unselect(c.id),a++)}return 1==arguments.length?this.refresh(arguments[0]):this.refresh(),a},enable:function(){for(var a=0,b=0;b<arguments.length;b++){var c=this.get(arguments[b]);null!==c&&(c.disabled=!1,a++)}return 1==arguments.length?this.refresh(arguments[0]):this.refresh(),a},select:function(a){var b=this.get(a);return b?this.selected==a&&b.selected?!1:(this.unselect(this.selected),$(this.box).find("#node_"+w2utils.escapeId(a)).addClass("w2ui-selected").find(".w2ui-icon").addClass("w2ui-icon-selected"),b.selected=!0,this.selected=a,!0):!1},unselect:function(a){var b=this.get(a);return b?(b.selected=!1,$(this.box).find("#node_"+w2utils.escapeId(a)).removeClass("w2ui-selected").find(".w2ui-icon").removeClass("w2ui-icon-selected"),this.selected==a&&(this.selected=null),!0):!1},toggle:function(a){var b=this.get(a);return null===b?!1:b.plus?(this.set(a,{plus:!1}),this.expand(a),void this.refresh(a)):0===b.nodes.length?!1:this.get(a).expanded?this.collapse(a):this.expand(a)},collapse:function(a){var b=this,c=this.get(a),d=this.trigger({phase:"before",type:"collapse",target:a,object:c});return d.isCancelled!==!0?($(this.box).find("#node_"+w2utils.escapeId(a)+"_sub").slideUp(200),$(this.box).find("#node_"+w2utils.escapeId(a)+" .w2ui-node-dots:first-child").html('<div class="w2ui-expand">+</div>'),c.expanded=!1,this.trigger($.extend(d,{phase:"after"})),setTimeout(function(){b.refresh(a)},200),!0):void 0},collapseAll:function(a){if("undefined"==typeof a&&(a=this),"string"==typeof a&&(a=this.get(a)),null==a.nodes)return!1;for(var b=0;b<a.nodes.length;b++)a.nodes[b].expanded===!0&&(a.nodes[b].expanded=!1),a.nodes[b].nodes&&a.nodes[b].nodes.length>0&&this.collapseAll(a.nodes[b]);return this.refresh(a.id),!0},expand:function(a){var b=this,c=this.get(a),d=this.trigger({phase:"before",type:"expand",target:a,object:c});return d.isCancelled!==!0?($(this.box).find("#node_"+w2utils.escapeId(a)+"_sub").slideDown(200),$(this.box).find("#node_"+w2utils.escapeId(a)+" .w2ui-node-dots:first-child").html('<div class="w2ui-expand">-</div>'),c.expanded=!0,this.trigger($.extend(d,{phase:"after"})),setTimeout(function(){b.refresh(a)},200),!0):void 0},expandAll:function(a){if("undefined"==typeof a&&(a=this),"string"==typeof a&&(a=this.get(a)),null==a.nodes)return!1;for(var b=0;b<a.nodes.length;b++)a.nodes[b].expanded===!1&&(a.nodes[b].expanded=!0),a.nodes[b].nodes&&a.nodes[b].nodes.length>0&&this.collapseAll(a.nodes[b]);this.refresh(a.id)},expandParents:function(a){var b=this.get(a);return null===b?!1:(b.parent&&(b.parent.expanded=!0,this.expandParents(b.parent.id)),this.refresh(a),!0)},click:function(a,b){var c=this,d=this.get(a);if(null!==d&&!d.disabled&&!d.group){$(c.box).find(".w2ui-node.w2ui-selected").each(function(a,b){var d=$(b).attr("id").replace("node_",""),e=c.get(d);null!=e&&(e.selected=!1),$(b).removeClass("w2ui-selected").find(".w2ui-icon").removeClass("w2ui-icon-selected")});var e=$(c.box).find("#node_"+w2utils.escapeId(a)),f=$(c.box).find("#node_"+w2utils.escapeId(c.selected));e.addClass("w2ui-selected").find(".w2ui-icon").addClass("w2ui-icon-selected"),setTimeout(function(){var g=c.trigger({phase:"before",type:"click",target:a,originalEvent:b,node:d,object:d});if(g.isCancelled===!0)return e.removeClass("w2ui-selected").find(".w2ui-icon").removeClass("w2ui-icon-selected"),void f.addClass("w2ui-selected").find(".w2ui-icon").addClass("w2ui-icon-selected");if(null!==f&&(f.selected=!1),c.get(a).selected=!0,c.selected=a,d.route){var h=String("/"+d.route).replace(/\/{2,}/g,"/"),i=w2utils.parseRoute(h);if(i.keys.length>0)for(var j=0;j<i.keys.length;j++)null!=c.routeData[i.keys[j].name]&&(h=h.replace(new RegExp(":"+i.keys[j].name,"g"),c.routeData[i.keys[j].name]));setTimeout(function(){window.location.hash=h},1)}c.trigger($.extend(g,{phase:"after"}))},1)}},keydown:function(a){function b(a,b){null===a||a.hidden||a.disabled||a.group||(g.click(a.id,b),setTimeout(function(){g.scrollIntoView()},50))}function c(a,b){for(a=b(a);null!==a&&(a.hidden||a.disabled)&&!a.group;)a=b(a);return a}function d(a,b){if(null===a)return null;var c=a.parent,e=g.get(a.id,!0),f=null;if(a.expanded&&a.nodes.length>0&&b!==!0){var h=a.nodes[0];f=h.hidden||h.disabled||h.group?d(h):h}else f=c&&e+1<c.nodes.length?c.nodes[e+1]:d(c,!0);return null!==f&&(f.hidden||f.disabled||f.group)&&(f=d(f)),f}function e(a){if(null===a)return null;var b=a.parent,c=g.get(a.id,!0),d=c>0?f(b.nodes[c-1]):b;return null!==d&&(d.hidden||d.disabled||d.group)&&(d=e(d)),d}function f(a){if(a.expanded&&a.nodes.length>0){var b=a.nodes[a.nodes.length-1];return b.hidden||b.disabled||b.group?e(b):f(b)}return a}var g=this,h=g.get(g.selected);if(h&&g.keyboard===!0){var i=g.trigger({phase:"before",type:"keydown",target:g.name,originalEvent:a});i.isCancelled!==!0&&((13==a.keyCode||32==a.keyCode)&&h.nodes.length>0&&g.toggle(g.selected),37==a.keyCode&&(h.nodes.length>0&&h.expanded?g.collapse(g.selected):(b(h.parent),h.parent.group||g.collapse(h.parent.id))),39==a.keyCode&&(h.nodes.length>0||h.plus)&&!h.expanded&&g.expand(g.selected),38==a.keyCode&&b(c(h,e)),40==a.keyCode&&b(c(h,d)),-1!=$.inArray(a.keyCode,[13,32,37,38,39,40])&&(a.preventDefault&&a.preventDefault(),a.stopPropagation&&a.stopPropagation()),g.trigger($.extend(i,{phase:"after"})))}},scrollIntoView:function(a){"undefined"==typeof a&&(a=this.selected);var b=this.get(a);if(null!==b){var c=$(this.box).find(".w2ui-sidebar-div"),d=$(this.box).find("#node_"+w2utils.escapeId(a)),e=d.offset().top-c.offset().top;e+d.height()>c.height()&&c.animate({scrollTop:c.scrollTop()+c.height()/1.3},250,"linear"),0>=e&&c.animate({scrollTop:c.scrollTop()-c.height()/1.3},250,"linear")}},dblClick:function(a,b){var c=this.get(a),d=this.trigger({phase:"before",type:"dblClick",target:a,originalEvent:b,object:c});d.isCancelled!==!0&&(this.toggle(a),this.trigger($.extend(d,{phase:"after"})))},contextMenu:function(a,b){var c=this,d=c.get(a);a!=c.selected&&c.click(a),setTimeout(function(){var e=c.trigger({phase:"before",type:"contextMenu",target:a,originalEvent:b,object:d});e.isCancelled!==!0&&(d.group||d.disabled||(c.menu.length>0&&$(c.box).find("#node_"+w2utils.escapeId(a)).w2menu(c.menu,{left:(b?b.offsetX||b.pageX:50)-25,onSelect:function(b){c.menuClick(a,parseInt(b.index),b.originalEvent)}}),c.trigger($.extend(e,{phase:"after"}))))},150)},menuClick:function(a,b,c){var d=this,e=d.trigger({phase:"before",type:"menuClick",target:a,originalEvent:c,menuIndex:b,menuItem:d.menu[b]});e.isCancelled!==!0&&d.trigger($.extend(e,{phase:"after"}))},render:function(a){var b=(new Date).getTime(),c=this.trigger({phase:"before",type:"render",target:this.name,box:a});return c.isCancelled!==!0&&("undefined"!=typeof a&&null!==a&&($(this.box).find("> div > div.w2ui-sidebar-div").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-sidebar").html(""),this.box=a),this.box)?($(this.box).attr("name",this.name).addClass("w2ui-reset w2ui-sidebar").html('<div><div class="w2ui-sidebar-top"></div><div class="w2ui-sidebar-div"></div><div class="w2ui-sidebar-bottom"></div></div>'),$(this.box).find("> div").css({width:$(this.box).width()+"px",height:$(this.box).height()+"px"}),$(this.box).length>0&&($(this.box)[0].style.cssText+=this.style),""!==this.topHTML&&($(this.box).find(".w2ui-sidebar-top").html(this.topHTML),$(this.box).find(".w2ui-sidebar-div").css("top",$(this.box).find(".w2ui-sidebar-top").height()+"px")),""!==this.bottomHTML&&($(this.box).find(".w2ui-sidebar-bottom").html(this.bottomHTML),$(this.box).find(".w2ui-sidebar-div").css("bottom",$(this.box).find(".w2ui-sidebar-bottom").height()+"px")),this.trigger($.extend(c,{phase:"after"})),this.refresh(),(new Date).getTime()-b):void 0},refresh:function(a){function b(a){var b="",c=a.img;null===c&&(c=this.img);var d=a.icon;null===d&&(d=this.icon);for(var e=a.parent,f=0;e&&null!==e.parent;)e.group&&f--,e=e.parent,f++;return"undefined"!=typeof a.caption&&(a.text=a.caption),a.group?b='<div class="w2ui-node-group" id="node_'+a.id+'" onclick="w2ui[\''+h.name+"'].toggle('"+a.id+"')\" onmouseout=\"$(this).find('span:nth-child(1)').css('color', 'transparent')\" onmouseover=\"$(this).find('span:nth-child(1)').css('color', 'inherit')\">"+(a.groupShowHide?"<span>"+w2utils.lang(!a.hidden&&a.expanded?"Hide":"Show")+"</span>":"<span></span>")+" <span>"+a.text+'</span></div><div class="w2ui-node-sub" id="node_'+a.id+'_sub" style="'+a.style+";"+(!a.hidden&&a.expanded?"":"display: none;")+'"></div>':(a.selected&&!a.disabled&&(h.selected=a.id),e="",c&&(e='<div class="w2ui-node-image w2ui-icon '+c+(a.selected&&!a.disabled?" w2ui-icon-selected":"")+'"></div>'),d&&(e='<div class="w2ui-node-image"><span class="'+d+'"></span></div>'),b='<div class="w2ui-node '+(a.selected?"w2ui-selected":"")+" "+(a.disabled?"w2ui-disabled":"")+'" id="node_'+a.id+'" style="'+(a.hidden?"display: none;":"")+'" ondblclick="w2ui[\''+h.name+"'].dblClick('"+a.id+"', event);\" oncontextmenu=\"w2ui['"+h.name+"'].contextMenu('"+a.id+"', event); if (event.preventDefault) event.preventDefault();\" onClick=\"w2ui['"+h.name+"'].click('"+a.id+'\', event); "><table cellpadding="0" cellspacing="0" style="margin-left:'+18*f+"px; padding-right:"+18*f+'px"><tr><td class="w2ui-node-dots" nowrap onclick="w2ui[\''+h.name+"'].toggle('"+a.id+'\'); if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;"> <div class="w2ui-expand">'+(a.nodes.length>0?a.expanded?"-":"+":a.plus?"+":"")+'</div></td><td class="w2ui-node-data" nowrap>'+e+(a.count||0===a.count?'<div class="w2ui-node-count">'+a.count+"</div>":"")+'<div class="w2ui-node-caption">'+a.text+'</div></td></tr></table></div><div class="w2ui-node-sub" id="node_'+a.id+'_sub" style="'+a.style+";"+(!a.hidden&&a.expanded?"":"display: none;")+'"></div>'),b}var c=(new Date).getTime(),d=this.trigger({phase:"before",type:"refresh",target:"undefined"!=typeof a?a:this.name});if(d.isCancelled!==!0){""!==this.topHTML&&($(this.box).find(".w2ui-sidebar-top").html(this.topHTML),$(this.box).find(".w2ui-sidebar-div").css("top",$(this.box).find(".w2ui-sidebar-top").height()+"px")),""!==this.bottomHTML&&($(this.box).find(".w2ui-sidebar-bottom").html(this.bottomHTML),$(this.box).find(".w2ui-sidebar-div").css("bottom",$(this.box).find(".w2ui-sidebar-bottom").height()+"px")),$(this.box).find("> div").css({width:$(this.box).width()+"px",height:$(this.box).height()+"px"});var e,f,g,h=this;if("undefined"==typeof a)e=this,g=".w2ui-sidebar-div";else{if(e=this.get(a),null===e)return;g="#node_"+w2utils.escapeId(e.id)+"_sub"}var i;if(e!==this){var j="#node_"+w2utils.escapeId(e.id);i=b(e),$(this.box).find(j).before('<div id="sidebar_'+this.name+'_tmp"></div>'),$(this.box).find(j).remove(),$(this.box).find(g).remove(),$("#sidebar_"+this.name+"_tmp").before(i),$("#sidebar_"+this.name+"_tmp").remove()}$(this.box).find(g).html("");for(var k=0;k<e.nodes.length;k++)f=e.nodes[k],i=b(f),$(this.box).find(g).append(i),0!==f.nodes.length&&this.refresh(f.id);return this.trigger($.extend(d,{phase:"after"})),(new Date).getTime()-c}},resize:function(){var a=(new Date).getTime(),b=this.trigger({phase:"before",type:"resize",target:this.name});return b.isCancelled!==!0?($(this.box).css("overflow","hidden"),$(this.box).find("> div").css({width:$(this.box).width()+"px",height:$(this.box).height()+"px"}),this.trigger($.extend(b,{phase:"after"})),(new Date).getTime()-a):void 0},destroy:function(){var a=this.trigger({phase:"before",type:"destroy",target:this.name});a.isCancelled!==!0&&($(this.box).find("> div > div.w2ui-sidebar-div").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-sidebar").html(""),delete w2ui[this.name],this.trigger($.extend(a,{phase:"after"})))},lock:function(){var a=$(this.box).find("> div:first-child"),b=Array.prototype.slice.call(arguments,0);b.unshift(a),w2utils.lock.apply(window,b)},unlock:function(){w2utils.unlock(this.box)
+}},$.extend(a.prototype,w2utils.event),w2obj.sidebar=a}(),function(a){var b=function(b){this.el=null,this.helpers={},this.type=b.type||"text",this.options=a.extend(!0,{},b),this.onSearch=b.onSearch||null,this.onRequest=b.onRequest||null,this.onLoad=b.onLoad||null,this.onError=b.onError||null,this.onClick=b.onClick||null,this.onAdd=b.onAdd||null,this.onNew=b.onNew||null,this.onRemove=b.onRemove||null,this.onMouseOver=b.onMouseOver||null,this.onMouseOut=b.onMouseOut||null,this.onIconClick=b.onIconClick||null,this.tmp={},delete this.options.type,delete this.options.onSearch,delete this.options.onRequest,delete this.options.onLoad,delete this.options.onError,delete this.options.onClick,delete this.options.onMouseOver,delete this.options.onMouseOut,delete this.options.onIconClick,a.extend(!0,this,w2obj.field)};a.fn.w2field=function(c,d){if(0!=this.length)return"string"==typeof c&&"object"==typeof d&&(c=a.extend(!0,{},d,{type:c})),"string"==typeof c&&"undefined"==typeof d&&(c={type:c}),c.type=String(c.type).toLowerCase(),this.each(function(d,e){var f=a(e).data("w2field");if("undefined"==typeof f){var f=new b(c);return a.extend(f,{handlers:[]}),e&&(f.el=a(e)[0]),f.init(),a(e).data("w2field",f),f}if(f.clear(),"clear"!=c.type){var f=new b(c);return a.extend(f,{handlers:[]}),e&&(f.el=a(e)[0]),f.init(),a(e).data("w2field",f),f}});var e=b.prototype;return e[c]?e[c].apply(e,Array.prototype.slice.call(arguments,1)):void 0},b.prototype={custom:{},pallete:[["000000","444444","666666","999999","CCCCCC","EEEEEE","F3F3F3","FFFFFF"],["FF011B","FF9838","FFFD59","01FD55","00FFFE","0424F3","9B24F4","FF21F5"],["F4CCCC","FCE5CD","FFF2CC","D9EAD3","D0E0E3","CFE2F3","D9D1E9","EAD1DC"],["EA9899","F9CB9C","FEE599","B6D7A8","A2C4C9","9FC5E8","B4A7D6","D5A6BD"],["E06666","F6B26B","FED966","93C47D","76A5AF","6FA8DC","8E7CC3","C27BA0"],["CC0814","E69138","F1C232","6AA84F","45818E","3D85C6","674EA7","A54D79"],["99050C","B45F17","BF901F","37761D","124F5C","0A5394","351C75","741B47"],["660205","783F0B","7F6011","274E12","0C343D","063762","20124D","4C1030"]],addType:function(a,b){return a=String(a).toLowerCase(),this.custom[a]=b,!0},removeType:function(a){return a=String(a).toLowerCase(),this.custom[a]?(delete this.custom[a],!0):!1},init:function(){var b,c=this,d=this.options;if("function"==typeof this.custom[this.type])return void this.custom[this.type].call(this,d);if(-1==["INPUT","TEXTAREA"].indexOf(this.el.tagName))return void console.log("ERROR: w2field could only be applied to INPUT or TEXTAREA.",this.el);switch(this.type){case"text":case"int":case"float":case"money":case"currency":case"percent":case"alphanumeric":case"hex":b={min:null,max:null,step:1,placeholder:"",autoFormat:!0,currencyPrefix:w2utils.settings.currencyPrefix,currencySuffix:w2utils.settings.currencySuffix,currencyPrecision:w2utils.settings.currencyPrecision,groupSymbol:w2utils.settings.groupSymbol,arrows:!1,keyboard:!0,precision:null,silent:!0,prefix:"",suffix:""},this.options=a.extend(!0,{},b,d),d=this.options,d.numberRE=new RegExp("["+d.groupSymbol+"]","g"),d.moneyRE=new RegExp("["+d.currencyPrefix+d.currencySuffix+d.groupSymbol+"]","g"),d.percentRE=new RegExp("["+d.groupSymbol+"%]","g"),-1!=["text","alphanumeric","hex"].indexOf(this.type)&&(d.arrows=!1,d.keyboard=!1),this.addPrefix(),this.addSuffix(),a(this.el).attr("placeholder",d.placeholder);break;case"color":b={prefix:"#",suffix:'<div style="width: '+(parseInt(a(this.el).css("font-size"))||12)+'px">&nbsp;</div>',placeholder:"",arrows:!1,keyboard:!1},a.extend(d,b),this.addPrefix(),this.addSuffix(),a(this.el).attr("maxlength",6),""!=a(this.el).val()&&setTimeout(function(){a(c.el).change()},1),a(this.el).attr("placeholder",d.placeholder);break;case"date":b={format:w2utils.settings.date_format,placeholder:"",keyboard:!0,silent:!0,start:"",end:"",blocked:{},colored:{}},this.options=a.extend(!0,{},b,d),d=this.options,a(this.el).attr("placeholder",d.placeholder?d.placeholder:d.format);break;case"time":b={format:w2utils.settings.time_format,placeholder:"",keyboard:!0,silent:!0,start:"",end:""},this.options=a.extend(!0,{},b,d),d=this.options,a(this.el).attr("placeholder",d.placeholder?d.placeholder:"h12"==d.format?"hh:mi pm":"hh:mi");break;case"datetime":break;case"list":case"combo":if(b={items:[],selected:{},placeholder:"",url:null,postData:{},minLength:1,cacheMax:250,maxDropHeight:350,match:"begins",silent:!0,icon:null,iconStyle:"",onSearch:null,onRequest:null,onLoad:null,onError:null,onIconClick:null,renderDrop:null,prefix:"",suffix:"",openOnFocus:!1,markSearch:!1},d.items=this.normMenu(d.items),"list"==this.type&&(b.openOnFocus=!0,b.suffix='<div class="arrow-down" style="margin-top: '+(parseInt(a(this.el).height())-6)/2+'px;"></div>',a(this.el).addClass("w2ui-select"),!a.isPlainObject(d.selected)))for(var e in d.items){var f=d.items[e];if(f&&f.id==d.selected){d.selected=a.extend(!0,{},f);break}}d=a.extend({},b,d,{align:"both",altRows:!0}),this.options=d,a.isPlainObject(d.selected)||(d.selected={}),a(this.el).data("selected",d.selected),d.url&&this.request(0),"list"==this.type&&this.addFocus(),this.addPrefix(),this.addSuffix(),setTimeout(function(){c.refresh()},10),a(this.el).attr("placeholder",d.placeholder).attr("autocomplete","off"),"undefined"!=typeof d.selected.text&&a(this.el).val(d.selected.text);break;case"enum":b={items:[],selected:[],placeholder:"",max:0,url:null,postData:{},minLength:1,cacheMax:250,maxWidth:250,maxHeight:350,maxDropHeight:350,match:"contains",silent:!0,openOnFocus:!1,markSearch:!0,renderDrop:null,renderItem:null,style:"",onSearch:null,onRequest:null,onLoad:null,onError:null,onClick:null,onAdd:null,onNew:null,onRemove:null,onMouseOver:null,onMouseOut:null},d=a.extend({},b,d,{align:"both",suffix:"",altRows:!0}),d.items=this.normMenu(d.items),d.selected=this.normMenu(d.selected),this.options=d,a.isArray(d.selected)||(d.selected=[]),a(this.el).data("selected",d.selected),d.url&&this.request(0),this.addSuffix(),this.addMulti();break;case"file":b={selected:[],placeholder:w2utils.lang("Attach files by dragging and dropping or Click to Select"),max:0,maxSize:0,maxFileSize:0,maxWidth:250,maxHeight:350,maxDropHeight:350,silent:!0,renderItem:null,style:"",onClick:null,onAdd:null,onRemove:null,onMouseOver:null,onMouseOut:null},d=a.extend({},b,d,{align:"both",altRows:!0}),this.options=d,a.isArray(d.selected)||(d.selected=[]),a(this.el).data("selected",d.selected),this.addMulti()}this.tmp={onChange:function(a){c.change.call(c,a)},onClick:function(a){c.click.call(c,a)},onFocus:function(a){c.focus.call(c,a)},onBlur:function(a){c.blur.call(c,a)},onKeydown:function(a){c.keyDown.call(c,a)},onKeyup:function(a){c.keyUp.call(c,a)},onKeypress:function(a){c.keyPress.call(c,a)}},a(this.el).addClass("w2field").data("w2field",this).on("change",this.tmp.onChange).on("click",this.tmp.onClick).on("focus",this.tmp.onFocus).on("blur",this.tmp.onBlur).on("keydown",this.tmp.onKeydown).on("keyup",this.tmp.onKeyup).on("keypress",this.tmp.onKeypress).css({"box-sizing":"border-box","-webkit-box-sizing":"border-box","-moz-box-sizing":"border-box","-ms-box-sizing":"border-box","-o-box-sizing":"border-box"}),this.change(a.Event("change"))},clear:function(){var b=this.options;-1!=["money","currency"].indexOf(this.type)&&a(this.el).val(a(this.el).val().replace(b.moneyRE,"")),"percent"==this.type&&a(this.el).val(a(this.el).val().replace(/%/g,"")),"color"==this.type&&a(this.el).removeAttr("maxlength"),"list"==this.type&&a(this.el).removeClass("w2ui-select"),-1!=["date","time"].indexOf(this.type)&&a(this.el).attr("placeholder")==b.format&&a(this.el).attr("placeholder",""),this.type="clear";var c=a(this.el).data("tmp");if(this.tmp){"undefined"!=typeof c&&(c&&c["old-padding-left"]&&a(this.el).css("padding-left",c["old-padding-left"]),c&&c["old-padding-right"]&&a(this.el).css("padding-right",c["old-padding-right"])),a(this.el).val(this.clean(a(this.el).val())).removeClass("w2field").removeData().off("change",this.tmp.onChange).off("click",this.tmp.onClick).off("focus",this.tmp.onFocus).off("blur",this.tmp.onBlur).off("keydown",this.tmp.onKeydown).off("keyup",this.tmp.onKeyup).off("keypress",this.tmp.onKeypress);for(var d in this.helpers)a(this.helpers[d]).remove();this.helpers={}}},refresh:function(){var b=this,c=this.options,d=a(this.el).data("selected"),e=(new Date).getTime();if(-1!=["list"].indexOf(this.type)&&(a(b.el).parent().css("white-space","nowrap"),b.helpers.prefix&&b.helpers.prefix.hide(),setTimeout(function(){if(b.helpers.focus){!a.isEmptyObject(d)&&c.icon?(c.prefix='<span class="w2ui-icon '+c.icon+'"style="cursor: pointer; font-size: 14px; display: inline-block; margin-top: -1px; color: #7F98AD;'+c.iconStyle+'"></span>',b.addPrefix()):(c.prefix="",b.addPrefix());var e=b.helpers.focus.find("input");""==a(e).val()?(a(e).css("opacity",0).prev().css("opacity",0),a(b.el).val(d&&null!=d.text?d.text:""),a(b.el).attr("placeholder",a(b.el).attr("_placeholder"))):(a(e).css("opacity",1).prev().css("opacity",1),a(b.el).val(""),a(b.el).attr("_placeholder",a(b.el).attr("placeholder")).removeAttr("placeholder"),setTimeout(function(){b.helpers.prefix&&b.helpers.prefix.hide();var d="position: absolute; opacity: 0; margin: 4px 0px 0px 2px; background-position: left !important;";c.icon?(a(e).css("margin-left","17px"),a(b.helpers.focus).find(".icon-search").attr("style",d+"width: 11px !important; opacity: 1")):(a(e).css("margin-left","0px"),a(b.helpers.focus).find(".icon-search").attr("style",d+"width: 0px !important; opacity: 0"))},1))}},1)),-1!=["enum","file"].indexOf(this.type)){var f="";for(var g in d){var h=d[g],i="";i="function"==typeof c.renderItem?c.renderItem(h,g,'<div class="w2ui-list-remove" title="'+w2utils.lang("Remove")+'" index="'+g+'">&nbsp;&nbsp;</div>'):'<div class="w2ui-list-remove" title="'+w2utils.lang("Remove")+'" index="'+g+'">&nbsp;&nbsp;</div>'+("enum"==b.type?h.text:h.name+'<span class="file-size"> - '+w2utils.size(h.size)+"</span>"),f+='<li index="'+g+'" style="max-width: '+parseInt(c.maxWidth)+"px; "+(h.style?h.style:"")+'">'+i+"</li>"}var j=b.helpers.multi,k=j.find("ul");if(j.attr("style",j.attr("style")+";"+c.style),a(b.el).attr("readonly")?j.addClass("w2ui-readonly"):j.removeClass("w2ui-readonly"),j.find(".w2ui-enum-placeholder").remove(),k.find("li").not("li.nomouse").remove(),""!=f)k.prepend(f);else if("undefined"!=typeof c.placeholder){var l="padding-top: "+a(this.el).css("padding-top")+";padding-left: "+a(this.el).css("padding-left")+"; box-sizing: "+a(this.el).css("box-sizing")+"; line-height: "+a(this.el).css("line-height")+"; font-size: "+a(this.el).css("font-size")+"; font-family: "+a(this.el).css("font-family")+"; ";j.prepend('<div class="w2ui-enum-placeholder" style="'+l+'">'+c.placeholder+"</div>")}j.find("li").data("mouse","out").on("click",function(c){var e=d[a(c.target).attr("index")];if(!a(c.target).hasClass("nomouse")){c.stopPropagation();var f=b.trigger({phase:"before",type:"click",target:b.el,originalEvent:c.originalEvent,item:e});if(f.isCancelled!==!0){if(a(c.target).hasClass("w2ui-list-remove")){if(a(b.el).attr("readonly"))return;var f=b.trigger({phase:"before",type:"remove",target:b.el,originalEvent:c.originalEvent,item:e});if(f.isCancelled===!0)return;a().w2overlay(),d.splice(a(c.target).attr("index"),1),a(b.el).trigger("change"),a(c.target).parent().fadeOut("fast"),setTimeout(function(){b.refresh(),b.trigger(a.extend(f,{phase:"after"}))},300)}if("file"==b.type&&!a(c.target).hasClass("w2ui-list-remove")){var g="";/image/i.test(e.type)&&(g='<div style="padding: 3px;"> <img src="'+(e.content?"data:"+e.type+";base64,"+e.content:"")+'" style="max-width: 300px;" onload="var w = $(this).width(); var h = $(this).height(); if (w < 300 & h < 300) return; if (w >= h && w > 300) $(this).width(300); if (w < h && h > 300) $(this).height(300);" onerror="this.style.display = \'none\'" ></div>');var h='style="padding: 3px; text-align: right; color: #777;"',i='style="padding: 3px"';g+='<div style="padding: 8px;"> <table cellpadding="2"> <tr><td '+h+">Name:</td><td "+i+">"+e.name+"</td></tr> <tr><td "+h+">Size:</td><td "+i+">"+w2utils.size(e.size)+"</td></tr> <tr><td "+h+">Type:</td><td "+i+'> <span style="width: 200px; display: block-inline; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">'+e.type+"</span> </td></tr> <tr><td "+h+">Modified:</td><td "+i+">"+w2utils.date(e.modified)+"</td></tr> </table></div>",a(c.target).w2overlay(g)}b.trigger(a.extend(f,{phase:"after"}))}}}).on("mouseover",function(c){var e=c.target;if("LI"!=e.tagName&&(e=e.parentNode),!a(e).hasClass("nomouse")){if("out"==a(e).data("mouse")){var f=d[a(e).attr("index")],g=b.trigger({phase:"before",type:"mouseOver",target:b.el,originalEvent:c.originalEvent,item:f});if(g.isCancelled===!0)return;b.trigger(a.extend(g,{phase:"after"}))}a(e).data("mouse","over")}}).on("mouseout",function(c){var e=c.target;"LI"!=e.tagName&&(e=e.parentNode),a(e).hasClass("nomouse")||(a(e).data("mouse","leaving"),setTimeout(function(){if("leaving"==a(e).data("mouse")){a(e).data("mouse","out");var f=d[a(e).attr("index")],g=b.trigger({phase:"before",type:"f",target:b.el,originalEvent:c.originalEvent,item:f});if(g.isCancelled===!0)return;b.trigger(a.extend(g,{phase:"after"}))}},0))}),a(this.el).height("auto");var m=a(j).find("> div").height()+2*w2utils.getSize(j,"+height");26>m&&(m=26),m>c.maxHeight&&(m=c.maxHeight),j.length>0&&(j[0].scrollTop=1e3);var n=w2utils.getSize(a(this.el),"height")-2;n>m&&(m=n),a(j).css({height:m+"px",overflow:m==c.maxHeight?"auto":"hidden"}),m<c.maxHeight&&a(j).prop("scrollTop",0),a(this.el).css({height:m+2+"px"})}return(new Date).getTime()-e},reset:function(){var a=this.type;this.clear(),this.type=a,this.init()},clean:function(b){var c=this.options;return b=String(b).trim(),-1!=["int","float","money","currency","percent"].indexOf(this.type)&&(c.autoFormat&&-1!=["money","currency"].indexOf(this.type)&&(b=String(b).replace(c.moneyRE,"")),c.autoFormat&&"percent"==this.type&&(b=String(b).replace(c.percentRE,"")),c.autoFormat&&-1!=["int","float"].indexOf(this.type)&&(b=String(b).replace(c.numberRE,"")),parseFloat(b)==b&&(null!==c.min&&b<c.min&&(b=c.min,a(this.el).val(c.min)),null!==c.max&&b>c.max&&(b=c.max,a(this.el).val(c.max))),b=""!==b&&w2utils.isFloat(b)?Number(b):""),b},format:function(a){var b=this.options;if(b.autoFormat&&""!=a)switch(this.type){case"money":case"currency":a=w2utils.formatNumber(Number(a).toFixed(b.currencyPrecision),b.groupSymbol),""!=a&&(a=b.currencyPrefix+a+b.currencySuffix);break;case"percent":a=w2utils.formatNumber(b.precision?Number(a).toFixed(b.precision):a,b.groupSymbol),""!=a&&(a+="%");break;case"float":a=w2utils.formatNumber(b.precision?Number(a).toFixed(b.precision):a,b.groupSymbol);break;case"int":a=w2utils.formatNumber(a,b.groupSymbol)}return a},change:function(b){{var c=this;c.options}if(-1!=["int","float","money","currency","percent"].indexOf(this.type)){var d=a(this.el).val(),e=this.format(this.clean(a(this.el).val()));if(""!=d&&d!=e)return a(this.el).val(e).change(),b.stopPropagation(),b.preventDefault(),!1}if("color"==this.type){var f="#"+a(this.el).val();6!=a(this.el).val().length&&3!=a(this.el).val().length&&(f=""),a(this.el).next().find("div").css("background-color",f),a(c.el).is(":focus")&&this.updateOverlay()}},click:function(b){b.stopPropagation(),-1!=["list","combo","enum"].indexOf(this.type)&&(a(this.el).is(":focus")||this.focus(b)),-1!=["date","time","color"].indexOf(this.type)&&this.updateOverlay()},focus:function(){{var b=this;this.options}if(-1!==["color","date","time"].indexOf(b.type)){if(a(b.el).attr("readonly"))return;a("#w2ui-overlay").length>0&&a("#w2ui-overlay")[0].hide(),setTimeout(function(){b.updateOverlay()},150)}if(-1!=["list","combo","enum"].indexOf(b.type)){if(a(b.el).attr("readonly"))return;a("#w2ui-overlay").length>0&&a("#w2ui-overlay")[0].hide(),setTimeout(function(){return"list"==b.type&&a(b.el).is(":focus")?void a(b.helpers.focus).find("input").focus():(b.search(),void setTimeout(function(){b.updateOverlay()},1))},1)}"file"==b.type&&a(b.helpers.multi).css({outline:"auto 5px #7DB4F3","outline-offset":"-2px"})},blur:function(){var b=this,c=b.options,d=a(b.el).val().trim();-1!=["color","date","time","list","combo","enum"].indexOf(b.type)&&a("#w2ui-overlay").length>0&&a("#w2ui-overlay")[0].hide(),-1!=["int","float","money","currency","percent"].indexOf(b.type)&&(""===d||b.checkType(d)||(a(b.el).val("").change(),c.silent===!1&&(a(b.el).w2tag("Not a valid number"),setTimeout(function(){a(b.el).w2tag("")},3e3)))),-1!=["date","time"].indexOf(b.type)&&(w2utils.isInt(b.el.value)&&a(b.el).val(w2utils.formatDate(new Date(parseInt(b.el.value)),c.format)).change(),""===d||b.inRange(b.el.value)?("date"!=b.type||""===d||w2utils.isDate(b.el.value,c.format)||(a(b.el).val("").removeData("selected").change(),c.silent===!1&&(a(b.el).w2tag("Not a valid date"),setTimeout(function(){a(b.el).w2tag("")},3e3))),"time"!=b.type||""===d||w2utils.isTime(b.el.value)||(a(b.el).val("").removeData("selected").change(),c.silent===!1&&(a(b.el).w2tag("Not a valid time"),setTimeout(function(){a(b.el).w2tag("")},3e3)))):(a(b.el).val("").removeData("selected").change(),c.silent===!1&&(a(b.el).w2tag("Not in range"),setTimeout(function(){a(b.el).w2tag("")},3e3)))),"enum"==b.type&&a(b.helpers.multi).find("input").val("").width(20),"file"==b.type&&a(b.helpers.multi).css({outline:"none"})},keyPress:function(a){{var b=this;b.options}if(-1!=["int","float","money","currency","percent","hex","color","alphanumeric"].indexOf(b.type)){if(a.metaKey||a.ctrlKey||a.altKey||a.charCode!=a.keyCode&&a.keyCode>0)return;var c=String.fromCharCode(a.charCode);if(!b.checkType(c,!0)&&13!=a.keyCode)return a.preventDefault(),a.stopPropagation?a.stopPropagation():a.cancelBubble=!0,!1}-1!=["date","time"].indexOf(b.type)&&setTimeout(function(){b.updateOverlay()},1)},keyDown:function(b,c){var d=this,e=d.options,f=b.keyCode||c&&c.keyCode;if(-1!=["int","float","money","currency","percent"].indexOf(d.type)){if(!e.keyboard||a(d.el).attr("readonly"))return;var g=!1,h=parseFloat(a(d.el).val().replace(e.moneyRE,""))||0,i=e.step;switch((b.ctrlKey||b.metaKey)&&(i=10),f){case 38:if(b.shiftKey)break;a(d.el).val(h+i<=e.max||null===e.max?Number((h+i).toFixed(12)):e.max).change(),g=!0;break;case 40:if(b.shiftKey)break;a(d.el).val(h-i>=e.min||null===e.min?Number((h-i).toFixed(12)):e.min).change(),g=!0}g&&(b.preventDefault(),setTimeout(function(){d.el.setSelectionRange(d.el.value.length,d.el.value.length)},0))}if("date"==d.type){if(!e.keyboard||a(d.el).attr("readonly"))return;var g=!1,j=864e5,i=1;(b.ctrlKey||b.metaKey)&&(i=10);var k=w2utils.isDate(a(d.el).val(),e.format,!0);switch(k||(k=new Date,j=0),f){case 38:if(b.shiftKey)break;var l=w2utils.formatDate(k.getTime()+j,e.format);10==i&&(l=w2utils.formatDate(new Date(k.getFullYear(),k.getMonth()+1,k.getDate()),e.format)),a(d.el).val(l).change(),g=!0;break;case 40:if(b.shiftKey)break;var l=w2utils.formatDate(k.getTime()-j,e.format);10==i&&(l=w2utils.formatDate(new Date(k.getFullYear(),k.getMonth()-1,k.getDate()),e.format)),a(d.el).val(l).change(),g=!0}g&&(b.preventDefault(),setTimeout(function(){d.el.setSelectionRange(d.el.value.length,d.el.value.length),d.updateOverlay()},0))}if("time"==d.type){if(!e.keyboard||a(d.el).attr("readonly"))return;var g=!1,i=1;(b.ctrlKey||b.metaKey)&&(i=60),w2utils.isInt(d.el.value)&&a(d.el).val(w2utils.formatTime(new Date(parseInt(d.el.value)),e.format)).change();var h=a(d.el).val(),m=d.toMin(h)||d.toMin((new Date).getHours()+":"+((new Date).getMinutes()-1));switch(f){case 38:if(b.shiftKey)break;m+=i,g=!0;break;case 40:if(b.shiftKey)break;m-=i,g=!0}g&&(a(d.el).val(d.fromMin(m)).change(),b.preventDefault(),setTimeout(function(){d.el.setSelectionRange(d.el.value.length,d.el.value.length)},0))}if("color"==d.type){if(a(d.el).attr("readonly"))return;if(86==b.keyCode&&(b.ctrlKey||b.metaKey)&&(a(d.el).prop("maxlength",7),setTimeout(function(){var b=a(d).val();"#"==b.substr(0,1)&&(b=b.substr(1)),w2utils.isHex(b)||(b=""),a(d).val(b).prop("maxlength",6).change()},20)),(b.ctrlKey||b.metaKey)&&!b.shiftKey){if("undefined"==typeof d.tmp.cind1)d.tmp.cind1=-1,d.tmp.cind2=-1;else{switch(f){case 38:d.tmp.cind1--;break;case 40:d.tmp.cind1++;break;case 39:d.tmp.cind2++;break;case 37:d.tmp.cind2--}d.tmp.cind1<0&&(d.tmp.cind1=0),d.tmp.cind1>this.pallete.length-1&&(d.tmp.cind1=this.pallete.length-1),d.tmp.cind2<0&&(d.tmp.cind2=0),d.tmp.cind2>this.pallete[0].length-1&&(d.tmp.cind2=this.pallete[0].length-1)}-1!=[37,38,39,40].indexOf(f)&&(a(d.el).val(this.pallete[d.tmp.cind1][d.tmp.cind2]).change(),b.preventDefault())}}if(-1!=["list","combo","enum"].indexOf(d.type)){if(a(d.el).attr("readonly"))return;var g=!1,n=a(d.el).data("selected"),o=a(d.helpers.focus).find("input");switch("list"==d.type&&-1==[37,38,39,40].indexOf(f)&&d.refresh(),f){case 27:"list"==d.type&&(""!=a(o).val()&&a(o).val(""),b.stopPropagation());break;case 37:case 39:break;case 13:if(0==a("#w2ui-overlay").length)break;var p=e.items[e.index],q=a(d.helpers.multi).find("input");if("enum"==d.type)if(null!=p){var r=d.trigger({phase:"before",type:"add",target:d.el,originalEvent:b.originalEvent,item:p});if(r.isCancelled===!0)return;p=r.item,n.length>=e.max&&e.max>0&&n.pop(),delete p.hidden,delete d.tmp.force_open,n.push(p),a(d.el).change(),q.val("").width(20),d.refresh(),d.trigger(a.extend(r,{phase:"after"}))}else{p={id:q.val(),text:q.val()};var r=d.trigger({phase:"before",type:"new",target:d.el,originalEvent:b.originalEvent,item:p});if(r.isCancelled===!0)return;p=r.item,"function"==typeof d.onNew&&(n.length>=e.max&&e.max>0&&n.pop(),delete d.tmp.force_open,n.push(p),a(d.el).change(),q.val("").width(20),d.refresh()),d.trigger(a.extend(r,{phase:"after"}))}else p&&a(d.el).data("selected",p).val(p.text).change(),""==a(d.el).val()&&a(d.el).data("selected")&&a(d.el).removeData("selected").val("").change(),"list"==d.type&&(o.val(""),d.refresh()),d.tmp.force_hide=!0;break;case 8:case 46:if("enum"==d.type&&8==f&&""==a(d.helpers.multi).find("input").val()&&n.length>0){var p=n[n.length-1],r=d.trigger({phase:"before",type:"remove",target:d.el,originalEvent:b.originalEvent,item:p});if(r.isCancelled===!0)return;n.pop(),a(d.el).trigger("change"),d.refresh(),d.trigger(a.extend(r,{phase:"after"}))}"list"==d.type&&""==a(o).val()&&(a(d.el).data("selected",{}).change(),d.refresh());break;case 38:for(e.index=w2utils.isInt(e.index)?parseInt(e.index):0,e.index--;e.index>0&&e.items[e.index].hidden;)e.index--;if(0==e.index&&e.items[e.index].hidden)for(;e.items[e.index]&&e.items[e.index].hidden;)e.index++;g=!0;break;case 40:for(e.index=w2utils.isInt(e.index)?parseInt(e.index):-1,e.index++;e.index<e.items.length-1&&e.items[e.index].hidden;)e.index++;if(e.index==e.items.length-1&&e.items[e.index].hidden)for(;e.items[e.index]&&e.items[e.index].hidden;)e.index--;var s=d.el;-1!=["enum"].indexOf(d.type)&&(s=d.helpers.multi.find("input")),""==a(s).val()&&0==a("#w2ui-overlay").length?d.tmp.force_open=!0:g=!0}if(g)return e.index<0&&(e.index=0),e.index>=e.items.length&&(e.index=e.items.length-1),d.updateOverlay(),b.preventDefault(),void setTimeout(function(){if("enum"==d.type){var a=d.helpers.multi.find("input").get(0);a.setSelectionRange(a.value.length,a.value.length)}else if("list"==d.type){var a=d.helpers.focus.find("input").get(0);a.setSelectionRange(a.value.length,a.value.length)}else d.el.setSelectionRange(d.el.value.length,d.el.value.length)},0);if("enum"==d.type){var s=d.helpers.multi.find("input"),t=s.val();s.width(8*(t.length+2)+"px")}-1==[16,17,18,20,37,39,91].indexOf(f)&&setTimeout(function(){d.tmp.force_hide||d.request(),d.search()},1)}},keyUp:function(b){"color"==this.type&&86==b.keyCode&&(b.ctrlKey||b.metaKey)&&a(this).prop("maxlength",6)},clearCache:function(){var a=this.options;a.items=[],this.tmp.xhr_loading=!1,this.tmp.xhr_search="",this.tmp.xhr_total=-1,this.search()},request:function(b){var c=this,d=this.options,e=a(c.el).val()||"";if(d.url){if("enum"==c.type){var f=a(c.helpers.multi).find("input");e=0==f.length?"":f.val()}if("list"==c.type){var f=a(c.helpers.focus).find("input");e=0==f.length?"":f.val()}if(0!=d.minLength&&e.length<d.minLength)return d.items=[],void this.updateOverlay();"undefined"==typeof b&&(b=350),"undefined"==typeof c.tmp.xhr_search&&(c.tmp.xhr_search=""),"undefined"==typeof c.tmp.xhr_total&&(c.tmp.xhr_total=-1),d.url&&1!=a(c.el).prop("readonly")&&(0===d.items.length&&0!==c.tmp.xhr_total||c.tmp.xhr_total==d.cacheMax&&e.length>c.tmp.xhr_search.length||e.length>=c.tmp.xhr_search.length&&e.substr(0,c.tmp.xhr_search.length)!=c.tmp.xhr_search||e.length<c.tmp.xhr_search.length)&&(c.tmp.xhr_loading=!0,c.search(),clearTimeout(c.tmp.timeout),c.tmp.timeout=setTimeout(function(){var b=d.url,f={search:e,max:d.cacheMax};a.extend(f,d.postData);var g=c.trigger({phase:"before",type:"request",target:c.el,url:b,postData:f});if(g.isCancelled!==!0){b=g.url,f=g.postData,c.tmp.xhr&&c.tmp.xhr.abort();var h={type:"GET",url:b,data:f,dataType:"JSON"};"JSON"==w2utils.settings.dataType&&(h.type="POST",h.data=JSON.stringify(h.data),h.contentType="application/json"),c.tmp.xhr=a.ajax(h).done(function(b,g,h){var i=c.trigger({phase:"before",type:"load",target:c.el,search:f.search,data:b,xhr:h});if(i.isCancelled!==!0){if(b=i.data,"string"==typeof b&&(b=JSON.parse(b)),"success"!=b.status)return void console.log("ERROR: server did not return proper structure. It should return",{status:"success",items:[{id:1,text:"item"}]});b.items.length>d.cacheMax&&b.items.splice(d.cacheMax,1e5),c.tmp.xhr_loading=!1,c.tmp.xhr_search=e,c.tmp.xhr_total=b.items.length,d.items=b.items,c.tmp.emptySet=""==e&&0==b.items.length?!0:!1,c.search(),c.trigger(a.extend(i,{phase:"after"}))}}).fail(function(b,d,f){var g={status:d,error:f,rawResponseText:b.responseText},h=c.trigger({phase:"before",type:"error",target:c.el,search:e,error:g,xhr:b});if(h.isCancelled!==!0){if("abort"!=d){var i;try{i=a.parseJSON(b.responseText)}catch(j){}console.log("ERROR: Server communication failed.","\n EXPECTED:",{status:"success",items:[{id:1,text:"item"}]},"\n OR:",{status:"error",message:"error message"},"\n RECEIVED:","object"==typeof i?i:b.responseText)}c.clearCache(),c.trigger(a.extend(h,{phase:"after"}))}}),c.trigger(a.extend(g,{phase:"after"}))}},b))}},search:function(){var b=this,c=this.options,d=a(b.el).val(),e=b.el,f=[],g=a(b.el).data("selected");if("enum"==b.type){e=a(b.helpers.multi).find("input"),d=e.val();for(var h in g)g[h]&&f.push(g[h].id)}if("list"==b.type){e=a(b.helpers.focus).find("input"),d=e.val();for(var h in g)g[h]&&f.push(g[h].id)}var i=b.trigger({phase:"before",type:"search",target:e,search:d});if(i.isCancelled!==!0){if(b.tmp.xhr_loading!==!0){var j=0;for(var k in c.items){var l=c.items[k],m="",n="";-1!=["is","begins"].indexOf(c.match)&&(m="^"),-1!=["is","ends"].indexOf(c.match)&&(n="$");try{var o=new RegExp(m+d+n,"i");l.hidden=o.test(l.text)||"..."==l.text?!1:!0}catch(p){}"enum"==b.type&&-1!=a.inArray(l.id,f)&&(l.hidden=!0),l.hidden!==!0&&j++}if("combo"!=b.type)for(c.index=0;c.items[c.index]&&c.items[c.index].hidden;)c.index++;else c.index=-1;0>=j&&(c.index=-1),c.spinner=!1,b.updateOverlay(),setTimeout(function(){var b=a("#w2ui-overlay").html()||"";c.markSearch&&-1!=b.indexOf("$.fn.w2menuHandler")&&a("#w2ui-overlay").w2marker(d)},1)}else c.items.splice(0,c.cacheMax),c.spinner=!0,b.updateOverlay();b.trigger(a.extend(i,{phase:"after"}))}},updateOverlay:function(){var b=this,c=this.options;if("color"==this.type){if(a(b.el).attr("readonly"))return;0==a("#w2ui-overlay").length?a(b.el).w2overlay(b.getColorHTML()):a("#w2ui-overlay").html(b.getColorHTML()),a("#w2ui-overlay .color").on("mousedown",function(c){var d=a(c.originalEvent.target).attr("name"),e=a(c.originalEvent.target).attr("index").split(":");b.tmp.cind1=e[0],b.tmp.cind2=e[1],a(b.el).val(d).change(),a(this).html("&#149;")}).on("mouseup",function(){setTimeout(function(){a("#w2ui-overlay").length>0&&a("#w2ui-overlay").removeData("keepOpen")[0].hide()},10)})}if("date"==this.type){if(a(b.el).attr("readonly"))return;0==a("#w2ui-overlay").length&&a(b.el).w2overlay('<div class="w2ui-reset w2ui-calendar" onclick="event.stopPropagation();"></div>',{css:{"background-color":"#f5f5f5"}});var d,e,f=w2utils.isDate(a(b.el).val(),b.options.format,!0);f&&(d=f.getMonth()+1,e=f.getFullYear()),function k(c,d){a("#w2ui-overlay > div > div").html(b.getMonthHTML(c,d)),a("#w2ui-overlay .w2ui-calendar-title").on("mousedown",function(){if(a(this).next().hasClass("w2ui-calendar-jump"))a(this).next().remove();else{var c,d;a(this).after('<div class="w2ui-calendar-jump" style=""></div>'),a(this).next().hide().html(b.getYearHTML()).fadeIn(200),setTimeout(function(){a("#w2ui-overlay .w2ui-calendar-jump").find(".w2ui-jump-month, .w2ui-jump-year").on("click",function(){a(this).hasClass("w2ui-jump-month")&&(a(this).parent().find(".w2ui-jump-month").removeClass("selected"),a(this).addClass("selected"),d=a(this).attr("name")),a(this).hasClass("w2ui-jump-year")&&(a(this).parent().find(".w2ui-jump-year").removeClass("selected"),a(this).addClass("selected"),c=a(this).attr("name")),null!=c&&null!=d&&(a("#w2ui-overlay .w2ui-calendar-jump").fadeOut(100),setTimeout(function(){k(parseInt(d)+1,c)},100))}),a("#w2ui-overlay .w2ui-calendar-jump >:last-child").prop("scrollTop",2e3)},1)}}),a("#w2ui-overlay .w2ui-date").on("mousedown",function(){var c=a(this).attr("date");a(b.el).val(c).change(),a(this).css({"background-color":"#B6D5FB","border-color":"#aaa"})}).on("mouseup",function(){setTimeout(function(){a("#w2ui-overlay").length>0&&a("#w2ui-overlay").removeData("keepOpen")[0].hide()},10)}),a("#w2ui-overlay .previous").on("mousedown",function(){var a=b.options.current.split("/");a[0]=parseInt(a[0])-1,k(a[0],a[1])}),a("#w2ui-overlay .next").on("mousedown",function(){var a=b.options.current.split("/");a[0]=parseInt(a[0])+1,k(a[0],a[1])})}(d,e)}if("time"==this.type){if(a(b.el).attr("readonly"))return;0==a("#w2ui-overlay").length&&a(b.el).w2overlay('<div class="w2ui-reset w2ui-calendar-time" onclick="event.stopPropagation();"></div>',{css:{"background-color":"#fff"}});var g="h24"==this.options.format?!0:!1;a("#w2ui-overlay > div").html(b.getHourHTML()),a("#w2ui-overlay .w2ui-time").on("mousedown",function(){a(this).css({"background-color":"#B6D5FB","border-color":"#aaa"});var c=a(this).attr("hour");a(b.el).val((c>12&&!g?c-12:c)+":00"+(g?"":12>c?" am":" pm")).change()}).on("mouseup",function(){var c=a(this).attr("hour");a("#w2ui-overlay").length>0&&a("#w2ui-overlay")[0].hide(),a(b.el).w2overlay('<div class="w2ui-reset w2ui-calendar-time"></div>',{css:{"background-color":"#fff"}}),a("#w2ui-overlay > div").html(b.getMinHTML(c)),a("#w2ui-overlay .w2ui-time").on("mousedown",function(){a(this).css({"background-color":"#B6D5FB","border-color":"#aaa"});var d=a(this).attr("min");a(b.el).val((c>12&&!g?c-12:c)+":"+(10>d?0:"")+d+(g?"":12>c?" am":" pm")).change()}).on("mouseup",function(){setTimeout(function(){a("#w2ui-overlay").length>0&&a("#w2ui-overlay").removeData("keepOpen")[0].hide()},10)})})}if(-1!=["list","combo","enum"].indexOf(this.type)){var h=this.el,i=this.el;if("enum"==this.type&&(h=a(this.helpers.multi),i=a(h).find("input")),"list"==this.type&&(i=a(this.helpers.focus).find("input")),a(i).is(":focus")){if(c.openOnFocus===!1&&""==a(i).val()&&b.tmp.force_open!==!0)return void a().w2overlay();if(b.tmp.force_hide)return a().w2overlay(),void setTimeout(function(){delete b.tmp.force_hide},1);""!=a(i).val()&&delete b.tmp.force_open,0==a("#w2ui-overlay").length&&(c.index=0);var j=w2utils.lang("No matches");null!=c.url&&a(i).val().length<c.minLength&&b.tmp.emptySet!==!0&&(j=c.minLength+" "+w2utils.lang("letters or more...")),null!=c.url&&""==a(i).val()&&b.tmp.emptySet!==!0&&(j=w2utils.lang("Type to search....")),a(h).w2menu("refresh",a.extend(!0,{},c,{search:!1,render:c.renderDrop,maxHeight:c.maxDropHeight,msgNoItems:j,onSelect:function(d){if("enum"==b.type){var e=a(b.el).data("selected");if(d.item){var f=b.trigger({phase:"before",type:"add",target:b.el,originalEvent:d.originalEvent,item:d.item});
+if(f.isCancelled===!0)return;e.length>=c.max&&c.max>0&&e.pop(),delete d.item.hidden,e.push(d.item),a(b.el).data("selected",e).change(),a(b.helpers.multi).find("input").val("").width(20),b.refresh(),a("#w2ui-overlay").length>0&&a("#w2ui-overlay")[0].hide(),b.trigger(a.extend(f,{phase:"after"}))}}else a(b.el).data("selected",d.item).val(d.item.text).change(),b.helpers.focus&&(b.helpers.focus.find("input").val(""),b.refresh())}}))}}},inRange:function(b){var c=!1;if("date"==this.type){var d=w2utils.isDate(b,this.options.format,!0);if(d){if(this.options.start||this.options.end){var e="string"==typeof this.options.start?this.options.start:a(this.options.start).val(),f="string"==typeof this.options.end?this.options.end:a(this.options.end).val(),g=w2utils.isDate(e,this.options.format,!0),h=w2utils.isDate(f,this.options.format,!0),i=new Date(d);g||(g=i),h||(h=i),i>=g&&h>=i&&(c=!0)}else c=!0;this.options.blocked&&-1!=a.inArray(b,this.options.blocked)&&(c=!1)}}if("time"==this.type)if(this.options.start||this.options.end){var j=this.toMin(b),k=this.toMin(this.options.start),l=this.toMin(this.options.end);k||(k=j),l||(l=j),j>=k&&l>=j&&(c=!0)}else c=!0;return c},checkType:function(a,b){var c=this;switch(c.type){case"int":return b&&-1!=["-"].indexOf(a)?!0:w2utils.isInt(a.replace(c.options.numberRE,""));case"percent":a=a.replace(/%/g,"");case"float":return b&&-1!=["-","."].indexOf(a)?!0:w2utils.isFloat(a.replace(c.options.numberRE,""));case"money":case"currency":return b&&-1!=["-",".",c.options.groupSymbol,c.options.currencyPrefix,c.options.currencySuffix].indexOf(a)?!0:w2utils.isFloat(a.replace(c.options.moneyRE,""));case"hex":case"color":return w2utils.isHex(a);case"alphanumeric":return w2utils.isAlphaNumeric(a)}return!0},addPrefix:function(){var b=this;setTimeout(function(){if("clear"!==b.type){var c,d=a(b.el).data("tmp")||{};d["old-padding-left"]&&a(b.el).css("padding-left",d["old-padding-left"]),d["old-padding-left"]=a(b.el).css("padding-left"),a(b.el).data("tmp",d),b.helpers.prefix&&a(b.helpers.prefix).remove(),""!==b.options.prefix&&(a(b.el).before('<div class="w2ui-field-helper">'+b.options.prefix+"</div>"),c=a(b.el).prev(),c.css({color:a(b.el).css("color"),"font-family":a(b.el).css("font-family"),"font-size":a(b.el).css("font-size"),"padding-top":a(b.el).css("padding-top"),"padding-bottom":a(b.el).css("padding-bottom"),"padding-left":a(b.el).css("padding-left"),"padding-right":0,"margin-top":parseInt(a(b.el).css("margin-top"),10)+2+"px","margin-bottom":parseInt(a(b.el).css("margin-bottom"),10)+1+"px","margin-left":a(b.el).css("margin-left"),"margin-right":0}).on("click",function(){if(b.options.icon&&"function"==typeof b.onIconClick){var c=b.trigger({phase:"before",type:"iconClick",target:b.el,el:a(this).find("span.w2ui-icon")[0]});if(c.isCancelled===!0)return;b.trigger(a.extend(c,{phase:"after"}))}else"list"==b.type?a(b.helpers.focus).find("input").focus():a(b.el).focus()}),a(b.el).css("padding-left",c.width()+parseInt(a(b.el).css("padding-left"),10)+"px"),b.helpers.prefix=c)}},1)},addSuffix:function(){var b,c,d=this;setTimeout(function(){if("clear"!==d.type){var e=a(d.el).data("tmp")||{};if(e["old-padding-right"]&&a(d.el).css("padding-right",e["old-padding-right"]),e["old-padding-right"]=a(d.el).css("padding-right"),a(d.el).data("tmp",e),c=parseInt(a(d.el).css("padding-right"),10),d.options.arrows){d.helpers.arrows&&a(d.helpers.arrows).remove(),a(d.el).after('<div class="w2ui-field-helper" style="border: 1px solid transparent">&nbsp; <div class="w2ui-field-up" type="up"> <div class="arrow-up" type="up"></div> </div> <div class="w2ui-field-down" type="down"> <div class="arrow-down" type="down"></div> </div></div>');{w2utils.getSize(d.el,"height")}b=a(d.el).next(),b.css({color:a(d.el).css("color"),"font-family":a(d.el).css("font-family"),"font-size":a(d.el).css("font-size"),height:a(d.el).height()+parseInt(a(d.el).css("padding-top"),10)+parseInt(a(d.el).css("padding-bottom"),10)+"px",padding:0,"margin-top":parseInt(a(d.el).css("margin-top"),10)+1+"px","margin-bottom":0,"border-left":"1px solid silver"}).css("margin-left","-"+(b.width()+parseInt(a(d.el).css("margin-right"),10)+12)+"px").on("mousedown",function(b){function c(){clearTimeout(a("body").data("_field_update_timer")),a("body").off("mouseup",c)}function e(c){a(d.el).focus(),d.keyDown(a.Event("keydown"),{keyCode:"up"==a(b.target).attr("type")?38:40}),c!==!1&&a("body").data("_field_update_timer",setTimeout(e,60))}a("body").on("mouseup",c),a("body").data("_field_update_timer",setTimeout(e,700)),e(!1)}),c+=b.width()+12,a(d.el).css("padding-right",c+"px"),d.helpers.arrows=b}""!==d.options.suffix&&(d.helpers.suffix&&a(d.helpers.suffix).remove(),a(d.el).after('<div class="w2ui-field-helper">'+d.options.suffix+"</div>"),b=a(d.el).next(),b.css({color:a(d.el).css("color"),"font-family":a(d.el).css("font-family"),"font-size":a(d.el).css("font-size"),"padding-top":a(d.el).css("padding-top"),"padding-bottom":a(d.el).css("padding-bottom"),"padding-left":"3px","padding-right":a(d.el).css("padding-right"),"margin-top":parseInt(a(d.el).css("margin-top"),10)+2+"px","margin-bottom":parseInt(a(d.el).css("margin-bottom"),10)+1+"px"}).on("click",function(){"list"==d.type?a(d.helpers.focus).find("input").focus():a(d.el).focus()}),b.css("margin-left","-"+(w2utils.getSize(b,"width")+parseInt(a(d.el).css("margin-right"),10)+2)+"px"),c+=b.width()+3,a(d.el).css("padding-right",c+"px"),d.helpers.suffix=b)}},1)},addFocus:function(){var b=this,c=(this.options,0);a(b.helpers.focus).remove();var d='<div class="w2ui-field-helper"> <div class="w2ui-icon icon-search"></div> <input type="text" autocomplete="off"><div>';a(b.el).attr("tabindex",-1).before(d);var e=a(b.el).prev();b.helpers.focus=e,e.css({width:a(b.el).width(),"margin-top":a(b.el).css("margin-top"),"margin-left":parseInt(a(b.el).css("margin-left"))+parseInt(a(b.el).css("padding-left"))+"px","margin-bottom":a(b.el).css("margin-bottom"),"margin-right":a(b.el).css("margin-right")}).find("input").css({cursor:"default",width:"100%",outline:"none",opacity:1,margin:0,border:"1px solid transparent",padding:a(b.el).css("padding-top"),"padding-left":0,"margin-left":c>0?c+6:0,"background-color":"transparent"}),e.find("input").on("click",function(c){0==a("#w2ui-overlay").length&&b.focus(c),c.stopPropagation()}).on("focus",function(c){a(b.el).css({outline:"auto 5px #7DB4F3","outline-offset":"-2px"}),a(this).val(""),a(b.el).triggerHandler("focus"),c.stopPropagation?c.stopPropagation():c.cancelBubble=!0}).on("blur",function(c){a(b.el).css("outline","none"),a(this).val(""),b.refresh(),a(b.el).triggerHandler("blur"),c.stopPropagation?c.stopPropagation():c.cancelBubble=!0}).on("keyup",function(a){b.keyUp(a)}).on("keydown",function(a){b.keyDown(a)}).on("keypress",function(a){b.keyPress(a)}),e.on("click",function(){a(this).find("input").focus()}),b.refresh()},addMulti:function(){{var b=this;this.options}a(b.helpers.multi).remove();var c="",d="margin-top : 0px; margin-bottom : 0px; margin-left : "+a(b.el).css("margin-left")+"; margin-right : "+a(b.el).css("margin-right")+"; width : "+(w2utils.getSize(b.el,"width")-parseInt(a(b.el).css("margin-left"),10)-parseInt(a(b.el).css("margin-right"),10))+"px;";"enum"==b.type&&(c='<div class="w2ui-field-helper w2ui-list" style="'+d+'; box-sizing: border-box"> <div style="padding: 0px; margin: 0px; margin-right: 20px; display: inline-block"> <ul> <li style="padding-left: 0px; padding-right: 0px" class="nomouse"> <input type="text" style="width: 20px" autocomplete="off" '+(a(b.el).attr("readonly")?"readonly":"")+"> </li>"),"file"==b.type&&(c='<div class="w2ui-field-helper w2ui-list" style="'+d+'; box-sizing: border-box"> <div style="padding: 0px; margin: 0px; margin-right: 20px; display: inline-block"> <ul><li style="padding-left: 0px; padding-right: 0px" class="nomouse"></li></ul> <input class="file-input" type="file" name="attachment" multiple style="display: none" tabindex="-1">'),a(b.el).before(c).css({"background-color":"transparent","border-color":"transparent"});var e=a(b.el).prev();b.helpers.multi=e,"enum"==b.type&&(a(b.el).attr("tabindex",-1),e.find("input").on("click",function(c){0==a("#w2ui-overlay").length&&b.focus(c),a(b.el).triggerHandler("click")}).on("focus",function(c){a(e).css({outline:"auto 5px #7DB4F3","outline-offset":"-2px"}),a(b.el).triggerHandler("focus"),c.stopPropagation?c.stopPropagation():c.cancelBubble=!0}).on("blur",function(c){a(e).css("outline","none"),a(b.el).triggerHandler("blur"),c.stopPropagation?c.stopPropagation():c.cancelBubble=!0}).on("keyup",function(a){b.keyUp(a)}).on("keydown",function(a){b.keyDown(a)}).on("keypress",function(a){e.find(".w2ui-enum-placeholder").remove(),b.keyPress(a)}),e.on("click",function(){a(this).find("input").focus()})),"file"==b.type&&(a(b.el).css("outline","none"),e.on("click",function(c){a(b.el).focus(),a(b.el).attr("readonly")||(b.blur(c),e.find("input").click())}).on("dragenter",function(){a(b.el).attr("readonly")||a(e).addClass("w2ui-file-dragover")}).on("dragleave",function(c){if(!a(b.el).attr("readonly")){var d=a(c.target).parents(".w2ui-field-helper");0==d.length&&a(e).removeClass("w2ui-file-dragover")}}).on("drop",function(c){if(!a(b.el).attr("readonly")){a(e).removeClass("w2ui-file-dragover");for(var d=c.originalEvent.dataTransfer.files,f=0,g=d.length;g>f;f++)b.addFile.call(b,d[f]);c.preventDefault(),c.stopPropagation()}}).on("dragover",function(a){a.preventDefault(),a.stopPropagation()}),e.find("input").on("click",function(a){a.stopPropagation()}).on("change",function(){if("undefined"!=typeof this.files)for(var a=0,c=this.files.length;c>a;a++)b.addFile.call(b,this.files[a])})),b.refresh()},addFile:function(b){var c,d=this,e=this.options,f=a(d.el).data("selected"),g={name:b.name,type:b.type,modified:b.lastModifiedDate,size:b.size,content:null},h=0,i=0;for(var j in f)h+=f[j].size,i++;var k=d.trigger({phase:"before",type:"add",target:d.el,file:g,total:i,totalSize:h});if(k.isCancelled!==!0){if(0!==e.maxFileSize&&g.size>e.maxFileSize)return c="Maximum file size is "+w2utils.size(e.maxFileSize),e.silent===!1&&a(d.el).w2tag(c),void console.log("ERROR: "+c);if(0!==e.maxSize&&h+g.size>e.maxSize)return c="Maximum total size is "+w2utils.size(e.maxSize),e.silent===!1&&a(d.el).w2tag(c),void console.log("ERROR: "+c);if(0!==e.max&&i>=e.max)return c="Maximum number of files is "+e.max,e.silent===!1&&a(d.el).w2tag(c),void console.log("ERROR: "+c);if(f.push(g),"undefined"!=typeof FileReader){var l=new FileReader;l.onload=function(){return function(b){var c=b.target.result,e=c.indexOf(",");g.content=c.substr(e+1),d.refresh(),a(d.el).trigger("change"),d.trigger(a.extend(k,{phase:"after"}))}}(),l.readAsDataURL(b)}else d.refresh(),a(d.el).trigger("change")}},normMenu:function(b){if(a.isArray(b)){for(var c=0;c<b.length;c++)"string"==typeof b[c]?b[c]={id:b[c],text:b[c]}:("undefined"!=typeof b[c].text&&"undefined"==typeof b[c].id&&(b[c].id=b[c].text),"undefined"==typeof b[c].text&&"undefined"!=typeof b[c].id&&(b[c].text=b[c].id),"undefined"!=typeof b[c].caption&&(b[c].text=b[c].caption));return b}if("object"==typeof b){var d=[];for(var c in b)d.push({id:c,text:b[c]});return d}},getColorHTML:function(){for(var b='<div class="w2ui-color"><table cellspacing="5">',c=0;8>c;c++){b+="<tr>";for(var d=0;8>d;d++)b+='<td> <div class="color" style="background-color: #'+this.pallete[c][d]+';" name="'+this.pallete[c][d]+'" index="'+c+":"+d+'"> '+(a(this.el).val()==this.pallete[c][d]?"&#149;":"&nbsp;")+" </div></td>";b+="</tr>",2>c&&(b+='<tr><td style="height: 8px" colspan="8"></td></tr>')}return b+="</table></div>"},getMonthHTML:function(a,b){var c=new Date,d=w2utils.settings.fullmonths,e=(w2utils.settings.fulldays,["31","28","31","30","31","30","31","31","30","31","30","31"]),f=c.getFullYear()+"/"+(Number(c.getMonth())+1)+"/"+c.getDate();b=w2utils.isInt(b)?parseInt(b):c.getFullYear(),a=w2utils.isInt(a)?parseInt(a):c.getMonth()+1,a>12&&(a-=12,b++),(1>a||0===a)&&(a+=12,b--),e[1]=b/4==Math.floor(b/4)?"29":"28",this.options.current=a+"/"+b,c=new Date(b,a-1,1);for(var g=c.getDay(),h=w2utils.settings.shortdays,i="",j=0,k=h.length;k>j;j++)i+="<td>"+h[j]+"</td>";for(var l='<div class="w2ui-calendar-title title"> <div class="w2ui-calendar-previous previous"> <div></div> </div> <div class="w2ui-calendar-next next"> <div></div> </div> '+d[a-1]+", "+b+'</div><table class="w2ui-calendar-days" cellspacing="0"> <tr class="w2ui-day-title">'+i+"</tr> <tr>",m=1,n=1;43>n;n++){if(0===g&&1==n){for(var o=0;6>o;o++)l+='<td class="w2ui-day-empty">&nbsp;</td>';n+=6}else if(g>n||m>e[a-1]){l+='<td class="w2ui-day-empty">&nbsp;</td>',n%7===0&&(l+="</tr><tr>");continue}var p=b+"/"+a+"/"+m,q="";n%7==6&&(q=" w2ui-saturday"),n%7===0&&(q=" w2ui-sunday"),p==f&&(q+=" w2ui-today");var r=m,s="",t="",u=w2utils.formatDate(p,this.options.format);this.options.colored&&void 0!==this.options.colored[u]&&(tmp=this.options.colored[u].split(":"),t="background-color: "+tmp[0]+";",s="color: "+tmp[1]+";"),l+='<td class="'+(this.inRange(u)?"w2ui-date ":"w2ui-blocked")+q+'" style="'+s+t+'" date="'+u+'">'+r+"</td>",(n%7===0||0===g&&1==n)&&(l+="</tr><tr>"),m++}return l+="</tr></table>"},getYearHTML:function(){var a=w2utils.settings.shortmonths,b="",c="";for(var d in a)b+='<div class="w2ui-jump-month" name="'+d+'">'+a[d]+"</div>";for(var e=1950;2020>=e;e++)c+='<div class="w2ui-jump-year" name="'+e+'">'+e+"</div>";return"<div>"+b+"</div><div>"+c+"</div>"},getHourHTML:function(){for(var a=[],b="h24"==this.options.format?!0:!1,c=0;24>c;c++){var d=(c>=12&&!b?c-12:c)+":00"+(b?"":12>c?" am":" pm");12!=c||b||(d="12:00 pm"),a[Math.floor(c/8)]||(a[Math.floor(c/8)]="");var e=this.fromMin(this.toMin(d)),f=this.fromMin(this.toMin(d)+59);a[Math.floor(c/8)]+='<div class="'+(this.inRange(e)||this.inRange(f)?"w2ui-time ":"w2ui-blocked")+'" hour="'+c+'">'+d+"</div>"}var g='<div class="w2ui-calendar-time"><table><tr> <td>'+a[0]+"</td> <td>"+a[1]+"</td> <td>"+a[2]+"</td></tr></table></div>";return g},getMinHTML:function(a){"undefined"==typeof a&&(a=0);for(var b="h24"==this.options.format?!0:!1,c=[],d=0;60>d;d+=5){var e=(a>12&&!b?a-12:a)+":"+(10>d?0:"")+d+" "+(b?"":12>a?"am":"pm"),f=20>d?0:40>d?1:2;c[f]||(c[f]=""),c[f]+='<div class="'+(this.inRange(e)?"w2ui-time ":"w2ui-blocked")+'" min="'+d+'">'+e+"</div>"}var g='<div class="w2ui-calendar-time"><table><tr> <td>'+c[0]+"</td> <td>"+c[1]+"</td> <td>"+c[2]+"</td></tr></table></div>";return g},toMin:function(a){if("string"!=typeof a)return null;var b=a.split(":");return 2!=b.length?null:(b[0]=parseInt(b[0]),b[1]=parseInt(b[1]),-1!=a.indexOf("pm")&&12!=b[0]&&(b[0]+=12),60*b[0]+b[1])},fromMin:function(a){var b="";a>=1440&&(a%=1440),0>a&&(a=1440+a);var c=Math.floor(a/60),d=(10>a%60?"0":"")+a%60;return b=-1!=this.options.format.indexOf("h24")?c+":"+d:(12>=c?c:c-12)+":"+d+" "+(c>=12?"pm":"am")}},a.extend(b.prototype,w2utils.event),w2obj.field=b}(jQuery),function(){var w2form=function(a){this.name=null,this.header="",this.box=null,this.url="",this.routeData={},this.formURL="",this.formHTML="",this.page=0,this.recid=0,this.fields=[],this.actions={},this.record={},this.original={},this.postData={},this.toolbar={},this.tabs={},this.style="",this.focus=0,this.msgNotJSON=w2utils.lang("Return data is not in JSON format."),this.msgAJAXerror=w2utils.lang("AJAX error. See console for more details."),this.msgRefresh=w2utils.lang("Refreshing..."),this.msgSaving=w2utils.lang("Saving..."),this.onRequest=null,this.onLoad=null,this.onValidate=null,this.onSubmit=null,this.onSave=null,this.onChange=null,this.onRender=null,this.onRefresh=null,this.onResize=null,this.onDestroy=null,this.onAction=null,this.onToolbar=null,this.onError=null,this.isGenerated=!1,this.last={xhr:null},$.extend(!0,this,w2obj.form,a)};$.fn.w2form=function(a){if("object"==typeof a||!a){var b=this;if(!w2utils.checkName(a,"w2form"))return;var c=a.record,d=a.original,e=a.fields,f=a.toolbar,g=a.tabs,h=new w2form(a);if($.extend(h,{record:{},original:{},fields:[],tabs:{},toolbar:{},handlers:[]}),$.isArray(g)){$.extend(!0,h.tabs,{tabs:[]});for(var i in g){var j=g[i];h.tabs.tabs.push("object"==typeof j?j:{id:j,caption:j})}}else $.extend(!0,h.tabs,g);$.extend(!0,h.toolbar,f);for(var k in e){var l=$.extend(!0,{},e[k]);"undefined"==typeof l.name&&"undefined"!=typeof l.field&&(l.name=l.field),"undefined"==typeof l.field&&"undefined"!=typeof l.name&&(l.field=l.name),h.fields[k]=l}for(var k in c)h.record[k]=$.isPlainObject(c[k])?$.extend(!0,{},c[k]):c[k];for(var k in d)h.original[k]=$.isPlainObject(d[k])?$.extend(!0,{},d[k]):d[k];return b.length>0&&(h.box=b[0]),""!=h.formURL?$.get(h.formURL,function(a){h.formHTML=a,h.isGenerated=!0,(0!=$(h.box).length||0!=a.length)&&($(h.box).html(a),h.render(h.box))}):""!=h.formHTML||(h.formHTML=0!=$(this).length&&""!=$.trim($(this).html())?$(this).html():h.generateHTML()),w2ui[h.name]=h,""==h.formURL&&(-1==String(h.formHTML).indexOf("w2ui-page")&&(h.formHTML='<div class="w2ui-page page-0">'+h.formHTML+"</div>"),$(h.box).html(h.formHTML),h.isGenerated=!0,h.render(h.box)),h}if(w2ui[$(this).attr("name")]){var b=w2ui[$(this).attr("name")];return b[a].apply(b,Array.prototype.slice.call(arguments,1)),this}console.log("ERROR: Method "+a+" does not exist on jQuery.w2form")},w2form.prototype={get:function(a,b){if(0===arguments.length){var c=[];for(var d in this.fields)null!=this.fields[d].name&&c.push(this.fields[d].name);return c}for(var e in this.fields)if(this.fields[e].name==a)return b===!0?e:this.fields[e];return null},set:function(a,b){for(var c in this.fields)if(this.fields[c].name==a)return $.extend(this.fields[c],b),this.refresh(),!0;return!1},reload:function(a){var b="object"!=typeof this.url?this.url:this.url.get;b&&0!=this.recid?this.request(a):"function"==typeof a&&a()},clear:function(){this.recid=0,this.record={},$().w2tag(),this.refresh()},error:function(a){var b=this.trigger({target:this.name,type:"error",message:a,xhr:this.last.xhr});return b.isCancelled===!0?void("function"==typeof callBack&&callBack()):(setTimeout(function(){w2alert(a,"Error")},1),void this.trigger($.extend(b,{phase:"after"})))},validate:function(a){"undefined"==typeof a&&(a=!0),$().w2tag();var b=[];for(var c in this.fields){var d=this.fields[c];switch(null==this.record[d.name]&&(this.record[d.name]=""),d.type){case"int":this.record[d.name]&&!w2utils.isInt(this.record[d.name])&&b.push({field:d,error:w2utils.lang("Not an integer")});break;case"float":this.record[d.name]&&!w2utils.isFloat(this.record[d.name])&&b.push({field:d,error:w2utils.lang("Not a float")});break;case"money":this.record[d.name]&&!w2utils.isMoney(this.record[d.name])&&b.push({field:d,error:w2utils.lang("Not in money format")});break;case"color":case"hex":this.record[d.name]&&!w2utils.isHex(this.record[d.name])&&b.push({field:d,error:w2utils.lang("Not a hex number")});break;case"email":this.record[d.name]&&!w2utils.isEmail(this.record[d.name])&&b.push({field:d,error:w2utils.lang("Not a valid email")});break;case"checkbox":this.record[d.name]=1==this.record[d.name]?1:0;break;case"date":d.options.format||(d.options.format=w2utils.settings.date_format),this.record[d.name]&&!w2utils.isDate(this.record[d.name],d.options.format)&&b.push({field:d,error:w2utils.lang("Not a valid date")+": "+d.options.format});break;case"list":case"combo":break;case"enum":}var e=this.record[d.name];d.required&&(""===e||$.isArray(e)&&0==e.length||$.isPlainObject(e)&&$.isEmptyObject(e))&&b.push({field:d,error:w2utils.lang("Required field")}),d.equalto&&this.record[d.name]!=this.record[d.equalto]&&b.push({field:d,error:w2utils.lang("Field should be equal to ")+d.equalto})}var f=this.trigger({phase:"before",target:this.name,type:"validate",errors:b});if(f.isCancelled!==!0){if(a)for(var g in f.errors){var h=f.errors[g];"radio"==h.field.type?$($(h.field.el).parents("div")[0]).w2tag(h.error,{"class":"w2ui-error"}):-1!=["enum","file"].indexOf(h.field.type)?!function(a){setTimeout(function(){var b=$(a.field.el).data("w2field").helpers.multi;$(a.field.el).w2tag(a.error),$(b).addClass("w2ui-error")},1)}(h):$(h.field.el).w2tag(h.error,{"class":"w2ui-error"}),this.goto(b[0].field.page)}return this.trigger($.extend(f,{phase:"after"})),b}},getChanges:function(){var a=function(b,c,d){for(var e in b)"object"==typeof b[e]?(d[e]=a(b[e],c[e]||{},{}),(!d[e]||$.isEmptyObject(d[e]))&&delete d[e]):b[e]!=c[e]&&(d[e]=b[e]);return d};return a(this.record,this.original,{})},request:function(postData,callBack){var obj=this;if("function"==typeof postData&&(callBack=postData,postData=null),("undefined"==typeof postData||null==postData)&&(postData={}),this.url&&("object"!=typeof this.url||this.url.get)){(null==this.recid||"undefined"==typeof this.recid)&&(this.recid=0);var params={};params.cmd="get-record",params.recid=this.recid,$.extend(params,this.postData),$.extend(params,postData);var eventData=this.trigger({phase:"before",type:"request",target:this.name,url:this.url,postData:params});if(eventData.isCancelled===!0)return void("function"==typeof callBack&&callBack({status:"error",message:"Request aborted."}));this.record={},this.original={},this.lock(this.msgRefresh);var url=eventData.url;if("object"==typeof eventData.url&&eventData.url.get&&(url=eventData.url.get),this.last.xhr)try{this.last.xhr.abort()}catch(e){}if(!$.isEmptyObject(obj.routeData)){var info=w2utils.parseRoute(url);if(info.keys.length>0)for(var k=0;k<info.keys.length;k++)null!=obj.routeData[info.keys[k].name]&&(url=url.replace(new RegExp(":"+info.keys[k].name,"g"),obj.routeData[info.keys[k].name]))}var ajaxOptions={type:"POST",url:url,data:eventData.postData,dataType:"text"};"HTTP"==w2utils.settings.dataType&&(ajaxOptions.data=String($.param(ajaxOptions.data,!1)).replace(/%5B/g,"[").replace(/%5D/g,"]")),"RESTFULL"==w2utils.settings.dataType&&(ajaxOptions.type="GET",ajaxOptions.data=String($.param(ajaxOptions.data,!1)).replace(/%5B/g,"[").replace(/%5D/g,"]")),"JSON"==w2utils.settings.dataType&&(ajaxOptions.type="POST",ajaxOptions.data=JSON.stringify(ajaxOptions.data),ajaxOptions.contentType="application/json"),this.last.xhr=$.ajax(ajaxOptions).done(function(data,status,xhr){obj.unlock();var eventData=obj.trigger({phase:"before",target:obj.name,type:"load",xhr:xhr});if(eventData.isCancelled===!0)return void("function"==typeof callBack&&callBack({status:"error",message:"Request aborted."}));var data,responseText=obj.last.xhr.responseText;if("error"!=status){if("undefined"!=typeof responseText&&""!=responseText){if("object"==typeof responseText)data=responseText;else try{eval("data = "+responseText)}catch(e){}"undefined"==typeof data&&(data={status:"error",message:obj.msgNotJSON,responseText:responseText}),"error"==data.status?obj.error(data.message):(obj.record=$.extend({},data.record),obj.original=$.extend({},data.record))}}else obj.error("AJAX Error "+xhr.status+": "+xhr.statusText),data={status:"error",message:obj.msgAJAXerror,responseText:responseText};obj.trigger($.extend(eventData,{phase:"after"})),obj.refresh(),"function"==typeof callBack&&callBack(data)}).fail(function(a,b,c){var d={status:b,error:c,rawResponseText:a.responseText},e=obj.trigger({phase:"before",type:"error",error:d,xhr:a});if(e.isCancelled!==!0){if("abort"!=b){var f;try{f=$.parseJSON(a.responseText)}catch(g){}console.log("ERROR: Server communication failed.","\n EXPECTED:",{status:"success",items:[{id:1,text:"item"}]},"\n OR:",{status:"error",message:"error message"},"\n RECEIVED:","object"==typeof f?f:a.responseText)}obj.trigger($.extend(e,{phase:"after"}))}}),this.trigger($.extend(eventData,{phase:"after"}))}},submit:function(a,b){return this.save(a,b)},save:function(postData,callBack){var obj=this;$(this.box).find(":focus").change(),"function"==typeof postData&&(callBack=postData,postData=null);var errors=obj.validate(!0);if(0===errors.length){if(("undefined"==typeof postData||null==postData)&&(postData={}),!obj.url||"object"==typeof obj.url&&!obj.url.save)return void console.log("ERROR: Form cannot be saved because no url is defined.");obj.lock(obj.msgSaving+' <span id="'+obj.name+'_progress"></span>'),setTimeout(function(){var params={};params.cmd="save-record",params.recid=obj.recid,$.extend(params,obj.postData),$.extend(params,postData),params.record=$.extend(!0,{},obj.record);var eventData=obj.trigger({phase:"before",type:"submit",target:obj.name,url:obj.url,postData:params});if(eventData.isCancelled!==!0){var url=eventData.url;if("object"==typeof eventData.url&&eventData.url.save&&(url=eventData.url.save),obj.last.xhr)try{obj.last.xhr.abort()}catch(e){}if(!$.isEmptyObject(obj.routeData)){var info=w2utils.parseRoute(url);if(info.keys.length>0)for(var k=0;k<info.keys.length;k++)null!=obj.routeData[info.keys[k].name]&&(url=url.replace(new RegExp(":"+info.keys[k].name,"g"),obj.routeData[info.keys[k].name]))}var ajaxOptions={type:"POST",url:url,data:eventData.postData,dataType:"text",xhr:function(){var a=new window.XMLHttpRequest;return a.upload.addEventListener("progress",function(a){if(a.lengthComputable){var b=Math.round(a.loaded/a.total*100);$("#"+obj.name+"_progress").text(""+b+"%")}},!1),a}};"HTTP"==w2utils.settings.dataType&&(ajaxOptions.data=String($.param(ajaxOptions.data,!1)).replace(/%5B/g,"[").replace(/%5D/g,"]")),"RESTFULL"==w2utils.settings.dataType&&(0!=obj.recid&&(ajaxOptions.type="PUT"),ajaxOptions.data=String($.param(ajaxOptions.data,!1)).replace(/%5B/g,"[").replace(/%5D/g,"]")),"JSON"==w2utils.settings.dataType&&(ajaxOptions.type="POST",ajaxOptions.data=JSON.stringify(ajaxOptions.data),ajaxOptions.contentType="application/json"),obj.last.xhr=$.ajax(ajaxOptions).done(function(data,status,xhr){obj.unlock();var eventData=obj.trigger({phase:"before",target:obj.name,type:"save",xhr:xhr,status:status});if(eventData.isCancelled!==!0){var data,responseText=xhr.responseText;if("error"!=status){if("undefined"!=typeof responseText&&""!=responseText){if("object"==typeof responseText)data=responseText;else try{eval("data = "+responseText)}catch(e){}"undefined"==typeof data&&(data={status:"error",message:obj.msgNotJSON,responseText:responseText}),"error"==data.status?obj.error(data.message):obj.original=$.extend({},obj.record)}}else obj.error("AJAX Error "+xhr.status+": "+xhr.statusText),data={status:"error",message:obj.msgAJAXerror,responseText:responseText};obj.trigger($.extend(eventData,{phase:"after"})),obj.refresh(),"success"==data.status&&"function"==typeof callBack&&callBack(data)}}).fail(function(a,b,c){var d={status:b,error:c,rawResponseText:a.responseText},e=obj.trigger({phase:"before",type:"error",error:d,xhr:a});e.isCancelled!==!0&&(console.log("ERROR: server communication failed. The server should return",{status:"success"},"OR",{status:"error",message:"error message"},", instead the AJAX request produced this: ",d),obj.trigger($.extend(e,{phase:"after"})))}),obj.trigger($.extend(eventData,{phase:"after"}))}},50)}},lock:function(){var a=$(this.box).find("> div:first-child"),b=Array.prototype.slice.call(arguments,0);b.unshift(a),w2utils.lock.apply(window,b)},unlock:function(){var a=this;setTimeout(function(){w2utils.unlock(a.box)},25)},"goto":function(a){"undefined"!=typeof a&&(this.page=a),$(this.box).data("auto-size")===!0&&$(this.box).height(0),this.refresh()},generateHTML:function(){var a,b=[],c="";for(var d in this.fields){var e="",f=this.fields[d];"undefined"==typeof f.html&&(f.html={}),f.html=$.extend(!0,{caption:"",span:6,attr:"",text:"",page:0},f.html),"undefined"==typeof a&&(a=f.html.page),""==f.html.caption&&(f.html.caption=f.name);var g='<input name="'+f.name+'" type="text" '+f.html.attr+"/>";("pass"===f.type||"password"===f.type)&&(g='<input name="'+f.name+'" type = "password" '+f.html.attr+"/>"),"checkbox"==f.type&&(g='<input name="'+f.name+'" type="checkbox" '+f.html.attr+"/>"),"textarea"==f.type&&(g='<textarea name="'+f.name+'" '+f.html.attr+"></textarea>"),"toggle"==f.type&&(g='<input name="'+f.name+'" type="checkbox" '+f.html.attr+' class="w2ui-toggle"/><div><div></div></div>'),f.html.group&&(""!=c&&(e+="\n </div>"),e+='\n <div class="w2ui-group-title">'+f.html.group+'</div>\n <div class="w2ui-group">',c=f.html.group),f.html.page!=a&&""!=c&&(b[b.length-1]+="\n </div>",c=""),e+='\n <div class="w2ui-field '+("undefined"!=typeof f.html.span?"w2ui-span"+f.html.span:"")+'">\n <label>'+w2utils.lang(f.html.caption)+"</label>\n <div>"+g+w2utils.lang(f.html.text)+"</div>\n </div>","undefined"==typeof b[f.html.page]&&(b[f.html.page]=""),b[f.html.page]+=e,a=f.html.page}if(""!=c&&(b[b.length-1]+="\n </div>"),this.tabs.tabs)for(var h=0;h<this.tabs.tabs.length;h++)"undefined"==typeof b[h]&&(b[h]="");for(var i in b)b[i]='<div class="w2ui-page page-'+i+'">'+b[i]+"\n</div>";var j="";if(!$.isEmptyObject(this.actions)){var k="";j+='\n<div class="w2ui-buttons">';for(var l in this.actions)k=-1!=["save","update","create"].indexOf(l.toLowerCase())?"btn-green":"",j+='\n <button name="'+l+'" class="btn '+k+'">'+w2utils.lang(l)+"</button>";j+="\n</div>"}return b.join("")+j},action:function(a,b){var c=this.trigger({phase:"before",target:a,type:"action",originalEvent:b});c.isCancelled!==!0&&("function"==typeof this.actions[a]&&this.actions[a].call(this,b),this.trigger($.extend(c,{phase:"after"})))},resize:function(){function a(){d.width($(b.box).width()).height($(b.box).height()),f.css("top",""!=b.header?w2utils.getSize(e,"height"):0),g.css("top",(""!=b.header?w2utils.getSize(e,"height"):0)+("object"==typeof b.toolbar&&$.isArray(b.toolbar.items)&&b.toolbar.items.length>0?w2utils.getSize(f,"height"):0)),h.css("top",(""!=b.header?w2utils.getSize(e,"height"):0)+("object"==typeof b.toolbar&&$.isArray(b.toolbar.items)&&b.toolbar.items.length>0?w2utils.getSize(f,"height")+5:0)+("object"==typeof b.tabs&&$.isArray(b.tabs.tabs)&&b.tabs.tabs.length>0?w2utils.getSize(g,"height")+5:0)),h.css("bottom",k.length>0?w2utils.getSize(k,"height"):0)}var b=this,c=this.trigger({phase:"before",target:this.name,type:"resize"});if(c.isCancelled!==!0){var d=$(this.box).find("> div"),e=$(this.box).find("> div .w2ui-form-header"),f=$(this.box).find("> div .w2ui-form-toolbar"),g=$(this.box).find("> div .w2ui-form-tabs"),h=$(this.box).find("> div .w2ui-page"),i=$(this.box).find("> div .w2ui-page.page-"+this.page),j=$(this.box).find("> div .w2ui-page.page-"+this.page+" > div"),k=$(this.box).find("> div .w2ui-buttons");a(),(0==parseInt($(this.box).height())||$(this.box).data("auto-size")===!0)&&($(this.box).height((e.length>0?w2utils.getSize(e,"height"):0)+("object"==typeof this.tabs&&$.isArray(this.tabs.tabs)&&this.tabs.tabs.length>0?w2utils.getSize(g,"height"):0)+("object"==typeof this.toolbar&&$.isArray(this.toolbar.items)&&this.toolbar.items.length>0?w2utils.getSize(f,"height"):0)+(h.length>0?w2utils.getSize(j,"height")+w2utils.getSize(i,"+height")+12:0)+(k.length>0?w2utils.getSize(k,"height"):0)),$(this.box).data("auto-size",!0)),a(),b.trigger($.extend(c,{phase:"after"}))}},refresh:function(){var a=(new Date).getTime(),b=this;if(this.box&&this.isGenerated&&"undefined"!=typeof $(this.box).html()){$(this.box).find("input, textarea, select").each(function(a,c){var d=$(c).attr("undefined"!=typeof $(c).attr("name")?"name":"id"),e=b.get(d);if(e){var f=$(c).parents(".w2ui-page");if(f.length>0)for(var g=0;100>g;g++)if(f.hasClass("page-"+g)){e.page=g;break}}});var c=this.trigger({phase:"before",target:this.name,type:"refresh",page:this.page});if(c.isCancelled!==!0){$(this.box).find(".w2ui-page").hide(),$(this.box).find(".w2ui-page.page-"+this.page).show(),$(this.box).find(".w2ui-form-header").html(this.header),"object"==typeof this.tabs&&$.isArray(this.tabs.tabs)&&this.tabs.tabs.length>0?($("#form_"+this.name+"_tabs").show(),this.tabs.active=this.tabs.tabs[this.page].id,this.tabs.refresh()):$("#form_"+this.name+"_tabs").hide(),"object"==typeof this.toolbar&&$.isArray(this.toolbar.items)&&this.toolbar.items.length>0?($("#form_"+this.name+"_toolbar").show(),this.toolbar.refresh()):$("#form_"+this.name+"_toolbar").hide();
+for(var d in this.fields){var e=this.fields[d];"undefined"==typeof e.name&&"undefined"!=typeof e.field&&(e.name=e.field),"undefined"==typeof e.field&&"undefined"!=typeof e.name&&(e.field=e.name),e.$el=$(this.box).find('[name="'+String(e.name).replace(/\\/g,"\\\\")+'"]'),e.el=e.$el[0],"undefined"==typeof e.el&&console.log('ERROR: Cannot associate field "'+e.name+'" with html control. Make sure html control exists with the same name.'),e.el&&(e.el.id=e.name);var f=$(e).data("w2field");f&&f.clear(),$(e.$el).off("change").on("change",function(){var a=this.value,c=b.record[this.name]?b.record[this.name]:"",d=b.get(this.name);if(-1!=["list","enum","file"].indexOf(d.type)&&$(this).data("selected")){var e=$(this).data("selected"),f=b.record[this.name];if($.isArray(e)){a=[];for(var g in e)a[g]=$.extend(!0,{},e[g])}if($.isPlainObject(e)&&(a=$.extend(!0,{},e)),$.isArray(f)){c=[];for(var g in f)c[g]=$.extend(!0,{},f[g])}$.isPlainObject(f)&&(c=$.extend(!0,{},f))}if("toggle"==d.type&&(a=$(this).prop("checked")?1:0),-1!=["int","float","percent","money","currency"].indexOf(d.type)&&(a=$(this).data("w2field").clean(a)),a!==c){var h=b.trigger({phase:"before",target:this.name,type:"change",value_new:a,value_previous:c});if(h.isCancelled===!0)return void $(this).val(b.record[this.name]);var i=this.value;if("select"==this.type&&(i=this.value),"checkbox"==this.type&&(i=this.checked?!0:!1),"radio"==this.type&&d.$el.each(function(a,b){b.checked&&(i=b.value)}),-1!=["int","float","percent","money","currency","list","combo","enum","file","toggle"].indexOf(d.type)&&(i=a),-1!=["enum","file"].indexOf(d.type)&&i.length>0){var j=$(d.el).data("w2field").helpers.multi;$(j).removeClass("w2ui-error")}b.record[this.name]=i,b.trigger($.extend(h,{phase:"after"}))}}),e.required?$(e.el).parent().parent().addClass("w2ui-required"):$(e.el).parent().parent().removeClass("w2ui-required")}$(this.box).find("button, input[type=button]").each(function(a,c){$(c).off("click").on("click",function(a){var c=this.value;this.id&&(c=this.id),this.name&&(c=this.name),b.action(c,a)})});for(var d in this.fields){var e=this.fields[d],g="undefined"!=typeof this.record[e.name]?this.record[e.name]:"";if(e.el)switch(e.type=String(e.type).toLowerCase(),e.options||(e.options={}),e.type){case"text":case"textarea":case"email":case"pass":case"password":e.el.value=g;break;case"int":case"float":case"money":case"currency":case"percent":case"hex":case"alphanumeric":case"color":case"date":case"time":e.el.value=g,$(e.el).w2field($.extend({},e.options,{type:e.type}));break;case"toggle":w2utils.isFloat(g)&&(g=parseFloat(g)),$(e.el).prop("checked",g?!0:!1),this.record[e.name]=g?1:0;break;case"list":case"combo":if("list"!=e.type||$.isPlainObject(g))e.el.value="combo"!=e.type||$.isPlainObject(g)?$.isPlainObject(g)&&"undefined"!=typeof g.text?g.text:"":g;else for(var h in e.options.items){var i=e.options.items[h];if($.isPlainObject(i)&&i.id==g){g=$.extend(!0,{},i),b.record[e.name]=g;break}if(h==g){g={id:h,text:i},b.record[e.name]=g;break}}$.isPlainObject(g)||(g={}),$(e.el).w2field($.extend({},e.options,{type:e.type,selected:g}));break;case"enum":case"file":$.isArray(g)||(g=[]),$(e.el).w2field($.extend({},e.options,{type:e.type,selected:g}));break;case"select":var j=e.options.items;if("undefined"!=typeof j&&j.length>0){j=w2obj.field.prototype.normMenu(j),$(e.el).html("");for(var k in j)$(e.el).append('<option value="'+j[k].id+'">'+j[k].text+"</option")}$(e.el).val(g);break;case"radio":$(e.$el).prop("checked",!1).each(function(a,b){$(b).val()==g&&$(b).prop("checked",!0)});break;case"checkbox":$(e.el).prop("checked",g?!0:!1);break;default:$(e.el).w2field($.extend({},e.options,{type:e.type}))}}for(var f=$(this.box).find(".w2ui-page"),h=0;h<f.length;h++)$(f[h]).find("> *").length>1&&$(f[h]).wrapInner("<div></div>");return this.trigger($.extend(c,{phase:"after"})),this.resize(),(new Date).getTime()-a}}},render:function(a){function b(){var a=$(d.box).find("input, select, textarea");a.length>d.focus&&a[d.focus].focus()}var c=(new Date).getTime(),d=this;if("object"==typeof a&&($(this.box).find("#form_"+this.name+"_tabs").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-form").html(""),this.box=a),this.isGenerated&&this.box){var e=this.trigger({phase:"before",target:this.name,type:"render",box:"undefined"!=typeof a?a:this.box});if(e.isCancelled!==!0){$.isEmptyObject(this.original)&&!$.isEmptyObject(this.record)&&(this.original=$.extend(!0,{},this.record));var f="<div>"+(""!=this.header?'<div class="w2ui-form-header">'+this.header+"</div>":"")+' <div id="form_'+this.name+'_toolbar" class="w2ui-form-toolbar"></div> <div id="form_'+this.name+'_tabs" class="w2ui-form-tabs"></div>'+this.formHTML+"</div>";$(this.box).attr("name",this.name).addClass("w2ui-reset w2ui-form").html(f),$(this.box).length>0&&($(this.box)[0].style.cssText+=this.style),"function"!=typeof this.toolbar.render&&(this.toolbar=$().w2toolbar($.extend({},this.toolbar,{name:this.name+"_toolbar",owner:this})),this.toolbar.on("click",function(a){var b=d.trigger({phase:"before",type:"toolbar",target:a.target,originalEvent:a});b.isCancelled!==!0&&d.trigger($.extend(b,{phase:"after"}))})),"object"==typeof this.toolbar&&"function"==typeof this.toolbar.render&&this.toolbar.render($("#form_"+this.name+"_toolbar")[0]),"function"!=typeof this.tabs.render&&(this.tabs=$().w2tabs($.extend({},this.tabs,{name:this.name+"_tabs",owner:this})),this.tabs.on("click",function(a){d.goto(this.get(a.target,!0))})),"object"==typeof this.tabs&&"function"==typeof this.tabs.render&&this.tabs.render($("#form_"+this.name+"_tabs")[0]),this.trigger($.extend(e,{phase:"after"})),this.resize();var g="object"!=typeof this.url?this.url:this.url.get;return g&&0!=this.recid?this.request():this.refresh(),0==$(".w2ui-layout").length&&(this.tmp_resize=function(){w2ui[d.name].resize()},$(window).off("resize","body").on("resize","body",this.tmp_resize)),setTimeout(function(){d.resize(),d.refresh()},150),this.focus>=0&&setTimeout(b,500),(new Date).getTime()-c}}},destroy:function(){var a=this.trigger({phase:"before",target:this.name,type:"destroy"});a.isCancelled!==!0&&("object"==typeof this.toolbar&&this.toolbar.destroy&&this.toolbar.destroy(),"object"==typeof this.tabs&&this.tabs.destroy&&this.tabs.destroy(),$(this.box).find("#form_"+this.name+"_tabs").length>0&&$(this.box).removeAttr("name").removeClass("w2ui-reset w2ui-form").html(""),delete w2ui[this.name],this.trigger($.extend(a,{phase:"after"})),$(window).off("resize","body"))}},$.extend(w2form.prototype,w2utils.event),w2obj.form=w2form}(); \ No newline at end of file
diff --git a/simple/simple-transport/pom.xml b/simple/simple-transport/pom.xml
new file mode 100644
index 00000000..061e5b52
--- /dev/null
+++ b/simple/simple-transport/pom.xml
@@ -0,0 +1,137 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.sonatype.oss</groupId>
+ <artifactId>oss-parent</artifactId>
+ <version>7</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple-transport</artifactId>
+ <packaging>jar</packaging>
+ <version>6.0.1</version>
+ <name>Simple Transport</name>
+ <url>http://www.simpleframework.org</url>
+ <description>Simple is a high performance asynchronous HTTP framework for Java</description>
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <scm>
+ <url>http://simpleweb.svn.sourceforge.net/viewvc/simpleweb.tags/simple-transport-6.0.1</url>
+ <developerConnection>scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-transport-6.0.1</developerConnection>
+ <connection>scm:svn:https://simpleweb.svn.sourceforge.net/svnroot/simpleweb.tags/simple-transport-6.0.1</connection>
+ </scm>
+ <developers>
+ <developer>
+ <id>niallg</id>
+ <name>Niall Gallagher</name>
+ <email>niallg@users.sf.net</email>
+ </developer>
+ </developers>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <file.encoding>UTF-8</file.encoding>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple-common</artifactId>
+ <version>6.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>3.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>cobertura-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </reporting>
+ <build>
+ <extensions>
+ <extension>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-ssh-external</artifactId>
+ <version>1.0-alpha-5</version>
+ </extension>
+ </extensions>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.5</source>
+ <target>1.5</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <configuration>
+ <encoding>UTF-8</encoding>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-javadocs</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <profiles>
+ <profile>
+ <id>release-sign-artifacts</id>
+ <activation>
+ <property>
+ <name>performRelease</name>
+ <value>true</value>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-gpg-plugin</artifactId>
+ <version>1.1</version>
+ <executions>
+ <execution>
+ <id>sign-artifacts</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>sign</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteCursor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteCursor.java
new file mode 100644
index 00000000..a5335ed4
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteCursor.java
@@ -0,0 +1,131 @@
+/*
+ * ByteCursor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+/**
+ * The <code>ByteCursor</code> object is used to acquire bytes from a
+ * given source. This provides a cursor style reading of bytes from
+ * a stream in that it will allow the reader to move the cursor back
+ * if the amount of bytes read is too much. Allowing the cursor to
+ * move ensures that excess bytes back be placed back in the stream.
+ * <p>
+ * This is used when parsing input from a stream as it ensures that
+ * on arrival at a terminal token any excess bytes can be placed
+ * back in to the stream. This allows data to be read efficiently
+ * in large chunks from blocking streams such as sockets.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.TransportCursor
+ */
+public interface ByteCursor {
+
+ /**
+ * 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
+ */
+ boolean isOpen() throws IOException;
+
+ /**
+ * 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
+ */
+ boolean isReady() throws IOException;
+
+ /**
+ * 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
+ */
+ int ready() throws IOException;
+
+ /**
+ * 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
+ */
+ int read(byte[] data) throws IOException;
+
+ /**
+ * 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
+ */
+ int read(byte[] data, int off, int len) throws IOException;
+
+ /**
+ * 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 <code>reset</code> 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
+ */
+ void push(byte[] data) throws IOException;
+
+ /**
+ * 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 <code>reset</code> 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
+ */
+ void push(byte[] data, int off, int len) throws IOException;
+
+ /**
+ * 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
+ */
+ int reset(int len) throws IOException;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteReader.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteReader.java
new file mode 100644
index 00000000..3e26e765
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteReader.java
@@ -0,0 +1,107 @@
+/*
+ * ByteReader.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+/**
+ * The <code>ByteReader</code> object is used to acquire bytes from
+ * a given source. This provides a cursor style reading of bytes from
+ * a stream in that it will allow the reader to move the cursor back
+ * if the amount of bytes read is too much. Allowing the cursor to
+ * move ensures that excess bytes can be placed back in the stream.
+ * <p>
+ * This is used when parsing input from a stream as it ensures that
+ * on arrival at a terminal token any excess bytes can be placed
+ * back in to the stream. This allows data to be read efficiently
+ * in large chunks from blocking streams such as sockets.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.ByteCursor
+ */
+interface ByteReader {
+
+ /**
+ * Determines whether the source is still open. The source is
+ * considered open if there are still bytes to read. If there is
+ * still bytes buffered and the underlying transport is closed
+ * then the source is still considered open.
+ *
+ * @return true if the read method does not return a -1 value
+ */
+ boolean isOpen() throws IOException;
+
+ /**
+ * Determines whether the source is ready for reading. When the
+ * source 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
+ */
+ boolean isReady() throws IOException;
+
+ /**
+ * 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 source may perform a blocking read.
+ *
+ * @return the number of bytes that can be read without blocking
+ */
+ int ready() throws IOException;
+
+ /**
+ * 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 source
+ */
+ int read(byte[] data) throws IOException;
+
+ /**
+ * 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 source
+ */
+ int read(byte[] data, int off, int len) throws IOException;
+
+ /**
+ * Moves the source 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
+ */
+ int reset(int len) throws IOException;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteWriter.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteWriter.java
new file mode 100644
index 00000000..dc647cf0
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/ByteWriter.java
@@ -0,0 +1,98 @@
+/*
+ * ByteWriter.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * The <code>ByteWriter</code> object is used to send data over the TCP
+ * transport. This provides direct contact with the connected socket.
+ * Delivery over a sender implementation can be either SSL based or
+ * direct. It is the responsibility of the implementation to provide
+ * such behavior as required.
+ *
+ * @author Niall Gallagher
+ */
+public interface ByteWriter {
+
+ /**
+ * This method is used to deliver the provided array of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param array this is the array of bytes to send to the client
+ */
+ void write(byte[] array) throws IOException;
+
+ /**
+ * This method is used to deliver the provided array of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @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
+ */
+ void write(byte[] array, int off, int len) throws IOException;
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param buffer this is the buffer of bytes to send to the client
+ */
+ void write(ByteBuffer buffer) throws IOException;
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @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
+ */
+ void write(ByteBuffer buffer, int off, int len) 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 close the sender and the underlying transport.
+ * If a close is performed on the sender then no more bytes can
+ * be read from or written to the transport and the client will
+ * received a connection close on their side.
+ */
+ void close() throws IOException;
+}
+
+
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/Certificate.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/Certificate.java
new file mode 100644
index 00000000..05f9c915
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/Certificate.java
@@ -0,0 +1,65 @@
+/*
+ * Certificate.java June 2013
+ *
+ * Copyright (C) 2013, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import javax.security.cert.X509Certificate;
+
+/**
+ * The <code>Certificate</code> interface represents the certificate
+ * that is sent by a client during a secure HTTPS conversation. This
+ * may or may not contain an X509 certificate chain from the client.
+ * If it does not a <code>CertificateChallenge</code> may be used to
+ * issue a renegotiation of the connection. One completion of the
+ * renegotiation the challenge executes a completion operation.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.CertificateChallenge
+ */
+public interface Certificate {
+
+ /**
+ * 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
+ */
+ X509Certificate[] getChain() throws Exception;
+
+ /**
+ * This returns a challenge for the certificate. A challenge is
+ * issued by providing a <code>Runnable</code> 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
+ */
+ CertificateChallenge getChallenge() throws Exception;
+
+ /**
+ * 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
+ */
+ boolean isChainPresent() throws Exception;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/CertificateChallenge.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/CertificateChallenge.java
new file mode 100644
index 00000000..5ed27434
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/CertificateChallenge.java
@@ -0,0 +1,73 @@
+/*
+ * CertificateChallenge.java June 2013
+ *
+ * Copyright (C) 2013, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.util.concurrent.Future;
+
+/**
+ * The <code>CertificateChallenge</code> object is used to challenge
+ * a client for their x509 certificate. Notification of a successful
+ * challenge for the certificate is done using a completion task.
+ * The task is executed when the SSL renegotiation completes with
+ * a client certificate.
+ * <p>
+ * For HTTPS the SSL renegotiation workflow used to challenge the
+ * client for their X509 certificate is rather bizzare. It starts
+ * with an initial challenge, where an SSL handshake is performed.
+ * This initial handshake typically completes but results in the
+ * TCP connection being closed by the client. Then a second
+ * handshake is performed by the client on a new TCP connection,
+ * this second handshake does not contain the certificate either.
+ * When the handshake is finished on this new connection the client
+ * will resubmit the original HTTP request. Again the server will
+ * have to challenge for the certificate, which should succeed and
+ * result in execution of the task provided.
+ * <p>
+ * An important point to note here, is that if the client closes
+ * the TCP connection on the first challenge, the completion task
+ * will not be executed, it will be ignored. Only a successful
+ * completion of a HTTPS renegotiation will result in execution
+ * of the provided task.
+ *
+ * @author Niall Gallagher
+ */
+public interface CertificateChallenge {
+
+ /**
+ * 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.
+ *
+ * @return this future containing the original certificate
+ */
+ Future<Certificate> challenge() throws Exception;
+
+ /**
+ * 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
+ *
+ * @return this future containing the original certificate
+ */
+ Future<Certificate> challenge(Runnable completion) throws Exception;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/Channel.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/Channel.java
new file mode 100644
index 00000000..02e6cbd0
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/Channel.java
@@ -0,0 +1,128 @@
+/*
+ * Channel.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.nio.channels.SocketChannel;
+import java.util.Map;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>Channel</code> interface represents a connected channel
+ * through which data can be sent and received. Typically a channel
+ * will have a connected TCP socket, which can be used to determine
+ * when the channel is read ready, and write ready. A channel can
+ * also contain a bag of attributes used to describe the connection.
+ * <p>
+ * Reading and writing to a channel is performed using two special
+ * interfaces. The first is the <code>ByteCursor</code> object which
+ * is used to read data from the channel in a non-blocking manner.
+ * This can also be used to reset data if it has read too much. To
+ * write the <code>ByteWriter</code> can be used, this provides a
+ * blocking interface much like a conventional output stream.
+ *
+ * @author Niall Gallagher
+ */
+public interface Channel {
+
+ /**
+ * This is used to determine if the channel is secure and that
+ * data read from and data written to the request is encrypted.
+ * Channels transferred over SSL are considered secure and will
+ * have this return true, otherwise it will return false.
+ *
+ * @return true if this is secure for reading and writing
+ */
+ boolean isSecure();
+
+ /**
+ * This is the connected socket channel associated with this. In
+ * order to determine if content can be read or written to or
+ * from the channel this socket can be used with a selector. This
+ * provides a means to react to I/O events as they occur rather
+ * than polling the channel which is generally less performant.
+ *
+ * @return this returns the connected socket channel
+ */
+ SocketChannel getSocket();
+
+ /**
+ * This is used to acquire the SSL certificate used for security.
+ * If the socket is connected to an SSL transport this returns an
+ * SSL certificate which was provided during the secure handshake
+ * between the client and server. If not certificates are present
+ * in the provided instance, a challenge can be issued.
+ *
+ * @return the SSL certificate provided by a secure transport
+ */
+ Certificate getCertificate();
+
+ /**
+ * This gets the <code>Trace</code> object associated with the
+ * channel. The trace is used to log various events for the life
+ * of the transaction such as low level read and write events
+ * as well as milestone events and errors.
+ *
+ * @return this returns the trace associated with the socket
+ */
+ Trace getTrace();
+
+ /**
+ * This provides a <code>ByteCursor</code> for this channel. The
+ * cursor provides a seekable view of the input buffer and will
+ * allow the server kernel to peek into the input buffer without
+ * having to take the data from the input. This allows overflow
+ * to be pushed back on to the cursor for subsequent reads.
+ *
+ * @return this returns the input cursor for the channel
+ */
+ ByteCursor getCursor();
+
+ /**
+ * This provides a <code>ByteWriter</code> for the channel. This
+ * is used to provide a blocking output mechanism for the channel.
+ * Enabling blocking reads ensures that output buffering can be
+ * limited to an extent, which ensures that memory remains low at
+ * high load periods. Writes to the sender may result in the data
+ * being copied and queued until the socket is write ready.
+ *
+ * @return this returns the output sender for this channel
+ */
+ ByteWriter getWriter();
+
+ /**
+ * This returns the <code>Map</code> of attributes used to hold
+ * connection information for the channel. The attributes here
+ * are taken from the pipeline attributes and may contain details
+ * such as SSL certificates or other such useful information.
+ *
+ * @return returns the attributes associated with the channel
+ */
+ Map getAttributes();
+
+ /**
+ * Because the channel represents a duplex means of communication
+ * there needs to be a means to close it down. This provides such
+ * a means. By closing the channel the cursor and sender will no
+ * longer send or recieve data to or from the network. The client
+ * will also be signaled that the connection has been severed.
+ */
+ void close();
+
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/FlushScheduler.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/FlushScheduler.java
new file mode 100644
index 00000000..43a64ed5
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/FlushScheduler.java
@@ -0,0 +1,197 @@
+/*
+ * FlushScheduler.java February 2008
+ *
+ * Copyright (C) 2008, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static java.nio.channels.SelectionKey.OP_WRITE;
+import static org.simpleframework.transport.TransportEvent.WRITE_BLOCKING;
+import static org.simpleframework.transport.TransportEvent.WRITE_WAIT;
+
+import java.io.IOException;
+
+import org.simpleframework.transport.reactor.Operation;
+import org.simpleframework.transport.reactor.Reactor;
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>FlushScheduler</code> object is used to schedule a task
+ * for execution when it is write ready. This is used by the socket
+ * flusher to ensure that the writing thread can be blocked until
+ * such time as all the bytes required to be written are written.
+ * <p>
+ * All methods are invoked by a <code>SocketFlusher</code> object
+ * which is synchronized. This ensures that the methods of the
+ * scheduler are thread safe in that only one thread will access
+ * them at any given time. The lock used by the socket flusher can
+ * thus be safely as it will be synchronized on by the flusher.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.SocketFlusher
+ */
+class FlushScheduler {
+
+ /**
+ * This is the operation that is scheduled for execution.
+ */
+ private Operation task;
+
+ /**
+ * This is the reactor to used to execute the operation.
+ */
+ private Reactor reactor;
+
+ /**
+ * This is the trace that listens to all transport events.
+ */
+ private Trace trace;
+
+ /**
+ * This is the lock that is used to signal a blocked thread.
+ */
+ private Object lock;
+
+ /**
+ * This is used to determine if the scheduler is running.
+ */
+ private volatile boolean running;
+
+ /**
+ * This is used to determine if the scheduler is interrupted.
+ */
+ private volatile boolean closed;
+
+ /**
+ * This is used to determine if there is currently a flush.
+ */
+ private volatile boolean flushing;
+
+ /**
+ * Constructor for the <code>FlushScheduler</code> object. This
+ * is* used to create a scheduler that will execute the provided
+ * task when the associated socket is write ready.
+ *
+ * @param socket this is the associated socket for the scheduler
+ * @param reactor this is the rector used to schedule execution
+ * @param task this is the task that is executed when writable
+ * @param lock this is the lock used to signal blocking threads
+ */
+ public FlushScheduler(Socket socket, Reactor reactor, Operation task, Object lock) {
+ this.trace = socket.getTrace();
+ this.reactor = reactor;
+ this.task = task;
+ this.lock = lock;
+ }
+
+ /**
+ * This is used to repeat schedule the operation for execution.
+ * This is executed if the operation has not fully completed
+ * its task. If the scheduler is not in a running state then
+ * this will not schedule the task for a repeat execution.
+ */
+ public void repeat() throws IOException {
+ if(closed) {
+ throw new TransportException("Socket closed");
+ }
+ if(running) {
+ trace.trace(WRITE_WAIT);
+ reactor.process(task, OP_WRITE);
+ }
+ }
+
+ /**
+ * This is used to schedule the task for execution. If this is
+ * given a boolean true to indicate that it wishes to block
+ * then this will block the calling thread until such time as
+ * the <code>ready</code> method is invoked.
+ *
+ * @param block indicates whether the thread should block
+ */
+ public void schedule(boolean block) throws IOException {
+ if(closed) {
+ throw new TransportException("Socket closed");
+ }
+ if(!running) {
+ trace.trace(WRITE_WAIT);
+ reactor.process(task, OP_WRITE);
+ running = true;
+ }
+ if(block) {
+ listen();
+ }
+ }
+
+ /**
+ * This is used to listen for a notification from the reactor to
+ * tell the thread that the write operation has completed. If
+ * the thread is interrupted upon this call then this will throw
+ * an <code>IOException</code> with the root cause.
+ */
+ private void listen() throws IOException {
+ if(flushing) {
+ throw new TransportException("Socket already flushing");
+ }
+ try {
+ if(!closed) {
+ try {
+ flushing = true;
+ trace.trace(WRITE_BLOCKING);
+ lock.wait(120000);
+ } finally {
+ flushing = false;
+ }
+ }
+ } catch(Exception e) {
+ throw new TransportException("Could not schedule for flush", e);
+ }
+ if(closed) {
+ throw new TransportException("Socket closed");
+ }
+ }
+
+ /**
+ * This is used to notify any waiting threads that they no longer
+ * need to wait. This is used when the flusher no longer needs
+ * the waiting thread to block. Such an occurrence happens when
+ * all shared data has been written or has been duplicated.
+ */
+ public void release() {
+ lock.notifyAll();
+ }
+
+ /**
+ * This is used to signal any blocking threads to wake up. When
+ * this is invoked blocking threads are signaled and they can
+ * return. This is typically done when the task has finished.
+ */
+ public void ready() {
+ lock.notifyAll();
+ running = false;
+ }
+
+ /**
+ * This is used to close the scheduler when the reactor is
+ * closed by the server. An close will happen when the server
+ * has been shutdown, it ensures there are no threads lingering
+ * waiting for a notification when the reactor has closed.
+ */
+ public void close() {
+ lock.notifyAll();
+ closed = true;
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/FlushSignaller.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/FlushSignaller.java
new file mode 100644
index 00000000..e7f2c4f7
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/FlushSignaller.java
@@ -0,0 +1,120 @@
+/*
+ * FlushSignaller.java February 2008
+ *
+ * Copyright (C) 2008, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static org.simpleframework.transport.TransportEvent.ERROR;
+
+import java.nio.channels.SocketChannel;
+
+import org.simpleframework.transport.reactor.Operation;
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>FlushSignaller</code> is an operation that performs
+ * writes operation asynchronously. This will basically determine
+ * if the socket is write ready and drain each queued buffer to
+ * the socket until there are no more pending buffers.
+ *
+ * @author Niall Gallagher
+ */
+class FlushSignaller implements Operation {
+
+ /**
+ * This is the writer that is used to write the data.
+ */
+ private final SocketFlusher writer;
+
+ /**
+ * This is the socket that this will be flushing.
+ */
+ private final Socket socket;
+
+ /**
+ * This is used to trace the activity for the operation.
+ */
+ private final Trace trace;
+
+ /**
+ * Constructor for the <code>FlushSignaller</code> object. This
+ * will create an operation that is used to flush the buffer
+ * queue to the underlying socket. This ensures that the data
+ * is written to the socket in the queued order.
+ *
+ * @param writer this is the writer to flush the data to
+ * @param socket this is the socket to be flushed
+ */
+ public FlushSignaller(SocketFlusher writer, Socket socket) {
+ this.trace = socket.getTrace();
+ this.socket = socket;
+ this.writer = writer;
+ }
+
+ /**
+ * 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 returns the socket channel for the connected pipeline. It
+ * is this channel that is used to determine if there are bytes
+ * that can be written. When closed this is no longer selectable.
+ *
+ * @return this returns the connected channel for the pipeline
+ */
+ public SocketChannel getChannel() {
+ return socket.getChannel();
+ }
+
+ /**
+ * This is used to perform the drain of the pending buffer
+ * queue. This will drain each pending queue if the socket is
+ * write ready. If the socket is not write ready the operation
+ * is enqueued for selection and this returns. This ensures
+ * that all the data will eventually be delivered.
+ */
+ public void run() {
+ try {
+ writer.execute();
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ cancel();
+ }
+ }
+
+ /**
+ * This is used to cancel the operation if it has timed out.
+ * If the delegate is waiting too long to flush the contents
+ * of the buffers to the underlying transport then the socket
+ * is closed and the flusher times out to avoid deadlock.
+ */
+ public void cancel() {
+ try {
+ writer.abort();
+ }catch(Exception cause){
+ trace.trace(ERROR, cause);
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/Handshake.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/Handshake.java
new file mode 100644
index 00000000..2a4b9a29
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/Handshake.java
@@ -0,0 +1,652 @@
+/*
+ * Handshake.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static java.nio.channels.SelectionKey.OP_READ;
+import static java.nio.channels.SelectionKey.OP_WRITE;
+import static org.simpleframework.transport.PhaseType.COMMIT;
+import static org.simpleframework.transport.PhaseType.CONSUME;
+import static org.simpleframework.transport.PhaseType.PRODUCE;
+import static org.simpleframework.transport.TransportEvent.ERROR;
+import static org.simpleframework.transport.TransportEvent.HANDSHAKE_BEGIN;
+import static org.simpleframework.transport.TransportEvent.HANDSHAKE_DONE;
+import static org.simpleframework.transport.TransportEvent.HANDSHAKE_FAILED;
+import static org.simpleframework.transport.TransportEvent.READ;
+import static org.simpleframework.transport.TransportEvent.WRITE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.Future;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+
+import org.simpleframework.transport.reactor.Reactor;
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>Handshake</code> object is used to perform secure SSL
+ * negotiations on a pipeline or <code>Transport</code>. This can
+ * be used to perform an SSL handshake. To perform the negotiation
+ * this uses an SSL engine provided with the transport to direct
+ * the conversation. The SSL engine tells the negotiation what is
+ * expected next, whether this is a response to the client or a
+ * message from it. During the negotiation this may need to wait
+ * for either a write ready event or a read ready event. Event
+ * notification is done using the processor provided.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.TransportProcessor
+ */
+class Handshake implements Negotiation {
+
+ /**
+ * This is the processor used to process the secure transport.
+ */
+ private final TransportProcessor processor;
+
+ /**
+ * This is the certificate associated with this negotiation.
+ */
+ private final NegotiationState state;
+
+ /**
+ * This is the socket channel used to read and write data to.
+ */
+ private final SocketChannel channel;
+
+ /**
+ * This is the transport dispatched when the negotiation ends.
+ */
+ private final Transport transport;
+
+ /**
+ * This is the reactor used to register for I/O notifications.
+ */
+ private final Reactor reactor;
+
+ /**
+ * This is the output buffer used to generate data to.
+ */
+ private final ByteBuffer output;
+
+ /**
+ * This is the input buffer used to read data from the socket.
+ */
+ private final ByteBuffer input;
+
+ /**
+ * This is an empty byte buffer used to generate a response.
+ */
+ private final ByteBuffer empty;
+
+ /**
+ * This is the SSL engine used to direct the conversation.
+ */
+ private final SSLEngine engine;
+
+ /**
+ * This is the trace that is used to monitor handshake events.
+ */
+ private final Trace trace;
+
+ /**
+ * This determines if the handshake is from the client side.
+ */
+ private final boolean client;
+
+ /**
+ * Constructor for the <code>Handshake</code> object. This is
+ * used to create an operation capable of performing negotiations
+ * for SSL connections. Typically this is used to perform request
+ * response negotiations, such as a handshake or termination.
+ *
+ * @param processor the processor used to dispatch the transport
+ * @param transport the transport to perform the negotiation for
+ * @param reactor this is the reactor used for I/O notifications
+ */
+ public Handshake(TransportProcessor processor, Transport transport, Reactor reactor) {
+ this(processor, transport, reactor, 20480);
+ }
+
+ /**
+ * Constructor for the <code>Handshake</code> object. This is
+ * used to create an operation capable of performing negotiations
+ * for SSL connections. Typically this is used to perform request
+ * response negotiations, such as a handshake or termination.
+ *
+ * @param transport the transport to perform the negotiation for
+ * @param processor the processor used to dispatch the transport
+ * @param reactor this is the reactor used for I/O notifications
+ * @param size the size of the buffers used for the negotiation
+ */
+ public Handshake(TransportProcessor processor, Transport transport, Reactor reactor, int size) {
+ this(processor, transport, reactor, size, false);
+ }
+
+ /**
+ * Constructor for the <code>Handshake</code> object. This is
+ * used to create an operation capable of performing negotiations
+ * for SSL connections. Typically this is used to perform request
+ * response negotiations, such as a handshake or termination.
+ *
+ * @param transport the transport to perform the negotiation for
+ * @param processor the processor used to dispatch the transport
+ * @param reactor this is the reactor used for I/O notifications
+ * @param client determines the side of the SSL handshake
+ */
+ public Handshake(TransportProcessor processor, Transport transport, Reactor reactor, boolean client) {
+ this(processor, transport, reactor, 20480, client);
+ }
+
+ /**
+ * Constructor for the <code>Handshake</code> object. This is
+ * used to create an operation capable of performing negotiations
+ * for SSL connections. Typically this is used to perform request
+ * response negotiations, such as a handshake or termination.
+ *
+ * @param transport the transport to perform the negotiation for
+ * @param processor the processor used to dispatch the transport
+ * @param reactor this is the reactor used for I/O notifications
+ * @param size the size of the buffers used for the negotiation
+ * @param client determines the side of the SSL handshake
+ */
+ public Handshake(TransportProcessor processor, Transport transport, Reactor reactor, int size, boolean client) {
+ this.state = new NegotiationState(this, transport);
+ this.output = ByteBuffer.allocate(size);
+ this.input = ByteBuffer.allocate(size);
+ this.channel = transport.getChannel();
+ this.engine = transport.getEngine();
+ this.trace = transport.getTrace();
+ this.empty = ByteBuffer.allocate(0);
+ this.processor = processor;
+ this.transport = transport;
+ this.reactor = reactor;
+ this.client = client;
+ }
+
+ /**
+ * 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 returns the socket channel for the connected pipeline. It
+ * is this channel that is used to determine if there are bytes
+ * that can be read. When closed this is no longer selectable.
+ *
+ * @return this returns the connected channel for the pipeline
+ */
+ public SelectableChannel getChannel() {
+ return channel;
+ }
+
+ /**
+ * This is used to start the negotiation. Once started this will
+ * send a message to the other side, once sent the negotiation
+ * reads the response. However if the response is not yet ready
+ * this will schedule the negotiation for a selectable operation
+ * ensuring that it can resume execution when ready.
+ */
+ public void run() {
+ if(engine != null) {
+ trace.trace(HANDSHAKE_BEGIN);
+ engine.setUseClientMode(client);
+ input.flip();
+ }
+ begin();
+ }
+
+ /**
+ * This is used to terminate the negotiation. This is excecuted
+ * when the negotiation times out. When the negotiation expires it
+ * is rejected by the processor and must be canceled. Canceling
+ * is basically termination of the connection to free resources.
+ */
+ public void cancel() {
+ try {
+ terminate();
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+
+ /**
+ * This is used to start the negotation. Once started this will
+ * send a message to the other side, once sent the negotiation
+ * reads the response. However if the response is not yet ready
+ * this will schedule the negotiation for a selectable operation
+ * ensuring that it can resume execution when ready.
+ */
+ private void begin() {
+ try {
+ resume();
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ cancel();
+ }
+ }
+
+ /**
+ * This is the main point of execution within the negotiation. It
+ * is where the negotiation is performed. Negotiations are done
+ * by performing a request response flow, governed by the SSL
+ * engine associated with the pipeline. Typically the client is
+ * the one to initiate the handshake and the server initiates the
+ * termination sequence. This may be executed several times
+ * depending on whether reading or writing blocks.
+ */
+ public void resume() throws IOException {
+ Runnable task = process();
+
+ if(task != null) {
+ task.run();
+ }
+ }
+
+ /**
+ * This is the main point of execution within the negotiation. It
+ * is where the negotiation is performed. Negotiations are done
+ * by performing a request response flow, governed by the SSL
+ * engine associated with the transport. Typically the client is
+ * the one to initiate the handshake and the server initiates the
+ * termination sequence. This may be executed several times
+ * depending on whether reading or writing blocks.
+ *
+ * @return this returns a task used to execute the next phase
+ */
+ private Runnable process() throws IOException {
+ PhaseType require = exchange();
+
+ if(require == CONSUME) {
+ return new Consumer(this, reactor, trace);
+ }
+ if(require == PRODUCE) {
+ return new Producer(this, reactor, trace);
+ }
+ return new Committer(this, reactor, trace);
+ }
+
+ /**
+ * This is the main point of execution within the negotiation. It
+ * is where the negotiation is performed. Negotiations are done
+ * by performing a request response flow, governed by the SSL
+ * engine associated with the transport. Typically the client is
+ * the one to initiate the handshake and the server initiates the
+ * termination sequence. This may be executed several times
+ * depending on whether reading or writing blocks.
+ *
+ * @return this returns what is expected next in the negotiation
+ */
+ private PhaseType exchange() throws IOException {
+ HandshakeStatus status = engine.getHandshakeStatus();
+
+ switch(status){
+ case NEED_WRAP:
+ return write();
+ case NOT_HANDSHAKING:
+ case NEED_UNWRAP:
+ return read();
+ }
+ return COMMIT;
+ }
+
+ /**
+ * This is used to perform the read part of the negotiation. The
+ * read part is where the other side sends information where it
+ * is consumed and is used to determine what action to take.
+ * Typically it is the SSL engine that determines what action is
+ * to be taken depending on the data send from the other side.
+ *
+ * @return the next action that should be taken by the handshake
+ */
+ private PhaseType read() throws IOException {
+ return read(5);
+ }
+
+ /**
+ * This is used to perform the read part of the negotiation. The
+ * read part is where the other side sends information where it
+ * is consumed and is used to determine what action to take.
+ * Typically it is the SSL engine that determines what action is
+ * to be taken depending on the data send from the other side.
+ *
+ * @param count this is the number of times a read can repeat
+ *
+ * @return the next action that should be taken by the handshake
+ */
+ private PhaseType read(int count) throws IOException {
+ while(count > 0) {
+ SSLEngineResult result = engine.unwrap(input, output);
+ HandshakeStatus status = result.getHandshakeStatus();
+
+ switch(status) {
+ case NOT_HANDSHAKING:
+ return COMMIT;
+ case NEED_WRAP:
+ return PRODUCE;
+ case FINISHED:
+ case NEED_UNWRAP:
+ return read(count-1);
+ case NEED_TASK:
+ execute();
+ }
+ }
+ return CONSUME;
+ }
+
+ /**
+ * This is used to perform the write part of the negotiation. The
+ * read part is where the this sends information to the other side
+ * and the other side interprets the data and determines what action
+ * to take. After a write the negotiation typically completes or
+ * waits for the next response from the other side.
+ *
+ * @return the next action that should be taken by the handshake
+ */
+ private PhaseType write() throws IOException {
+ return write(5);
+ }
+
+ /**
+ * This is used to perform the write part of the negotiation. The
+ * read part is where the this sends information to the other side
+ * and the other side interprets the data and determines what action
+ * to take. After a write the negotiation typically completes or
+ * waits for the next response from the other side.
+ *
+ * @param count this is the number of times a read can repeat
+ *
+ * @return the next action that should be taken by the handshake
+ */
+ private PhaseType write(int count) throws IOException {
+ while(count > 0) {
+ SSLEngineResult result = engine.wrap(empty, output);
+ HandshakeStatus status = result.getHandshakeStatus();
+
+ switch(status) {
+ case NOT_HANDSHAKING:
+ case FINISHED:
+ case NEED_UNWRAP:
+ return PRODUCE;
+ case NEED_WRAP:
+ return write(count-1);
+ case NEED_TASK:
+ execute();
+ }
+ }
+ return PRODUCE;
+ }
+
+ /**
+ * This is used to execute the delegated tasks. These tasks are
+ * used to digest the information received from the client in
+ * order to generate a response. This may need to execute several
+ * tasks from the associated SSL engine.
+ */
+ private void execute() throws IOException {
+ while(true) {
+ Runnable task = engine.getDelegatedTask();
+
+ if(task == null) {
+ break;
+ }
+ task.run();
+ }
+ }
+
+ /**
+ * This is used to receive data from the client. If at any
+ * point during the negotiation a message is required that
+ * can not be read immediately this is used to asynchronously
+ * read the data when a select operation is signalled.
+ *
+ * @return this returns true when the message has been read
+ */
+ public boolean receive() throws IOException {
+ int count = input.capacity();
+
+ if(count > 0) {
+ input.compact();
+ }
+ int size = channel.read(input);
+
+ if(trace != null) {
+ trace.trace(READ, size);
+ }
+ if(size < 0) {
+ throw new TransportException("Client closed connection");
+ }
+ if(count > 0) {
+ input.flip();
+ }
+ return size > 0;
+ }
+
+ /**
+ * Here we attempt to send all data within the output buffer. If
+ * all of the data is delivered to the other side then this will
+ * return true. If however there is content yet to be sent to
+ * the other side then this returns false, telling the negotiation
+ * that in order to resume it must attempt to send the content
+ * again after a write ready operation on the underlying socket.
+ *
+ * @return this returns true if all of the content is delivered
+ */
+ public boolean send() throws IOException {
+ int require = output.position();
+ int count = 0;
+
+ if(require > 0) {
+ output.flip();
+ }
+ while(count < require) {
+ int size = channel.write(output);
+
+ if(trace != null) {
+ trace.trace(WRITE, size);
+ }
+ if(size <= 0) {
+ break;
+ }
+ count += size;
+ }
+ if(require > 0) {
+ output.compact();
+ }
+ return count == require;
+ }
+
+ /**
+ * This method is invoked when the negotiation is done and the
+ * next phase of the connection is to take place. This will
+ * be invoked when the SSL handshake has completed and the new
+ * secure transport is to be handed to the processor.
+ */
+ private void dispatch() throws IOException {
+ Transport secure = new SecureTransport(transport, state, output, input);
+
+ if(processor != null) {
+ trace.trace(HANDSHAKE_DONE);
+ processor.process(secure);
+ }
+ }
+
+ /**
+ * This method is used to terminate the handshake. Termination
+ * typically occurs when there has been some error in the handshake
+ * or when there is a timeout on some event, such as waiting for
+ * for a read or write operation to occur. As a result the TCP
+ * channel is closed and any challenge future is cancelled.
+ */
+ private void terminate() throws IOException {
+ Future<Certificate> future = state.getFuture();
+
+ trace.trace(HANDSHAKE_FAILED);
+ transport.close();
+ future.cancel(true);
+ }
+
+ /**
+ * This is used to execute the completion task after a challenge
+ * for the clients X509 certificate. Execution of the completion
+ * task in this way allows any challanger to be notified that
+ * the handshake has complete.
+ */
+ private void complete() throws IOException {
+ Runnable task = state.getFuture();
+
+ if(task != null) {
+ task.run();
+ }
+ }
+
+ /**
+ * This method is invoked when the negotiation is done and the
+ * next phase of the connection is to take place. If a certificate
+ * challenge was issued then the completion task is executed, if
+ * this was the handshake for the initial connection a transport
+ * is created and handed to the processor.
+ */
+ public void commit() throws IOException {
+ if(!state.isChallenge()) {
+ dispatch();
+ } else {
+ complete();
+ }
+ }
+
+ /**
+ * The <code>Committer</code> task is used to transfer the transport
+ * created to the processor. This is executed when the SSL
+ * handshake is completed. It allows the transporter to use the
+ * newly created transport to read and write in plain text and
+ * to have the SSL transport encrypt and decrypt transparently.
+ */
+ private class Committer extends Phase {
+
+ /**
+ * Constructor for the <code>Committer</code> task. This is used to
+ * pass the transport object object to the processor when the
+ * SSL handshake has completed.
+ *
+ * @param state this is the underlying negotiation to use
+ * @param reactor this is the reactor used for I/O notifications
+ * @param trace the trace that is used to monitor the handshake
+ */
+ public Committer(Negotiation state, Reactor reactor, Trace trace) {
+ super(state, reactor, trace, OP_READ);
+ }
+
+ /**
+ * This is used to execute the task. It is up to the specific
+ * task implementation to decide what to do when executed. If
+ * the task needs to read or write data then it can attempt
+ * to perform the read or write, if it incomplete the it can
+ * be scheduled for execution with the reactor.
+ */
+ @Override
+ public void execute() throws IOException{
+ state.commit();
+ }
+ }
+
+ /**
+ * The <code>Consumer</code> task is used to schedule the negotiation
+ * for a read operation. This allows the negotiation to receive any
+ * messages generated by the client asynchronously. Once this has
+ * completed then it will resume the negotiation.
+ */
+ private class Consumer extends Phase {
+
+ /**
+ * Constructor for the <code>Consumer</code> task. This is used
+ * to create a task which will schedule a read operation for
+ * the negotiation. When the operation completes this will
+ * resume the negotiation.
+ *
+ * @param state this is the negotiation object that is used
+ * @param reactor this is the reactor used for I/O notifications
+ * @param trace the trace that is used to monitor the handshake
+ */
+ public Consumer(Negotiation state, Reactor reactor, Trace trace) {
+ super(state, reactor, trace, OP_READ);
+ }
+
+ /**
+ * This method is used to determine if the task is ready. This
+ * is executed when the select operation is signalled. When this
+ * is true the the task completes. If not then this will
+ * schedule the task again for the specified select operation.
+ *
+ * @return this returns true when the task has completed
+ */
+ @Override
+ protected boolean ready() throws IOException {
+ return state.receive();
+ }
+ }
+
+ /**
+ * The <code>Producer</code> is used to schedule the negotiation
+ * for a write operation. This allows the negotiation to send any
+ * messages generated during the negotiation asynchronously. Once
+ * this has completed then it will resume the negotiation.
+ */
+ private class Producer extends Phase {
+
+ /**
+ * Constructor for the <code>Producer</code> task. This is used
+ * to create a task which will schedule a write operation for
+ * the negotiation. When the operation completes this will
+ * resume the negotiation.
+ *
+ * @param state this is the negotiation object that is used
+ * @param reactor this is the reactor used for I/O notifications
+ * @param trace the trace that is used to monitor the handshake
+ */
+ public Producer(Negotiation state, Reactor reactor, Trace trace) {
+ super(state, reactor, trace, OP_WRITE);
+ }
+
+ /**
+ * This method is used to determine if the task is ready. This
+ * is executed when the select operation is signalled. When this
+ * is true the the task completes. If not then this will
+ * schedule the task again for the specified select operation.
+ *
+ * @return this returns true when the task has completed
+ */
+ @Override
+ protected boolean ready() throws IOException {
+ return state.send();
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/Negotiation.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/Negotiation.java
new file mode 100644
index 00000000..4140b345
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/Negotiation.java
@@ -0,0 +1,69 @@
+/*
+ * Negotiation.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+import org.simpleframework.transport.reactor.Operation;
+
+/**
+ * The <code>Negotiation</code> interface is used to represent an
+ * SSL negotiation. When an operation can not be completed this
+ * will allow a task to perform asynchronous operations and resume
+ * the negotiation when those operations can be fulfilled.
+ *
+ * @author Niall Gallagher
+ */
+interface Negotiation extends Operation {
+
+ /**
+ * This is used to resume the negotiation when an operation
+ * has completed. This will continue the decrypt and encrypt
+ * sequence of messages required to fulfil the negotiation.
+ */
+ void resume() throws IOException;
+
+ /**
+ * This method is invoked when the negotiation is done and
+ * the next phase of the connection is to take place. This
+ * will typically be invoked when an SSL handshake or
+ * termination exchange has completed successfully.
+ */
+ void commit() throws IOException;
+
+ /**
+ * This is used to send any messages the negotiation may have.
+ * If the negotiation can not send the information during its
+ * execution then this method will be executed when a select
+ * operation is signaled.
+ *
+ * @return this returns true when the message has been sent
+ */
+ boolean send() throws IOException;
+
+ /**
+ * This is used to receive data from the other side. If at any
+ * point during the negotiation a message is required that
+ * can not be read immediately this is used to asynchronously
+ * read the data when a select operation is signaled.
+ *
+ * @return this returns true when the message has been read
+ */
+ boolean receive() throws IOException;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/NegotiationState.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/NegotiationState.java
new file mode 100644
index 00000000..160b5f91
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/NegotiationState.java
@@ -0,0 +1,337 @@
+/*
+ * NegotiationCertificate.java June 2013
+ *
+ * Copyright (C) 2013, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static org.simpleframework.transport.TransportEvent.CERTIFICATE_CHALLENGE;
+import static org.simpleframework.transport.TransportEvent.ERROR;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RunnableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+import javax.security.cert.X509Certificate;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>NegotiationState</code> represents the certificate
+ * that is sent by a client during a secure HTTPS conversation. This
+ * may or may not contain an X509 certificate chain from the client.
+ * If it does not a <code>CertificateChallenge</code> may be used to
+ * issue a renegotiation of the connection. One completion of the
+ * renegotiation the challenge executes a completion operation.
+ *
+ * @author Niall Gallagher
+ */
+class NegotiationState implements Certificate {
+
+ /**
+ * This is used to hold the completion task for the challenge.
+ */
+ private final RunnableFuture<Certificate> future;
+
+ /**
+ * This is the handshake used to acquire the certificate details.
+ */
+ private final Negotiation negotiation;
+
+ /**
+ * This is the challenge used to request the client certificate.
+ */
+ private final Challenge challenge;
+
+ /**
+ * This is the runnable task that is executed on task completion.
+ */
+ private final Delegate delegate;
+
+ /**
+ * This is the socket representing the underlying TCP connection.
+ */
+ private final Socket socket;
+
+ /**
+ * Constructor for the <code>NegotiationCertificate</code> object.
+ * This creates an object used to provide certificate details and
+ * a means to challenge for certificate details for the connected
+ * client if required.
+ *
+ * @param negotiation the negotiation associated with this
+ * @param socket the underlying TCP connection to the client
+ */
+ public NegotiationState(Negotiation negotiation, Socket socket) {
+ this.delegate = new Delegate(socket);
+ this.future = new FutureTask<Certificate>(delegate, this);
+ this.challenge = new Challenge(socket);
+ this.negotiation = negotiation;
+ this.socket = socket;
+ }
+
+ /**
+ * This is used to determine if the state is in challenge mode.
+ * In challenge mode a challenge future will be executed on
+ * completion of the challenge. This will the completion task.
+ *
+ * @return this returns true if the state is in challenge mode
+ */
+ public boolean isChallenge() {
+ return delegate.isSet();
+ }
+
+ /**
+ * This returns the completion task associated with any challenge
+ * made for the client certificate. If this returns null then no
+ * challenge has been made for the client certificate.
+ *
+ * @return this returns the challenge completion task if any
+ */
+ public RunnableFuture<Certificate> getFuture() {
+ return future;
+ }
+
+ /**
+ * This returns a challenge for the certificate. A challenge is
+ * issued by providing a <code>Runnable</code> 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 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 {
+ SSLSession session = getSession();
+
+ if(session != null) {
+ return session.getPeerCertificateChain();
+ }
+ return null;
+ }
+
+ /**
+ * This is used to acquire the SSL session associated with the
+ * handshake. The session makes all of the details associated
+ * with the handshake available, including the cipher suites
+ * used and the SSL context used to create the session.
+ *
+ * @return the SSL session associated with the connection
+ */
+ public SSLSession getSession() throws Exception{
+ SSLEngine engine = socket.getEngine();
+
+ if(engine != null) {
+ return engine.getSession();
+ }
+ return null;
+ }
+
+ /**
+ * 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() {
+ try {
+ return getChain() != null;
+ } catch(Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * The <code>Challenge</code> object is used to enable the server
+ * to challenge for the client X509 certificate if desired. It
+ * performs the challenge by performing an SSL renegotiation to
+ * request that the client sends the
+ */
+ private class Challenge implements CertificateChallenge {
+
+ /**
+ * This is the SSL engine that is used to begin the handshake.
+ */
+ private final SSLEngine engine;
+
+ /**
+ * This is used to trace the certificate challenge request.
+ */
+ private final Trace trace;
+
+ /**
+ * Constructor for the <code>Challenge</code> object. This can
+ * be used to challenge the client for their X509 certificate.
+ * It does this by performing an SSL renegotiation on the
+ * existing TCP connection.
+ *
+ * @param socket this is the TCP connection to the client
+ */
+ public Challenge(Socket socket) {
+ this.engine = socket.getEngine();
+ this.trace = socket.getTrace();
+ }
+
+ /**
+ * 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.
+ */
+ public Future<Certificate> challenge() {
+ 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<Certificate> challenge(Runnable task) {
+ try {
+ if(!isChainPresent()) {
+ resume(task);
+ } else {
+ future.run();
+ }
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ return future;
+ }
+
+ /**
+ * 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
+ */
+ private void resume(Runnable task) {
+ try {
+ trace.trace(CERTIFICATE_CHALLENGE);
+ delegate.set(task);
+ engine.setNeedClientAuth(true);
+ engine.beginHandshake();
+ negotiation.resume();
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ negotiation.cancel();
+ }
+ }
+ }
+
+ /**
+ * The <code>Delegate</code> is basically a settable runnable object.
+ * It enables the challenge to set an optional runnable that will
+ * be executed when the challenge has completed. If the challenge
+ * has not been given a completion task this runs straight through
+ * without any state change or action on the certificate.
+ */
+ private class Delegate implements Runnable {
+
+ /**
+ * This is the reference to the runnable that is to be executed.
+ */
+ private final AtomicReference<Runnable> task;
+
+ /**
+ * This is used to determine if the challenge is ready to run.
+ */
+ private final AtomicBoolean ready;
+
+ /**
+ * This is used to trace any errors when running the task.
+ */
+ private final Trace trace;
+
+ /**
+ * Constructor for the <code>Delegate</code> object. This is
+ * used to create a wrapper for the completion task so that it
+ * can be executed safely and have any errors traced.
+ *
+ * @param socket this socket the handshake is associated with
+ */
+ public Delegate(Socket socket) {
+ this.task = new AtomicReference<Runnable>();
+ this.ready = new AtomicBoolean();
+ this.trace = socket.getTrace();
+ }
+
+ /**
+ * This is used to determine if the delegate is ready to be
+ * used. It is ready only after the completion task has been
+ * set. When ready a challenge can be executed.
+ *
+ * @return this returns true if a completion task is set
+ */
+ public boolean isSet() {
+ return ready.get();
+ }
+
+ /**
+ * This is used to set the completion task that is to be executed
+ * when the challenge has finished. This can be set to null if
+ * no task is to be executed on completion.
+ *
+ * @param runnable the task to run when the challenge finishes
+ */
+ public void set(Runnable runnable) {
+ ready.set(true);
+ task.set(runnable);
+ }
+
+ /**
+ * This is used to run the completion task. If no completion
+ * task has been set this will run through without any change to
+ * the state of the certificate. All errors thrown by the task
+ * will be caught and traced.
+ */
+ public void run() {
+ try {
+ Runnable runnable = task.get();
+
+ if(runnable != null) {
+ runnable.run();
+ }
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ } finally {
+ task.set(null);
+ }
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/OperationFactory.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/OperationFactory.java
new file mode 100644
index 00000000..343a12b9
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/OperationFactory.java
@@ -0,0 +1,150 @@
+/*
+ * OperationFactory.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+import javax.net.ssl.SSLEngine;
+
+import org.simpleframework.transport.reactor.Operation;
+import org.simpleframework.transport.reactor.Reactor;
+
+/**
+ * The <code>OperationFactory</code> is used to create operations
+ * for the transport processor. Depending on the configuration of the
+ * pipeline object this will create different operations. Typically
+ * this will create an SSL handshake operation if the pipeline has
+ * an <code>SSLEngine</code> instance. This allows the transport
+ * processor to complete the handshake before handing the transport
+ * to the transporter for processing.
+ *
+ * @author Niall Gallagher
+ */
+class OperationFactory {
+
+ /**
+ * This is the processor used to process the created transport.
+ */
+ private final TransportProcessor processor;
+
+ /**
+ * This is the reactor used to register for I/O notifications.
+ */
+ private final Reactor reactor;
+
+ /**
+ * This is the threshold for the asynchronous buffers to use.
+ */
+ private final int threshold;
+
+ /**
+ * This is the size of the buffers to be used by the transport.
+ */
+ private final int buffer;
+
+ /**
+ * This determines if the SSL handshake is for the client side.
+ */
+ private final boolean client;
+
+ /**
+ * Constructor for the <code>OperationFactory</code> object. This
+ * uses the processor provided to hand off the created transport
+ * when it has been created. All operations created typically
+ * execute in an asynchronous thread.
+ *
+ * @param processor the processor used to dispatch the transport
+ * @param reactor this is the reactor used for I/O notifications
+ * @param buffer this is the initial size of the buffer to use
+ */
+ public OperationFactory(TransportProcessor processor, Reactor reactor, int buffer) {
+ this(processor, reactor, buffer, 20480);
+ }
+
+ /**
+ * Constructor for the <code>OperationFactory</code> object. This
+ * uses the processor provided to hand off the created transport
+ * when it has been created. All operations created typically
+ * execute in an asynchronous thread.
+ *
+ * @param processor the processor used to dispatch the transport
+ * @param reactor this is the reactor used for I/O notifications
+ * @param buffer this is the initial size of the buffer to use
+ * @param threshold maximum size of the output buffer to use
+ */
+ public OperationFactory(TransportProcessor processor, Reactor reactor, int buffer, int threshold) {
+ this(processor, reactor, buffer, threshold, false);
+ }
+
+ /**
+ * Constructor for the <code>OperationFactory</code> object. This
+ * uses the processor provided to hand off the created transport
+ * when it has been created. All operations created typically
+ * execute in an asynchronous thread.
+ *
+ * @param processor the processor used to dispatch the transport
+ * @param reactor this is the reactor used for I/O notifications
+ * @param buffer this is the initial size of the buffer to use
+ * @param threshold maximum size of the output buffer to use
+ * @param client determines if the SSL handshake is for a client
+ */
+ public OperationFactory(TransportProcessor processor, Reactor reactor, int buffer, int threshold, boolean client) {
+ this.processor = processor;
+ this.threshold = threshold;
+ this.reactor = reactor;
+ this.buffer = buffer;
+ this.client = client;
+ }
+
+ /**
+ * This method is used to create <code>Operation</code> object to
+ * process the next phase of the negotiation. The operations that
+ * are created using this factory ensure the processing can be
+ * done asynchronously, which reduces the overhead the connection
+ * thread has when handing the pipelines over for processing.
+ *
+ * @param socket this is the pipeline that is to be processed
+ *
+ * @return this returns the operation used for processing
+ */
+ public Operation getInstance(Socket socket) throws IOException {
+ return getInstance(socket, socket.getEngine());
+ }
+
+ /**
+ * This method is used to create <code>Operation</code> object to
+ * process the next phase of the negotiation. The operations that
+ * are created using this factory ensure the processing can be
+ * done asynchronously, which reduces the overhead the connection
+ * thread has when handing the pipelines over for processing.
+ *
+ * @param socket this is the pipeline that is to be processed
+ * @param engine this is the engine used for SSL negotiations
+ *
+ * @return this returns the operation used for processing
+ */
+ private Operation getInstance(Socket socket, SSLEngine engine) throws IOException {
+ Transport transport = new SocketTransport(socket, reactor, buffer, threshold);
+
+ if(engine != null) {
+ return new Handshake(processor, transport, reactor, client);
+ }
+ return new TransportDispatcher(processor, transport);
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/Phase.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/Phase.java
new file mode 100644
index 00000000..a2bb2cd5
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/Phase.java
@@ -0,0 +1,165 @@
+/*
+ * Phase.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static org.simpleframework.transport.TransportEvent.ERROR;
+
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+
+import org.simpleframework.transport.reactor.Operation;
+import org.simpleframework.transport.reactor.Reactor;
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>Phase</code> object represents an asynchronous phase
+ * within the negotiation. This is typically used to either schedule
+ * an asynchronous read or write when it can not be performed
+ * directly. It ensures that the negotiation does not block the
+ * thread so that execution can be optimized of high concurrency.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.Handshake
+ */
+abstract class Phase implements Operation {
+
+ /**
+ * This is the negotiation that this task will operate on.
+ */
+ protected final Negotiation state;
+
+ /**
+ * This is the reactor that is used to schedule execution.
+ */
+ protected final Reactor reactor;
+
+ /**
+ * This is the trace used to monitor the handshake socket.
+ */
+ protected final Trace trace;
+
+ /**
+ * This is the required operation for the task to complete.
+ */
+ protected final int require;
+
+ /**
+ * Constructor for the <code>Phase</code> object. This is used to
+ * create an operation that performs some phase of a negotiation.
+ * It allows the negotiation to schedule the read and write
+ * operations asynchronously.
+ *
+ * @param state this is the negotiation this task works on
+ * @param reactor this is the reactor used to schedule the task
+ * @param trace the trace that is used to monitor the handshake
+ * @param require this is the required operation for the task
+ */
+ public Phase(Negotiation state, Reactor reactor, Trace trace, int require) {
+ this.reactor = reactor;
+ this.require = require;
+ this.state = state;
+ this.trace = trace;
+ }
+
+ /**
+ * 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 <code>SelectableChannel</code> which is used to
+ * determine if the operation should be executed. If the channel
+ * is ready for a given I/O event it can be run. For instance if
+ * the operation is used to perform some form of read operation
+ * it can be executed when ready to read data from the channel.
+ *
+ * @return this returns the channel used to govern execution
+ */
+ public SelectableChannel getChannel() {
+ return state.getChannel();
+ }
+
+ /**
+ * This is used to execute the task. It is up to the specific
+ * task implementation to decide what to do when executed. If
+ * the task needs to read or write data then it can attempt
+ * to perform the read or write, if it incomplete the it can
+ * be scheduled for execution with the reactor.
+ */
+ public void run() {
+ try {
+ execute();
+ }catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ cancel();
+ }
+ }
+
+ /**
+ * This is used to cancel the operation if it has timed out. This
+ * is typically invoked when it has been waiting in a selector for
+ * an extended duration of time without any active operations on
+ * it. In such a case the reactor must purge the operation to free
+ * the memory and open channels associated with the operation.
+ */
+ public void cancel() {
+ try {
+ state.cancel();
+ }catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+
+ /**
+ * This is used to execute the task. It is up to the specific
+ * task implementation to decide what to do when executed. If
+ * the task needs to read or write data then it can attempt
+ * to perform the read or write, if it incomplete the it can
+ * be scheduled for execution with the reactor.
+ */
+ protected void execute() throws IOException {
+ boolean done = ready();
+
+ if(!done) {
+ reactor.process(this, require);
+ } else {
+ state.resume();
+ }
+ }
+
+ /**
+ * This method is used to determine if the task is ready. This is
+ * executed when the select operation is signaled. When this is
+ * true the the task completes. If not then this will schedule
+ * the task again for the specified select operation.
+ *
+ * @return this returns true when the task has completed
+ */
+ protected boolean ready() throws IOException {
+ return true;
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/PhaseType.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/PhaseType.java
new file mode 100644
index 00000000..dd202d6b
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/PhaseType.java
@@ -0,0 +1,45 @@
+/*
+ * PhaseType.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+/**
+ * The <code>PhaseType</code> enumeration is used to determine what
+ * phase of the negotiation the handshake is in. This allows the
+ * negotiation to control the selection for read and write ready
+ * operations. Also, status signals completion of the handshake.
+ *
+ * @author Niall Gallagher
+ */
+enum PhaseType {
+
+ /**
+ * Tells the negotiation that a read operations is needed.
+ */
+ CONSUME,
+
+ /**
+ * Tells the negotiation that a write operation is required.
+ */
+ PRODUCE,
+
+ /**
+ * Tells the negotiation that the the handshake is complete.
+ */
+ COMMIT
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SecureTransport.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SecureTransport.java
new file mode 100644
index 00000000..20838735
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SecureTransport.java
@@ -0,0 +1,428 @@
+/*
+ * SecureTransport.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Map;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.Status;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>SecureTransport</code> object provides an implementation
+ * of a transport used to send and receive data over SSL. Data read
+ * from this transport is decrypted using an <code>SSLEngine</code>.
+ * Also, all data is written is encrypted with the same engine. This
+ * ensures that data can be send and received in a transparent way.
+ *
+ * @author Niall Gallagher
+ */
+class SecureTransport implements Transport {
+
+ /**
+ * This is the certificate associated with this SSL connection.
+ */
+ private Certificate certificate;
+
+ /**
+ * This is the transport used to send data over the socket.
+ */
+ private Transport transport;
+
+ /**
+ * This buffer is used to output the data for the SSL sent.
+ */
+ private ByteBuffer output;
+
+ /**
+ * This is the internal buffer used to exchange the SSL data.
+ */
+ private ByteBuffer input;
+
+ /**
+ * This is the internal buffer used to exchange the SSL data.
+ */
+ private ByteBuffer swap;
+
+ /**
+ * This is the SSL engine used to encrypt and decrypt data.
+ */
+ private SSLEngine engine;
+
+ /**
+ * This is the trace that is used to monitor socket activity.
+ */
+ private Trace trace;
+
+ /**
+ * This is used to determine if the transport was closed.
+ */
+ private boolean closed;
+
+ /**
+ * This is used to determine if the end of stream was reached.
+ */
+ private boolean finished;
+
+ /**
+ * Constructor for the <code>SecureTransport</code> object. This
+ * is used to create a transport for sending and receiving data
+ * over SSL. This must be created with a pipeline that has already
+ * performed the SSL handshake and is read to used.
+ *
+ * @param transport this is the transport to delegate operations to
+ * @param certificate this is the certificate for the connection
+ * @param input this is the input buffer used to read the data
+ * @param swap this is the swap buffer to be used for reading
+ */
+ public SecureTransport(Transport transport, Certificate certificate, ByteBuffer input, ByteBuffer swap) {
+ this(transport, certificate, input, swap, 20480);
+ }
+
+ /**
+ * Constructor for the <code>SecureTransport</code> object. This
+ * is used to create a transport for sending and receiving data
+ * over SSL. This must be created with a pipeline that has already
+ * performed the SSL handshake and is read to used.
+ *
+ * @param transport this is the transport to delegate operations to
+ * @param certificate this is the certificate for the connection
+ * @param input this is the input buffer used to read the data
+ * @param swap this is the swap buffer to be used for reading
+ * @param size this is the size of the buffers to be allocated
+ */
+ public SecureTransport(Transport transport, Certificate certificate, ByteBuffer input, ByteBuffer swap, int size) {
+ this.output = ByteBuffer.allocate(size);
+ this.engine = transport.getEngine();
+ this.trace = transport.getTrace();
+ this.certificate = certificate;
+ this.transport = transport;
+ this.input = input;
+ this.swap = swap;
+ }
+
+ /**
+ * 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 getCertificate() {
+ return certificate;
+ }
+
+ /**
+ * This is used to acquire the trace object that is associated
+ * with the socket. A trace object is used to collection details
+ * on what operations are being performed on the socket. For
+ * instance it may contain information relating to I/O events
+ * or more application specific events such as errors.
+ *
+ * @return this returns the trace associated with this socket
+ */
+ public Trace getTrace() {
+ return trace;
+ }
+
+ /**
+ * This is used to acquire the SSL engine used for HTTPS. If the
+ * pipeline is connected to an SSL transport this returns an SSL
+ * engine which can be used to establish the secure connection
+ * and send and receive content over that connection. If this is
+ * null then the pipeline represents a normal transport.
+ *
+ * @return the SSL engine used to establish a secure transport
+ */
+ public SSLEngine getEngine() {
+ return engine;
+ }
+
+ /**
+ * This method is used to get the <code>Map</code> of attributes
+ * by this pipeline. The attributes map is used to maintain details
+ * about the connection. Information such as security credentials
+ * to client details can be placed within the attribute map.
+ *
+ * @return this returns the map of attributes for this pipeline
+ */
+ public Map getAttributes() {
+ return transport.getAttributes();
+ }
+
+ /**
+ * This method is used to acquire the <code>SocketChannel</code>
+ * for the connection. This allows the server to acquire the input
+ * and output streams with which to communicate. It can also be
+ * used to configure the connection and perform various network
+ * operations that could otherwise not be performed.
+ *
+ * @return this returns the socket used by this HTTP pipeline
+ */
+ public SocketChannel getChannel() {
+ return transport.getChannel();
+ }
+
+ /**
+ * This is used to perform a non-blocking read on the transport.
+ * If there are no bytes available on the input buffers then
+ * this method will return zero and the buffer will remain the
+ * same. If there is data and the buffer can be filled then this
+ * will return the number of bytes read. Finally if the socket
+ * is closed this will return a -1 value.
+ *
+ * @param buffer this is the buffer to append the bytes to
+ *
+ * @return this returns the number of bytes that have been read
+ */
+ public int read(ByteBuffer buffer) throws IOException {
+ if(closed) {
+ throw new TransportException("Transport is closed");
+ }
+ if(finished) {
+ return -1;
+ }
+ int count = fill(buffer);
+
+ if(count <= 0) {
+ return process(buffer);
+ }
+ return count;
+ }
+
+ /**
+ * This is used to perform a non-blocking read on the transport.
+ * If there are no bytes available on the input buffers then
+ * this method will return zero and the buffer will remain the
+ * same. If there is data and the buffer can be filled then this
+ * will return the number of bytes read.
+ *
+ * @param buffer this is the buffer to append the bytes to
+ *
+ * @return this returns the number of bytes that have been read
+ */
+ private int process(ByteBuffer buffer) throws IOException {
+ int size = swap.position();
+
+ if(size >= 0) {
+ swap.compact();
+ }
+ int space = swap.remaining();
+
+ if(space > 0) {
+ size = transport.read(swap);
+
+ if(size < 0) {
+ finished = true;
+ }
+ }
+ if(size > 0 || space > 0) {
+ swap.flip();
+ receive();
+ }
+ return fill(buffer);
+ }
+
+ /**
+ * This is used to fill the provided buffer with data that has
+ * been read from the secure socket channel. This enables reading
+ * of the decrypted data in chunks that are smaller than the
+ * size of the input buffer used to contain the plain text data.
+ *
+ * @param buffer this is the buffer to append the bytes to
+ *
+ * @return this returns the number of bytes that have been read
+ */
+ private int fill(ByteBuffer buffer) throws IOException {
+ int space = buffer.remaining();
+ int count = input.position();
+
+ if(count > 0) {
+ if(count > space) {
+ count = space;
+ }
+ }
+ return fill(buffer, count);
+
+ }
+
+ /**
+ * This is used to fill the provided buffer with data that has
+ * been read from the secure socket channel. This enables reading
+ * of the decrypted data in chunks that are smaller than the
+ * size of the input buffer used to contain the plain text data.
+ *
+ * @param buffer this is the buffer to append the bytes to
+ * @param count this is the number of bytes that are to be read
+ *
+ * @return this returns the number of bytes that have been read
+ */
+ private int fill(ByteBuffer buffer, int count) throws IOException {
+ input.flip();
+
+ if(count > 0) {
+ count = append(buffer, count);
+ }
+ input.compact();
+ return count;
+ }
+
+ /**
+ * This will append bytes within the transport to the given buffer.
+ * Once invoked the buffer will contain the transport bytes, which
+ * will have been drained from the buffer. This effectively moves
+ * the bytes in the buffer to the end of the packet instance.
+ *
+ * @param buffer this is the buffer containing the bytes
+ * @param count this is the number of bytes that should be used
+ *
+ * @return returns the number of bytes that have been moved
+ */
+ private int append(ByteBuffer buffer, int count) throws IOException {
+ ByteBuffer segment = input.slice();
+
+ if(closed) {
+ throw new TransportException("Transport is closed");
+ }
+ int mark = input.position();
+ int size = mark + count;
+
+ if(count > 0) {
+ input.position(size);
+ segment.limit(count);
+ buffer.put(segment);
+ }
+ return count;
+ }
+
+ /**
+ * This is used to perform a non-blocking read on the transport.
+ * If there are no bytes available on the input buffers then
+ * this method will return zero and the buffer will remain the
+ * same. If there is data and the buffer can be filled then this
+ * will return the number of bytes read. Finally if the socket
+ * is closed this will return a -1 value.
+ */
+ private void receive() throws IOException {
+ int count = swap.remaining();
+
+ while(count > 0) {
+ SSLEngineResult result = engine.unwrap(swap, input);
+ Status status = result.getStatus();
+
+ switch(status) {
+ case BUFFER_OVERFLOW:
+ case BUFFER_UNDERFLOW:
+ return;
+ case CLOSED:
+ throw new TransportException("Transport error " + result);
+ }
+ count = swap.remaining();
+
+ if(count <= 0) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param buffer this is the array of bytes to send to the client
+ */
+ public void write(ByteBuffer buffer) throws IOException {
+ if(closed) {
+ throw new TransportException("Transport is closed");
+ }
+ int capacity = output.capacity();
+ int ready = buffer.remaining();
+ int length = ready;
+
+ while(ready > 0) {
+ int size = Math.min(ready, capacity / 2);
+ int mark = buffer.position();
+
+ if(length * 2 > capacity) {
+ buffer.limit(mark + size);
+ }
+ send(buffer);
+ output.clear();
+ ready -= size;
+ }
+ }
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param buffer this is the array of bytes to send to the client
+ */
+ private void send(ByteBuffer buffer) throws IOException {
+ SSLEngineResult result = engine.wrap(buffer, output);
+ Status status = result.getStatus();
+
+ switch(status){
+ case BUFFER_OVERFLOW:
+ case BUFFER_UNDERFLOW:
+ case CLOSED:
+ throw new TransportException("Transport error " + status);
+ default:
+ output.flip();
+ }
+ transport.write(output);
+ }
+
+ /**
+ * 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(closed) {
+ throw new TransportException("Transport is closed");
+ }
+ transport.flush();
+ }
+
+ /**
+ * This is used to close the sender and the underlying transport.
+ * If a close is performed on the sender then no more bytes can
+ * be read from or written to the transport and the client will
+ * received a connection close on their side.
+ */
+ public void close() throws IOException {
+ if(!closed) {
+ transport.close();
+ closed = true;
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/ServerCleaner.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/ServerCleaner.java
new file mode 100644
index 00000000..27aaf797
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/ServerCleaner.java
@@ -0,0 +1,86 @@
+/*
+ * ServerCleaner.java February 2009
+ *
+ * Copyright (C) 2009, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import org.simpleframework.common.thread.ConcurrentExecutor;
+import org.simpleframework.common.thread.Daemon;
+import org.simpleframework.transport.reactor.Reactor;
+
+/**
+ * The <code>ServerCleaner</code> object allows for the termination
+ * and resource recovery to be done asynchronously. This ensures that
+ * should a HTTP request be used to terminate the processor that it
+ * does not block waiting for the servicing thread pool to terminate
+ * causing a deadlock.
+ *
+ * @author Niall Gallagher
+ */
+class ServerCleaner extends Daemon {
+
+ /**
+ * This is the internal processor that is to be terminated.
+ */
+ private final TransportProcessor processor;
+
+ /**
+ * This is the thread pool implementation used by the server.
+ */
+ private final ConcurrentExecutor executor;
+
+ /**
+ * This is the internal write reactor that is terminated.
+ */
+ private final Reactor reactor;
+
+ /**
+ * Constructor for the <code>ServerCleaner</code> object. For an
+ * orderly termination of the processor, the processor and reactor
+ * provided to the constructor will be stopped asynchronously.
+ *
+ * @param processor this is the processor that is to be stopped
+ * @param executor this is the executor used by the server
+ * @param reactor this is the reactor that is to be closed
+ */
+ public ServerCleaner(TransportProcessor processor, ConcurrentExecutor executor, Reactor reactor) {
+ this.processor = processor;
+ this.executor = executor;
+ this.reactor = reactor;
+ }
+
+ /**
+ * When this method runs it will firstly stop the processor in
+ * a synchronous fashion. Once the <code>TransportProcessor</code>
+ * has stopped it will stop the <code>Reactor</code> ensuring that
+ * all threads will be released.
+ * <p>
+ * It is important to note that stopping the processor before
+ * stopping the reactor is required. This ensures that if there
+ * are any threads executing within the processor that require
+ * the reactor threads, they can complete without a problem.
+ */
+ public void run() {
+ try {
+ processor.stop();
+ executor.stop();
+ reactor.stop();
+ } catch(Exception e) {
+ return;
+ }
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/Socket.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/Socket.java
new file mode 100644
index 00000000..b59cb0fd
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/Socket.java
@@ -0,0 +1,89 @@
+/*
+ * Socket.java February 2001
+ *
+ * Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.nio.channels.SocketChannel;
+import java.util.Map;
+
+import javax.net.ssl.SSLEngine;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * This is a <code>Socket</code> interface that is used to represent
+ * a socket. This has a map that allows attributes to be associated
+ * with the client connection. Attributes such as security details
+ * or other transport related details can be exposed by placing them
+ * in the socket map. The <code>Processor</code> can then use these
+ * attributes as required.
+ * <p>
+ * This provides the connected <code>SocketChannel</code> that can
+ * be used to read and write data asynchronously. The socket channel
+ * must be selectable and in non-blocking mode. If the socket is not
+ * in a non-blocking state the connection will not be processed.
+ *
+ * @author Niall Gallagher
+ */
+public interface Socket {
+
+ /**
+ * This is used to acquire the trace object that is associated
+ * with the socket. A trace object is used to collection details
+ * on what operations are being performed on the socket. For
+ * instance it may contain information relating to I/O events
+ * or more application specific events such as errors.
+ *
+ * @return this returns the trace associated with this socket
+ */
+ Trace getTrace();
+
+ /**
+ * This is used to acquire the SSL engine used for security. If
+ * the socket is connected to an SSL transport this returns an
+ * SSL engine which can be used to establish the secure connection
+ * and send and receive content over that connection. If this is
+ * null then the socket represents a normal transport.
+ *
+ * @return the SSL engine used to establish a secure transport
+ */
+ SSLEngine getEngine();
+
+ /**
+ * This method is used to acquire the <code>SocketChannel</code>
+ * for the connection. This allows the server to acquire the input
+ * and output streams with which to communicate. It can also be
+ * used to configure the connection and perform various network
+ * operations that could otherwise not be performed.
+ *
+ * @return this returns the socket used by this socket
+ */
+ SocketChannel getChannel();
+
+ /**
+ * This method is used to get the <code>Map</code> of attributes
+ * for this socket. The attributes map is used to maintain details
+ * about the connection. Information such as security credentials
+ * to client details can be placed within the attribute map.
+ *
+ * @return this returns the map of attributes for this socket
+ */
+ Map getAttributes();
+}
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBuffer.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBuffer.java
new file mode 100644
index 00000000..d3cdc220
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBuffer.java
@@ -0,0 +1,308 @@
+/*
+ * SocketBuffer.java February 2014
+ *
+ * Copyright (C) 2008, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static org.simpleframework.transport.TransportEvent.CLOSE;
+import static org.simpleframework.transport.TransportEvent.ERROR;
+import static org.simpleframework.transport.TransportEvent.WRITE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>SocketBuffer</code> represents a buffer that aggregates
+ * small fragments in to a single buffer before sending them. This
+ * is primarily used as a means to avoid sending many small packets
+ * rather than reasonable size ones for performance. This also
+ * enables a higher level of concurrency, as it will allow data
+ * that can't be sent over the socket to be buffered until it gets
+ * the signal that says it can be sent on.
+ *
+ * @author Niall Gallagher
+ */
+class SocketBuffer {
+
+ /**
+ * This is a small internal buffer to collect fragments.
+ */
+ private SocketBufferAppender appender;
+
+ /**
+ * This is the underlying socket to sent to the data over.
+ */
+ private SocketChannel channel;
+
+ /**
+ * This is a reference to the last buffer to be sent.
+ */
+ private ByteBuffer reference;
+
+ /**
+ * This is used to trace various events that occur.
+ */
+ private Trace trace;
+
+ /**
+ * This is the recommended minimum packet size to send.
+ */
+ private int chunk;
+
+ /**
+ * This is used to determine if the buffer was closed.
+ */
+ private boolean closed;
+
+ /**
+ * Constructor for the <code>SocketBuffer</code> object. This is
+ * used to create a buffer that will collect small fragments sent
+ * in to a more reasonably sized packet.
+ *
+ * @param socket this is the socket to write the data to
+ * @param chunk this is the minimum packet size to used
+ * @param limit this is the maximum size of the output buffer
+ */
+ public SocketBuffer(Socket socket, int chunk, int limit) {
+ this.appender = new SocketBufferAppender(socket, chunk, limit);
+ this.channel = socket.getChannel();
+ this.trace = socket.getTrace();
+ this.chunk = chunk;
+ }
+
+ /**
+ * This is used to determine if the buffer is ready to be written
+ * to. A buffer is ready when it does not hold a reference to
+ * any other buffer internally. The the <code>flush</code> method
+ * must return true for a buffer to be considered ready.
+ *
+ * @return returns true if the buffer is ready to write to
+ */
+ public synchronized boolean ready() throws IOException {
+ if(closed) {
+ throw new TransportException("Buffer has been closed");
+ }
+ if(reference != null) {
+ int remaining = reference.remaining();
+
+ if(remaining <= 0) {
+ reference = null;
+ return true;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * This will write the bytes to underlying channel if the data is
+ * greater than the minimum buffer size. If it is less than the
+ * minimum size then it will be appended to the internal buffer.
+ * If it is larger than the maximum size of the internal buffer
+ * a reference is kept to it. This reference can only be cleared
+ * with the <code>flush</code> method, which will attempt to
+ * write the data to the channel, and buffer any remaining data
+ * if the underly connection is busy.
+ *
+ * @param data this is the data to write the the channel.
+ *
+ * @return this returns true if no reference was held
+ */
+ public synchronized boolean write(ByteBuffer duplicate) throws IOException {
+ if(closed) {
+ throw new TransportException("Buffer has been closed");
+ }
+ if(reference != null) {
+ throw new IOException("Buffer already pending write");
+ }
+ int count = appender.length();
+
+ if(count > 0) {
+ return merge(duplicate);
+ }
+ int remaining = duplicate.remaining();
+
+ if(remaining < chunk) {
+ appender.append(duplicate);// just save it..
+ return true;
+ }
+ if(!flush(duplicate)) { // attempt a write
+ int space = appender.space();
+
+ if(remaining < space) {
+ appender.append(duplicate);
+ return true;
+ }
+ reference = duplicate;
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * This method is used to perform a merge of the buffer to be sent
+ * with the current buffer. If the internal buffer is large enough
+ * to send after the merge then it will be sent. Also, if the
+ * remaining bytes in the buffer are large enough for a packet
+ * then that too will be sent over the socket.
+ *
+ * @param duplicate this is the buffer to be merged
+ *
+ * @return this returns true if no reference was held
+ */
+ private synchronized boolean merge(ByteBuffer duplicate) throws IOException {
+ if(closed) {
+ throw new TransportException("Buffer has been closed");
+ }
+ int count = appender.length();
+ int merged = appender.append(duplicate);
+ int payload = merged + count;
+
+ if(payload >= chunk) { // viable packet size
+ int written = appender.write(channel);
+
+ if(written < payload) {// count not fully flush buffer
+ reference = duplicate;
+ return false;
+ }
+ return write(duplicate); // we are back at zero
+ }
+ return true; // everything was buffered as chunk >= capacity
+ }
+
+ /**
+ * This method is used to fully flush the contents of the buffer to
+ * the underlying output stream. This will only ever return true
+ * if there are no references held and no data internally buffered.
+ * If before this method is invoked a reference to a byte buffer
+ * is held then this will attempt to merge it with the internal
+ * buffer so that the <code>ready</code> method can return true.
+ * This ensures that the writing thread does not need to block.
+ *
+ * @return this returns true if all of the bytes are sent
+ */
+ public synchronized boolean flush() throws IOException {
+ if(closed) {
+ throw new TransportException("Buffer has been closed");
+ }
+ int count = appender.length();
+
+ if(count > 0) {
+ int written = appender.write(channel);
+
+ if(written < count) {
+ compact();
+ return false; // we are still buffering
+ }
+ }
+ if(reference != null) {
+ if(!flush(reference)) {
+ compact();
+ return false;
+ }
+ reference = null;
+ }
+ return true; // no more data buffered
+ }
+
+ /**
+ * This write method will write the contents of the buffer to the
+ * provided byte channel. If the whole buffer can be be written
+ * then this will simply return the number of bytes that have.
+ * The number of bytes remaining within the packet after a write
+ * can be acquired from the <code>length</code> method. Once all
+ * of the bytes are written the packet must be closed.
+ *
+ * @param channel this is the channel to write the packet to
+ * @param segment this is the segment that is to be written
+ *
+ * @return this returns the number of bytes that were written
+ */
+ private synchronized boolean flush(ByteBuffer segment) throws IOException {
+ if(closed) {
+ throw new TransportException("Buffer has been closed");
+ }
+ int require = segment.remaining();
+ int count = 0;
+
+ while(count < require) {
+ int size = channel.write(segment);
+
+ if(size <= 0) {
+ break;
+ }
+ if(trace != null) {
+ trace.trace(WRITE, size);
+ }
+ count += size;
+ }
+ if(count == require) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * To ensure that we can release any references and thus avoid a
+ * blocking thread this method will attempt to merge references
+ * in to the internal buffer. Compacting in this manner is done
+ * only if the full reference can fit in to the available space.
+ */
+ private synchronized void compact() throws IOException {
+ if(closed) {
+ throw new TransportException("Buffer has been closed");
+ }
+ if(reference != null) {
+ int remaining = reference.remaining();
+ int space = appender.space();
+
+ if(remaining < space) {
+ appender.append(reference); // try to release the buffer
+ reference = null;
+ }
+ }
+ }
+
+ /**
+ * This is used to close the writer and the underlying socket.
+ * If a close is performed on the writer then no more bytes
+ * can be read from or written to the writer and the client
+ * will receive a connection close on their side. This also
+ * ensures that the TCP FIN ACK is sent before the actual
+ * channel is closed. This is required for a clean shutdown.
+ */
+ public synchronized void close() throws IOException {
+ if(closed) {
+ throw new TransportException("Buffer has been closed");
+ }
+ if(!closed) {
+ try{
+ closed = true;
+ trace.trace(CLOSE);
+ channel.socket().shutdownOutput();
+ }catch(Throwable cause){
+ trace.trace(ERROR, cause);
+ }
+ channel.close();
+ }
+ }
+}
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferAppender.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferAppender.java
new file mode 100644
index 00000000..1b9c2795
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferAppender.java
@@ -0,0 +1,289 @@
+/*
+ * SocketBufferAppender.java February 2008
+ *
+ * Copyright (C) 2008, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static org.simpleframework.transport.TransportEvent.WRITE;
+import static org.simpleframework.transport.TransportEvent.WRITE_BUFFER;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.charset.Charset;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>SocketBufferAppender</code> represents a buffer fragment
+ * collector. This provides write access to a direct byte buffer which
+ * is used to collect fragments. Once a sufficient amount of data
+ * has been collected by this then can be written out to a channel.
+ *
+ * @author Niall Gallagher
+ */
+class SocketBufferAppender {
+
+ /**
+ * This is the buffer used to store the contents of the buffer.
+ */
+ private ByteBuffer buffer;
+
+ /**
+ * This is the trace used to watch the buffering events.
+ */
+ private Trace trace;
+
+ /**
+ * This represents the the initial size of the buffer to use.
+ */
+ private int chunk;
+
+ /**
+ * This represents the largest this appender can grow to.
+ */
+ private int limit;
+
+ /**
+ * Constructor for the <code>SocketBufferAppender</code> object. This
+ * is used to create an appender that can collect smaller fragments
+ * in to a larger buffer so that it can be delivered more efficiently.
+ *
+ * @param socket this is the socket to append data for
+ * @param chunk this is the initial size of the buffer
+ * @param limit this is the maximum size of the buffer
+ */
+ public SocketBufferAppender(Socket socket, int chunk, int limit) {
+ this.buffer = ByteBuffer.allocateDirect(chunk);
+ this.trace = socket.getTrace();
+ this.chunk = chunk;
+ this.limit = limit;
+ }
+
+ /**
+ * This is used to determine how much space is left to append
+ * data to this buffer. This is typically equivalent to capacity
+ * minus the length. However in the event that the buffer uses
+ * a private memory store that can not be written to then this
+ * can return zero regardless of the capacity and length.
+ *
+ * @return the space left within the buffer to append data to
+ */
+ public int space() {
+ return buffer.remaining();
+ }
+
+ /**
+ * This represents the capacity of the backing store. The buffer
+ * is full when length is equal to capacity and it can typically
+ * be appended to when the length is less than the capacity. The
+ * only exception is when <code>space</code> returns zero, which
+ * means that the buffer can not have bytes appended to it.
+ *
+ * @return this is the capacity of other backing byte storage
+ */
+ public int capacity() {
+ return buffer.capacity();
+ }
+
+ /**
+ * This is used to determine how mnay bytes remain within this
+ * buffer. It represents the number of write ready bytes, so if
+ * the length is greater than zero the buffer can be written to
+ * a byte channel. When length is zero the buffer can be closed.
+ *
+ * @return this is the number of bytes remaining in this buffer
+ */
+ public int length() {
+ return capacity() - space();
+ }
+
+ /**
+ * This is used to encode the underlying byte sequence to text.
+ * Converting the byte sequence to text can be useful when either
+ * debugging what exactly is being sent. Also, for transports
+ * that require string delivery of buffers this can be used.
+ *
+ * @return this returns the bytes sequence as a string object
+ */
+ public String encode() throws IOException {
+ return encode("UTF-8");
+ }
+
+ /**
+ * This is used to encode the underlying byte sequence to text.
+ * Converting the byte sequence to text can be useful when either
+ * debugging what exactly is being sent. Also, for transports
+ * that require string delivery of buffers this can be used.
+ *
+ * @param encoding this is the character set to use for encoding
+ *
+ * @return this returns the bytes sequence as a string object
+ */
+ public String encode(String encoding) throws IOException {
+ ByteBuffer segment = buffer.duplicate();
+
+ if(segment != null) {
+ segment.flip();
+ }
+ return encode(encoding, segment);
+ }
+
+ /**
+ * This is used to encode the underlying byte sequence to text.
+ * Converting the byte sequence to text can be useful when either
+ * debugging what exactly is being sent. Also, for transports
+ * that require string delivery of buffers this can be used.
+ *
+ * @param encoding this is the character set to use for encoding
+ * @param segment this is the buffer that is to be encoded
+ *
+ * @return this returns the bytes sequence as a string object
+ */
+ private String encode(String encoding, ByteBuffer segment) throws IOException {
+ Charset charset = Charset.forName(encoding);
+ CharBuffer text = charset.decode(segment);
+
+ return text.toString();
+ }
+
+ /**
+ * This will append bytes within the given buffer to the buffer.
+ * Once invoked the buffer will contain the buffer bytes, which
+ * will have been drained from the buffer. This effectively moves
+ * the bytes in the buffer to the end of the buffer instance.
+ *
+ * @param data this is the buffer containing the bytes
+ *
+ * @return returns the number of bytes that have been moved
+ */
+ public int append(ByteBuffer data) throws IOException {
+ int require = data.remaining();
+ int space = space();
+
+ if(require > space) {
+ require = space;
+ }
+ return append(data, require);
+ }
+
+ /**
+ * This will append bytes within the given buffer to the buffer.
+ * Once invoked the buffer will contain the buffer bytes, which
+ * will have been drained from the buffer. This effectively moves
+ * the bytes in the buffer to the end of the buffer instance.
+ *
+ * @param data this is the buffer containing the bytes
+ * @param count this is the number of bytes that should be used
+ *
+ * @return returns the number of bytes that have been moved
+ */
+ public int append(ByteBuffer data, int count) throws IOException {
+ ByteBuffer segment = data.slice();
+ int mark = data.position();
+ int size = mark + count;
+
+ if(count > 0) {
+ if(trace != null) {
+ trace.trace(WRITE_BUFFER, count);
+ }
+ data.position(size);
+ segment.limit(count);
+ buffer.put(segment);
+ }
+ return count;
+ }
+
+ /**
+ * This write method will write the contents of the buffer to the
+ * provided byte channel. If the whole buffer can be be written
+ * then this will simply return the number of bytes that have.
+ * The number of bytes remaining within the buffer after a write
+ * can be acquired from the <code>length</code> method. Once all
+ * of the bytes are written the buffer must be closed.
+ *
+ * @param channel this is the channel to write the buffer to
+ *
+ * @return this returns the number of bytes that were written
+ */
+ public int write(ByteChannel channel) throws IOException {
+ int size = length();
+
+ if(size <= 0) {
+ return 0;
+ }
+ return write(channel, size);
+ }
+
+ /**
+ * This write method will write the contents of the buffer to the
+ * provided byte channel. If the whole buffer can be be written
+ * then this will simply return the number of bytes that have.
+ * The number of bytes remaining within the buffer after a write
+ * can be acquired from the <code>length</code> method. Once all
+ * of the bytes are written the buffer must be closed.
+ *
+ * @param channel this is the channel to write the buffer to
+ * @param count the number of bytes to write to the channel
+ *
+ * @return this returns the number of bytes that were written
+ */
+ public int write(ByteChannel channel, int count) throws IOException {
+ if(count > 0) {
+ buffer.flip();
+ } else {
+ return 0;
+ }
+ return write(channel, buffer);
+ }
+
+ /**
+ * This write method will write the contents of the buffer to the
+ * provided byte channel. If the whole buffer can be be written
+ * then this will simply return the number of bytes that have.
+ * The number of bytes remaining within the buffer after a write
+ * can be acquired from the <code>length</code> method. Once all
+ * of the bytes are written the buffer must be closed.
+ *
+ * @param channel this is the channel to write the buffer to
+ * @param segment this is the buffer that is to be written
+ *
+ * @return this returns the number of bytes that were written
+ */
+ private int write(ByteChannel channel, ByteBuffer segment) throws IOException {
+ int require = segment.remaining();
+ int count = 0;
+
+ while(count < require) {
+ int size = channel.write(segment);
+
+ if(size <= 0) {
+ break;
+ }
+ if(trace != null) {
+ trace.trace(WRITE, size);
+ }
+ count += size;
+ }
+ if(count >= 0) {
+ segment.compact();
+ }
+ return count;
+ }
+}
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferWriter.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferWriter.java
new file mode 100644
index 00000000..346aef34
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketBufferWriter.java
@@ -0,0 +1,103 @@
+/*
+ * SocketBufferWriter.java February 2008
+ *
+ * Copyright (C) 2008, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.simpleframework.transport.reactor.Reactor;
+
+/**
+ * The <code>SocketBufferWriter</code> is used to represent the means
+ * to write buffers to an underlying transport. This manages all of
+ * the selection required to determine if the socket is write ready.
+ * If the buffer to be written is to block then this will wait
+ * until all queue buffers are fully written.
+ *
+ * @author Niall Gallagher
+ */
+class SocketBufferWriter {
+
+ /**
+ * This is the flusher that is used to asynchronously flush.
+ */
+ private final SocketFlusher flusher;
+
+ /**
+ * This is the writer that is used to queue the buffers.
+ */
+ private final SocketBuffer writer;
+
+ /**
+ * Constructor for the <code>SocketBufferWriter</code> object. This
+ * is used to create a writer that can write buffers to the socket
+ * in such a way that it write either asynchronously or block
+ * the calling thread until such time as the buffers are written.
+ *
+ * @param socket this is the pipeline that this writes to
+ * @param reactor this is the writer used to scheduler writes
+ * @param buffer this is the initial size of the output buffer
+ * @param threshold this is the maximum size of the buffer
+ */
+ public SocketBufferWriter(Socket socket, Reactor reactor, int buffer, int threshold) throws IOException {
+ this.writer = new SocketBuffer(socket, buffer, threshold);
+ this.flusher = new SocketFlusher(writer, socket, reactor);
+ }
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. This will not modify the data that
+ * is to be written, this will simply queue the buffers in the
+ * order that they are provided.
+ *
+ * @param buffer this is the array of bytes to send to the client
+ */
+ public void write(ByteBuffer buffer) throws IOException {
+ boolean done = writer.write(buffer); // returns true if we can buffer
+
+ if(!done) {
+ flusher.flush(); // we could not fully write or buffer the data so we must flush
+ }
+ }
+
+ /**
+ * This method is used to flush all of the queued buffers to
+ * the client. This method will not block but will simply flush
+ * any data to the underlying transport. Internally the data
+ * will be queued for delivery to the connected entity.
+ */
+ public void flush() throws IOException {
+ boolean done = writer.flush(); // returns true only if everything is delivered
+
+ if(!done) {
+ flusher.flush(); // here we will block for an op write event if the buffer contains a reference
+ }
+ }
+
+ /**
+ * This is used to close the writer and the underlying socket.
+ * If a close is performed on the writer then no more bytes
+ * can be read from or written to the writer and the client
+ * will receive a connection close on their side.
+ */
+ public void close() throws IOException {
+ flusher.close();
+ writer.close();
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketFlusher.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketFlusher.java
new file mode 100644
index 00000000..a10cee79
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketFlusher.java
@@ -0,0 +1,142 @@
+/*
+ * SocketFlusher.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+import org.simpleframework.transport.reactor.Reactor;
+
+/**
+ * The <code>SocketFlusher</code> flushes bytes to the underlying
+ * socket channel. This allows asynchronous writes to the socket
+ * to be managed in such a way that there is order to the way data
+ * is delivered over the socket. This uses a selector to dispatch
+ * flush invocations to the underlying socket when the socket is
+ * write ready. This allows the writing thread to continue without
+ * having to wait for all the data to be written to the socket.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.SocketBufferWriter
+ */
+class SocketFlusher {
+
+ /**
+ * This is the signaller used to determine when to flush.
+ */
+ private FlushSignaller signaller;
+
+ /**
+ * This is the scheduler used to block and signal the writer.
+ */
+ private FlushScheduler scheduler;
+
+ /**
+ * This is the writer used to queue the buffers written.
+ */
+ private SocketBuffer buffer;
+
+ /**
+ * This is used to determine if the socket flusher is closed.
+ */
+ private boolean closed;
+
+ /**
+ * Constructor for the <code>SocketFlusher</code> object. This is
+ * used to flush buffers to the underlying socket asynchronously.
+ * When finished flushing all of the buffered data this signals
+ * any threads that are blocking waiting for the write to finish.
+ *
+ * @param buffer this is used to write the buffered buffers
+ * @param reactor this is used to perform asynchronous writes
+ * @param socket this is the socket used to select with
+ */
+ public SocketFlusher(SocketBuffer buffer, Socket socket, Reactor reactor) throws IOException {
+ this.signaller = new FlushSignaller(this, socket);
+ this.scheduler = new FlushScheduler(socket, reactor, signaller, this);
+ this.buffer = buffer;
+ }
+
+ /**
+ * Here in this method we schedule a flush when the underlying
+ * writer is write ready. This allows the writer thread to return
+ * without having to fully flush the content to the underlying
+ * transport. If there are references queued this will block.
+ */
+ public synchronized void flush() throws IOException {
+ if(closed) {
+ throw new TransportException("Flusher is closed");
+ }
+ boolean block = !buffer.ready();
+
+ if(!closed) {
+ scheduler.schedule(block);
+ }
+ }
+
+ /**
+ * This is executed when the flusher is to write all of the data to
+ * the underlying socket. In this situation the writes are attempted
+ * in a non blocking way, if the task does not complete then this
+ * will simply enqueue the writing task for OP_WRITE and leave the
+ * method. This returns true if all the buffers are written.
+ */
+ public synchronized void execute() throws IOException {
+ boolean ready = buffer.flush();
+
+ if(!ready) {
+ boolean block = !buffer.ready();
+
+ if(!block && !closed) {
+ scheduler.release();
+ }
+ scheduler.repeat();
+ } else{
+ scheduler.ready();
+ }
+ }
+
+ /**
+ * This is used to abort the flushing process when the reactor has
+ * been stopped. An abort to the flusher typically happens when the
+ * server has been shutdown. It prevents threads lingering waiting
+ * for a I/O operation which prevents the server from shutting down.
+ */
+ public synchronized void abort() throws IOException {
+ scheduler.close();
+ buffer.close();
+ }
+
+ /**
+ * This is used to close the flusher ensuring that all of the
+ * data within the writer will be flushed regardless of the
+ * amount of data within the writer that needs to be written. If
+ * the writer does not block then this waits to be finished.
+ */
+ public synchronized void close() throws IOException {
+ boolean ready = buffer.flush();
+
+ if(!closed) {
+ closed = true;
+ }
+ if(!ready) {
+ scheduler.schedule(true);
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketProcessor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketProcessor.java
new file mode 100644
index 00000000..a5c52b3c
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketProcessor.java
@@ -0,0 +1,61 @@
+/*
+ * SocketProcessor.java February 2001
+ *
+ * Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+/**
+ * The <code>SocketProcessor</code> interface represents a processor that
+ * is used to accept <code>Socket</code> objects. Implementations of
+ * this object will typically hand the socket over for processing either
+ * by some form of protocol handler or message processor. If the socket
+ * contains an <code>SSLEngine</code> an SSL hand shake may be performed
+ * before any messages on the socket are interpreted.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.connect.SocketConnection
+ */
+public interface SocketProcessor {
+
+ /**
+ * Used to process the <code>Socket</code> which is a full duplex
+ * TCP connection to a higher layer the application. It is this
+ * layer that is responsible for interpreting a protocol or handling
+ * messages in some manner. In the case of HTTP this will initiate
+ * the consumption of a HTTP request after any SSL handshake is
+ * finished if the connection is secure.
+ *
+ * @param socket this is the connected HTTP socket to process
+ */
+ void process(Socket socket) throws IOException;
+
+ /**
+ * This method is used to stop the <code>SocketProcessor</code> such
+ * that it will accept no more sockets. Stopping the server ensures
+ * that all resources occupied will be released. This is required
+ * so that all threads are stopped, and all memory is released.
+ * <p>
+ * Typically this method is called once all connections to the
+ * server have been stopped. As a final act of shutting down the
+ * entire server all threads must be stopped, this allows collection
+ * of unused memory and the closing of file and socket resources.
+ */
+ void stop() throws IOException;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketTransport.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketTransport.java
new file mode 100644
index 00000000..b0ba04a5
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketTransport.java
@@ -0,0 +1,262 @@
+/*
+ * SocketTransport.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static org.simpleframework.transport.TransportEvent.READ;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Map;
+
+import javax.net.ssl.SSLEngine;
+
+import org.simpleframework.transport.reactor.Reactor;
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>SocketTransport</code> object offers a transport that can
+ * send and receive bytes in a non-blocking manner. The contract of
+ * the <code>Transport</code> is that it must either write the data
+ * it is asked to write or it must queue that data for delivery. For
+ * the vast majority of cases data is written directly to the socket
+ * without any need for queuing or selection for write ready events.
+ * <p>
+ * In the event that the client TCP window is full and writing would
+ * block this makes use of a queue of buffers which can be used to
+ * append data to. The buffers are lazily instantiated so the memory
+ * required is created only in the rare case that they are needed.
+ * Once a buffer is full it is queued to an asynchronous thread where
+ * the buffer queue is drained and sent to the client when the TCP
+ * window of the client is capable of accepting it.
+ * <p>
+ * In order to improve the network performance of this transport the
+ * default packet size sent to the TCP stack is four kilobytes. This
+ * ensures that the fragments of response delivered to the TCP layer
+ * are sufficiently large for optimum network performance.
+ *
+ * @author Niall Gallagher
+ */
+public class SocketTransport implements Transport {
+
+ /**
+ * This is the writer that is used to flush the buffer queue.
+ */
+ private SocketBufferWriter writer;
+
+ /**
+ * This is the underlying byte channel used to send the data.
+ */
+ private SocketChannel channel;
+
+ /**
+ * This is the socket that this transport is representing.
+ */
+ private Socket socket;
+
+ /**
+ * This is the trace used to monitor all transport events.
+ */
+ private Trace trace;
+
+ /**
+ * This is used to determine if the transport has been closed.
+ */
+ private boolean closed;
+
+ /**
+ * Constructor for the <code>SocketTransport</code> object. This
+ * requires a reactor to perform asynchronous writes and also the
+ * pipeline which is used to read and write data. This transport
+ * will use a queue of buffers which are lazily initialized so as
+ * to only allocate the memory on demand.
+ *
+ * @param socket this is used to read and write the data
+ * @param reactor this is used to perform asynchronous writes
+ */
+ public SocketTransport(Socket socket, Reactor reactor) throws IOException {
+ this(socket, reactor, 4096);
+ }
+
+ /**
+ * Constructor for the <code>SocketTransport</code> object. This
+ * requires a reactor to perform asynchronous writes and also the
+ * pipeline which is used to read and write data. This transport
+ * will use a queue of buffers which are lazily initialized so as
+ * to only allocate the memory on demand.
+ *
+ * @param socket this is used to read and write the data
+ * @param reactor this is used to perform asynchronous writes
+ * @param buffer this is the size of the output buffer to use
+ */
+ public SocketTransport(Socket socket, Reactor reactor, int buffer) throws IOException {
+ this(socket, reactor, buffer, 20480);
+ }
+
+ /**
+ * Constructor for the <code>SocketTransport</code> object. This
+ * requires a reactor to perform asynchronous writes and also the
+ * pipeline which is used to read and write data. This transport
+ * will use a queue of buffers which are lazily initialized so as
+ * to only allocate the memory on demand.
+ *
+ * @param socket this is used to read and write the data
+ * @param reactor this is used to perform asynchronous writes
+ * @param buffer this is the size of the output buffer to use
+ * @param threshold this is the maximum size of the output buffer
+ */
+ public SocketTransport(Socket socket, Reactor reactor, int buffer, int threshold) throws IOException {
+ this.writer = new SocketBufferWriter(socket, reactor, buffer, threshold);
+ this.channel = socket.getChannel();
+ this.trace = socket.getTrace();
+ this.socket = socket;
+ }
+
+ /**
+ * 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 getCertificate() {
+ return null;
+ }
+
+ /**
+ * This is used to acquire the trace object that is associated
+ * with the socket. A trace object is used to collection details
+ * on what operations are being performed on the socket. For
+ * instance it may contain information relating to I/O events
+ * or more application specific events such as errors.
+ *
+ * @return this returns the trace associated with this socket
+ */
+ public Trace getTrace() {
+ return trace;
+ }
+
+ /**
+ * This method is used to get the <code>Map</code> of attributes
+ * by this pipeline. The attributes map is used to maintain details
+ * about the connection. Information such as security credentials
+ * to client details can be placed within the attribute map.
+ *
+ * @return this returns the map of attributes for this pipeline
+ */
+ public Map getAttributes() {
+ return socket.getAttributes();
+ }
+
+ /**
+ * This is used to acquire the SSL engine used for https. If the
+ * pipeline is connected to an SSL transport this returns an SSL
+ * engine which can be used to establish the secure connection
+ * and send and receive content over that connection. If this is
+ * null then the pipeline represents a normal transport.
+ *
+ * @return the SSL engine used to establish a secure transport
+ */
+ public SSLEngine getEngine() {
+ return socket.getEngine();
+ }
+
+ /**
+ * This method is used to acquire the <code>SocketChannel</code>
+ * for the connection. This allows the server to acquire the input
+ * and output streams with which to communicate. It can also be
+ * used to configure the connection and perform various network
+ * operations that could otherwise not be performed.
+ *
+ * @return this returns the socket used by this HTTP pipeline
+ */
+ public SocketChannel getChannel() {
+ return socket.getChannel();
+ }
+
+ /**
+ * This is used to perform a non-blocking read on the transport.
+ * If there are no bytes available on the input buffers then
+ * this method will return zero and the buffer will remain the
+ * same. If there is data and the buffer can be filled then this
+ * will return the number of bytes read. Finally if the socket
+ * is closed this will return a -1 value.
+ *
+ * @param data this is the buffer to append the bytes to
+ *
+ * @return this returns the number of bytes that were read
+ */
+ public int read(ByteBuffer data) throws IOException {
+ if(closed) {
+ throw new TransportException("Transport is closed");
+ }
+ int count = channel.read(data);
+
+ if(trace != null) {
+ trace.trace(READ, count);
+ }
+ return count;
+ }
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. This
+ * will buffer the bytes within the internal buffer to ensure
+ * that the response fragments are sufficiently large for the
+ * network. Smaller packets result poorer performance.
+ *
+ * @param data this is the array of bytes to send to the client
+ */
+ public void write(ByteBuffer data) throws IOException{
+ if(closed) {
+ throw new TransportException("Transport is closed");
+ }
+ writer.write(data);
+ }
+
+ /**
+ * This is used to flush the internal buffer to the underlying
+ * socket. Flushing with this method is always non-blocking, so
+ * if the socket is not write ready and the buffer can be queued
+ * it will be queued and the calling thread will return.
+ */
+ public void flush() throws IOException {
+ if(closed) {
+ throw new TransportException("Transport is closed");
+ }
+ writer.flush();
+ }
+
+ /**
+ * This method is used to flush the internal buffer and close
+ * the underlying socket. This method will not complete until
+ * all buffered data is written and the underlying socket is
+ * closed at which point this can be disposed of.
+ */
+ public void close() throws IOException {
+ if(!closed) {
+ writer.flush();
+ writer.close();
+ closed = true;
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketWrapper.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketWrapper.java
new file mode 100644
index 00000000..805ab916
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/SocketWrapper.java
@@ -0,0 +1,144 @@
+/*
+ * SocketWrapper.java February 2001
+ *
+ * Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.nio.channels.SocketChannel;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.net.ssl.SSLEngine;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * This is a <code>SocketWrapper</code> objects that represents a TCP
+ * socket connections. This contains a map that allows attributes to be
+ * associated with the client connection. Attributes such as security
+ * certificates or other transport related details can be exposed to
+ * the <code>Request</code> using the socket attribute map.
+ * <p>
+ * This provides the connected <code>SocketChannel</code> that can be
+ * used to receive and response to HTTP requests. The socket channel
+ * must be selectable and in non-blocking mode. If the socket is not
+ * in a non-blocking state the connection will not be processed.
+ *
+ * @author Niall Gallagher
+ */
+public class SocketWrapper implements Socket {
+
+ /**
+ * This is the socket that provides the input and output.
+ */
+ private final SocketChannel channel;
+
+ /**
+ * This is used to encrypt content for secure connections.
+ */
+ private final SSLEngine engine;
+
+ /**
+ * This can be used to trace specific events for the socket.
+ */
+ private final Trace trace;
+
+ /**
+ * This is used to store the attributes for the socket.
+ */
+ private final Map map;
+
+ /**
+ * This creates a <code>SocketWrapper</code> from a socket channel.
+ * Any implementations of the object may use this constructor to
+ * ensure that all the data is initialized.
+ *
+ * @param channel the socket channel that is used as the transport
+ * @param trace used to trace specific events for the socket
+ */
+ public SocketWrapper(SocketChannel channel, Trace trace) {
+ this(channel, trace, null);
+ }
+
+ /**
+ * This creates a <code>SecureSocket</code> from a socket channel.
+ * Any implementations of the object may use this constructor to
+ * ensure that all the data is initialized.
+ *
+ * @param channel the socket channel that is used as the transport
+ * @param trace used to trace specific events for the socket
+ * @param engine this is the SSL engine used for secure transport
+ */
+ public SocketWrapper(SocketChannel channel, Trace trace, SSLEngine engine) {
+ this.map = new HashMap();
+ this.channel = channel;
+ this.engine = engine;
+ this.trace = trace;
+ }
+
+ /**
+ * This is used to acquire the trace object that is associated
+ * with the socket. A trace object is used to collection details
+ * on what operations are being performed on the socket. For
+ * instance it may contain information relating to I/O events
+ * or more application specific events such as errors.
+ *
+ * @return this returns the trace associated with this socket
+ */
+ public Trace getTrace() {
+ return trace;
+ }
+
+ /**
+ * This is used to acquire the SSL engine used for HTTPS. If the
+ * socket is connected to an SSL transport this returns an SSL
+ * engine which can be used to establish the secure connection
+ * and send and receive content over that connection. If this is
+ * null then the socket represents a normal transport.
+ *
+ * @return the SSL engine used to establish a secure transport
+ */
+ public SSLEngine getEngine() {
+ return engine;
+ }
+
+ /**
+ * This method is used to acquire the <code>SocketChannel</code>
+ * for the connection. This allows the server to acquire the input
+ * and output streams with which to communicate. It can also be
+ * used to configure the connection and perform various network
+ * operations that could otherwise not be performed.
+ *
+ * @return this returns the socket used by this HTTP socket
+ */
+ public SocketChannel getChannel() {
+ return channel;
+ }
+
+ /**
+ * This method is used to get the <code>Map</code> of attributes
+ * by this socket. The attributes map is used to maintain details
+ * about the connection. Information such as security credentials
+ * to client details can be placed within the attribute map.
+ *
+ * @return this returns the map of attributes for this socket
+ */
+ public Map getAttributes() {
+ return map;
+ }
+}
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/Transport.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/Transport.java
new file mode 100644
index 00000000..cc499f36
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/Transport.java
@@ -0,0 +1,91 @@
+/*
+ * Transport.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * The <code>Transport</code> interface represents a low level means
+ * to deliver content to the connected client. Typically this will
+ * be a connected, non-blocking, TCP connection. However, for tests
+ * and other purposes this may be adapted. The general contract of
+ * the transport is that it provides non-blocking reads and blocking
+ * writes. Blocking writes are required to ensure that memory does
+ * not build up in output buffers during high load periods.
+ *
+ * @author Niall Gallagher
+ */
+public interface Transport extends Socket {
+
+ /**
+ * 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 getCertificate() throws IOException;
+
+ /**
+ * This is used to perform a non-blocking read on the transport.
+ * If there are no bytes available on the input buffers then
+ * this method will return zero and the buffer will remain the
+ * same. If there is data and the buffer can be filled then this
+ * will return the number of bytes read. Finally if the socket
+ * is closed this will return a -1 value.
+ *
+ * @param buffer this is the buffer to append the bytes to
+ *
+ * @return this returns the number of bytes that have been read
+ */
+ int read(ByteBuffer buffer) throws IOException;
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or send directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param buffer this is the buffer of bytes to send to the client
+ */
+ void write(ByteBuffer buffer) throws IOException;
+
+ /**
+ * This method is used to flush the contents of the buffer to
+ * the client. This method will block not block but will simply
+ * flush any data to the underlying transport. Internally the
+ * data will be queued for delivery to the connected entity.
+ */
+ void flush() throws IOException;
+
+ /**
+ * This is used to close the transport and the underlying socket.
+ * If a close is performed on the transport then no more bytes
+ * can be read from or written to the transport and the client
+ * will receive a connection close on their side.
+ */
+ void close() throws IOException;
+}
+
+
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportChannel.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportChannel.java
new file mode 100644
index 00000000..2d5ff1af
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportChannel.java
@@ -0,0 +1,195 @@
+/*
+ * TransportChannel.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import static org.simpleframework.transport.TransportEvent.ERROR;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+import java.util.Map;
+
+import javax.net.ssl.SSLEngine;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>TransportChannel</code> provides a means to deliver and
+ * receive content over a transport. This essentially provides two
+ * adapters which enable simpler communication with the underlying
+ * transport. They hide the complexities involved with buffering and
+ * resetting data written to and read from the socket.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.TransportProcessor
+ */
+public class TransportChannel implements Channel {
+
+ /**
+ * This is the certificate associated with this SSL channel.
+ */
+ private final Certificate certificate;
+
+ /**
+ * This represents the underlying transport that is to be used.
+ */
+ private final Transport transport;
+
+ /**
+ * This is the engine that is used to secure the transport.
+ */
+ private final SSLEngine engine;
+
+ /**
+ * This is used to provide a cursor view on the input.
+ */
+ private final ByteCursor cursor;
+
+ /**
+ * This is used to provide a blocking means for sending data.
+ */
+ private final ByteWriter writer;
+
+ /**
+ * This is the trace used to monitor events on the channel.
+ */
+ private final Trace trace;
+
+ /**
+ * Constructor for the <code>TransportChannel</code> object. The
+ * transport channel basically wraps a channel and provides a
+ * means to send and receive data using specialized adapters.
+ * These adapters provide a simpler means for communicating over
+ * the network to the connected client.
+ *
+ * @param transport this is the underlying transport to be used
+ */
+ public TransportChannel(Transport transport) throws IOException {
+ this.cursor = new TransportCursor(transport);
+ this.writer = new TransportWriter(transport);
+ this.certificate = transport.getCertificate();
+ this.engine = transport.getEngine();
+ this.trace = transport.getTrace();
+ this.transport = transport;
+ }
+
+ /**
+ * This is used to determine if the channel is secure and that
+ * data read from and data written to the request is encrypted.
+ * Channels transferred over SSL are considered secure and will
+ * have this return true, otherwise it will return false.
+ *
+ * @return true if this is secure for reading and writing
+ */
+ public boolean isSecure() {
+ return engine != null;
+ }
+
+ /**
+ * This is used to acquire the SSL certificate used for security.
+ * If the socket is connected to an SSL transport this returns an
+ * SSL certificate which was provided during the secure handshake
+ * between the client and server. If not certificates are present
+ * in the provided instance, a challenge can be issued.
+ *
+ * @return the SSL certificate provided by a secure transport
+ */
+ public Certificate getCertificate() {
+ return certificate;
+ }
+
+ /**
+ * This gets the <code>Trace</code> object associated with the
+ * channel. The trace is used to log various events for the life
+ * of the transaction such as low level read and write events
+ * as well as milestone events and errors.
+ *
+ * @return this returns the trace associated with the socket
+ */
+ public Trace getTrace() {
+ return trace;
+ }
+
+ /**
+ * This is the connected socket channel associated with this. In
+ * order to determine if content can be read or written to or
+ * from the channel this socket can be used with a selector. This
+ * provides a means to react to I/O events as they occur rather
+ * than polling the channel which is generally less performant.
+ *
+ * @return this returns the connected socket channel
+ */
+ public SocketChannel getSocket() {
+ return transport.getChannel();
+ }
+
+ /**
+ * This returns the <code>Map</code> of attributes used to hold
+ * connection information for the channel. The attributes here
+ * are taken from the pipeline attributes and may contain details
+ * such as SSL certificates or other such useful information.
+ *
+ * @return returns the attributes associated with the channel
+ */
+ public Map getAttributes() {
+ return transport.getAttributes();
+ }
+
+ /**
+ * This provides a <code>ByteCursor</code> for this channel. The
+ * cursor provides a seekable view of the input buffer and will
+ * allow the server kernel to peek into the input buffer without
+ * having to take the data from the input. This allows overflow
+ * to be pushed back on to the cursor for subsequent reads.
+ *
+ * @return this returns the input cursor for the channel
+ */
+ public ByteCursor getCursor() {
+ return cursor;
+ }
+
+ /**
+ * This provides a <code>ByteWriter</code> for the channel. This
+ * is used to provide a blocking output mechanism for the channel.
+ * Enabling blocking reads ensures that output buffering can be
+ * limited to an extent, which ensures that memory remains low at
+ * high load periods. Writes to the sender may result in the data
+ * being copied and queued until the socket is write ready.
+ *
+ * @return this returns the output sender for this channel
+ */
+ public ByteWriter getWriter() {
+ return writer;
+ }
+
+ /**
+ * Because the channel represents a duplex means of communication
+ * there needs to be a means to close it down. This provides such
+ * a means. By closing the channel the cursor and sender will no
+ * longer send or receive data to or from the network. The client
+ * will also be signalled that the connection has been severed.
+ */
+ public void close() {
+ try {
+ transport.close();
+ }catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportCursor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportCursor.java
new file mode 100644
index 00000000..d25cb901
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportCursor.java
@@ -0,0 +1,260 @@
+/*
+ * TransportCursor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+/**
+ * The <code>TransportCursor</code> object represents a cursor that
+ * can read and buffer data from an underlying transport. If the
+ * number of bytes read from the cursor is more than required for
+ * the HTTP request then those bytes can be pushed back in to the
+ * cursor using the <code>reset</code> method. This will only allow
+ * the last read to be reset within the cursor safely.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.Transport
+ */
+public class TransportCursor implements ByteCursor {
+
+ /**
+ * This is the stream for the bytes read by this cursor object.
+ */
+ private ByteReader reader;
+
+ /**
+ * This is the buffer used to collect the bytes pushed back.
+ */
+ private byte[] buffer;
+
+ /**
+ * This is the number of bytes that have been pushed back.
+ */
+ private int count;
+
+ /**
+ * This is the mark from the last read from this cursor object.
+ */
+ private int mark;
+
+ /**
+ * This is the position to read data from the internal buffer.
+ */
+ private int pos;
+
+ /**
+ * This is the maximum number of bytes that can be pushed back.
+ */
+ private int limit;
+
+ /**
+ * Constructor for the <code>TransportCursor</code> object. This
+ * requires a transport to read the bytes from. By default this
+ * will create a buffer of of the specified size to read the
+ * input in to which enabled bytes to be buffered internally.
+ *
+ * @param transport this is the underlying transport to use
+ */
+ public TransportCursor(Transport transport) {
+ this(transport, 2048);
+ }
+
+ /**
+ * Constructor for the <code>TransportCursor</code> object. This
+ * requires a transport to read the bytes from. By default this
+ * will create a buffer of of the specified size to read the
+ * input in to which enabled bytes to be buffered internally.
+ *
+ * @param transport this is the underlying transport to use
+ * @param size this is the size of the internal buffer to use
+ */
+ public TransportCursor(Transport transport, int size) {
+ this.reader = new TransportReader(transport, size);
+ this.buffer = new byte[0];
+ this.limit = size;
+ }
+
+ /**
+ * 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 there is nothing more to be read from this
+ */
+ public boolean isOpen() throws IOException {
+ return reader.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 {
+ return ready() > 0;
+ }
+
+ /**
+ * 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 {
+ if(count > 0) {
+ return count;
+ }
+ return reader.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 {
+ if(count <= 0) {
+ mark = pos;
+ return reader.read(data, off, len);
+ }
+ int size = Math.min(count, len);
+
+ if(size > 0) {
+ System.arraycopy(buffer, pos, data, off, size);
+ mark = pos;
+ pos += size;
+ count -= size;
+ }
+ return size;
+ }
+
+ /**
+ * 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 <code>reset</code> 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 <code>reset</code> 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 {
+ int size = buffer.length;
+
+ if(size < len + count) {
+ expand(len + count);
+ }
+ int start = pos - len;
+
+ if(len > 0) {
+ System.arraycopy(data, off, buffer, start, len);
+ mark = start;
+ pos = start;
+ count += len;
+ }
+ }
+
+ /**
+ * 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 the buffer
+ */
+ private void expand(int capacity) throws IOException {
+ if(capacity > limit) {
+ throw new TransportException("Capacity limit exceeded");
+ }
+ byte[] temp = new byte[capacity];
+ int start = capacity - count;
+ int shift = pos - mark
+ ;
+ if(count > 0) {
+ System.arraycopy(buffer, pos, temp, start, count);
+ }
+ pos = capacity - count;
+ mark = pos - shift;
+ buffer = temp;
+ }
+
+ /**
+ * 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 size this is the number of bytes to reset back
+ *
+ * @return this is the number of bytes that have been reset
+ */
+ public int reset(int size) throws IOException {
+ if(mark == pos) {
+ return reader.reset(size);
+ }
+ if(pos - size < mark) {
+ size = pos - mark;
+ }
+ if(size > 0) {
+ count += size;
+ pos -= size;
+ }
+ return size;
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportDispatcher.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportDispatcher.java
new file mode 100644
index 00000000..ec481404
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportDispatcher.java
@@ -0,0 +1,114 @@
+/*
+ * TransportDispatcher.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.nio.channels.SocketChannel;
+
+import org.simpleframework.transport.reactor.Operation;
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>TransportDispatcher</code> operation is used transfer a
+ * transport to the processor so it can be processed. This is used so
+ * that when a transport is given to the processor it can be dispatched
+ * in another thread to the processor. This is needed so that the
+ * connection thread is occupied only briefly.
+ *
+ * @author Niall Gallagher
+ */
+class TransportDispatcher implements Operation {
+
+ /**
+ * This is the processor used to transfer the transport to.
+ */
+ private final TransportProcessor processor;
+
+ /**
+ * This is the transport to be passed to the processor.
+ */
+ private final Transport transport;
+
+ /**
+ * Constructor for the <code>TransportDispatcher</code> object. This
+ * is used to transfer a transport to a processor. Transferring the
+ * transport using an operation ensures that the thread that is
+ * used to process the transport is not occupied for long.
+ *
+ * @param transport this is the transport this exchange uses
+ * @param processor this is the negotiation to dispatch to
+ */
+ public TransportDispatcher(TransportProcessor processor, Transport transport) {
+ this.transport = transport;
+ this.processor = processor;
+ }
+
+ /**
+ * This is the <code>SelectableChannel</code> which is used to
+ * determine if the operation should be executed. If the channel
+ * is ready for a given I/O event it can be run. For instance if
+ * the operation is used to perform some form of read operation
+ * it can be executed when ready to read data from the channel.
+ *
+ * @return this returns the channel used to govern execution
+ */
+ public SocketChannel getChannel() {
+ return transport.getChannel();
+ }
+
+ /**
+ * 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 transport.getTrace();
+ }
+
+ /**
+ * This is used to transfer the transport to the processor. This
+ * will typically be executed asynchronously so that it does not
+ * delay the thread that passes the <code>Transport</code> to the
+ * transport processor, ensuring quicker processing.
+ */
+ public void run() {
+ try {
+ processor.process(transport);
+ }catch(Exception e) {
+ cancel();
+ }
+ }
+
+ /**
+ * This is used to cancel the operation if it has timed out. This
+ * is typically invoked when it has been waiting in a selector for
+ * an extended duration of time without any active operations on
+ * it. In such a case the reactor must purge the operation to free
+ * the memory and open channels associated with the operation.
+ */
+ public void cancel() {
+ try {
+ transport.close();
+ }catch(Exception e) {
+ return;
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportEvent.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportEvent.java
new file mode 100644
index 00000000..97be771a
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportEvent.java
@@ -0,0 +1,91 @@
+/*
+ * TransportEvent.java October 2012
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+/**
+ * The <code>TransportEvent</code> enum represents various events that
+ * can occur with the transport. Events that are available here are
+ * typically those that refer to low level I/O operations within the
+ * server. If a <code>Trace</code> has been associated with the socket
+ * connection then it will receive these events as they occur.
+ *
+ * @author Niall Gallagher
+ */
+public enum TransportEvent {
+
+ /**
+ * This event represents a read operation on the underlying socket.
+ */
+ READ,
+
+ /**
+ * This event occurs when there is no more data available to read.
+ */
+ READ_WAIT,
+
+ /**
+ * This event represents a write operation on the underlying socket.
+ */
+ WRITE,
+
+ /**
+ * This event represents a write buffer operation on the underlying socket.
+ */
+ WRITE_BUFFER,
+
+ /**
+ * This event occurs when no more data can be sent over the socket.
+ */
+ WRITE_WAIT,
+
+ /**
+ * This event occurs when a thread must wait for a write to finish.
+ */
+ WRITE_BLOCKING,
+
+ /**
+ * This event occurs with HTTPS when a new SSL handshake starts.
+ */
+ HANDSHAKE_BEGIN,
+
+ /**
+ * This event occurs with HTTPS when a SSL handshake has finished.
+ */
+ HANDSHAKE_DONE,
+
+ /**
+ * This event occurs when a server challenges for an X509 certificate.
+ */
+ CERTIFICATE_CHALLENGE,
+
+ /**
+ * This event indicates that the handshake failed in some way.
+ */
+ HANDSHAKE_FAILED,
+
+ /**
+ * This event occurs when the underlying connection is terminated.
+ */
+ CLOSE,
+
+ /**
+ * This event occurs when there is an error with the transport.
+ */
+ ERROR
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportException.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportException.java
new file mode 100644
index 00000000..c6394eec
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportException.java
@@ -0,0 +1,55 @@
+/*
+ * TransportException.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+/**
+ * The <code>TransportException</code> object is thrown when there
+ * is a problem with the transport. Typically this is done thrown if
+ * there is a problem reading or writing to the transport.
+ *
+ * @author Niall Gallagher
+ */
+public class TransportException extends IOException {
+
+ /**
+ * Constructor for the <code>TransportException</code> object. If
+ * there is a problem sending or reading from a transport then it
+ * will throw a transport exception to report the error.
+ *
+ * @param message this is the message associated with the error
+ */
+ public TransportException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor for the <code>TransportException</code> object. If
+ * there is a problem sending or reading from a transport then it
+ * will throw a transport exception to report the error.
+ *
+ * @param message this is the message associated with the error
+ * @param cause this is the cause of the producer exception
+ */
+ public TransportException(String message, Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportProcessor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportProcessor.java
new file mode 100644
index 00000000..13f505b1
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportProcessor.java
@@ -0,0 +1,63 @@
+/*
+ * TransportProcessor.java February 2007
+ *
+ * Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+/**
+ * This is the <code>TransportProcessor</code> used to process the
+ * provided transport in a higher layer. It is the responsibility of
+ * the delegate to handle protocols and message processing. In the
+ * case of HTTP this will process requests for a container. The
+ * transport provided can be either a direct transport or provide
+ * some form of secure encoding such as SSL.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.Transport
+ */
+public interface TransportProcessor {
+
+ /**
+ * This is used to process a <code>Transport</code> instance in
+ * a higher layer that can handle a protocol. A transport can be
+ * a direct transport or a secure transport providing SSL. At this
+ * point any SSL handshake will have already completed.
+ * <p>
+ * Typical usage of this method is to accept multiple transport
+ * objects, each representing a unique TCP channel to the client,
+ * and process requests from those transports concurrently.
+ *
+ * @param transport the transport to process requests from
+ */
+ void process(Transport transport) throws IOException;
+
+ /**
+ * This method is used to stop the <code>TransportProcessor</code>
+ * such that it will accept no more pipelines. Stopping the connector
+ * ensures that all resources occupied will be released. This is
+ * required so that all threads are stopped and released.
+ * <p>
+ * Typically this method is called once all connections to the
+ * server have been stopped. As a final act of shutting down the
+ * entire server all threads must be stopped, this allows collection
+ * of unused memory and the closing of file and socket resources.
+ */
+ void stop() throws IOException;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportReader.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportReader.java
new file mode 100644
index 00000000..713c1628
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportReader.java
@@ -0,0 +1,229 @@
+/*
+ * TransportReader.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * The <code>TransportReader</code> object represents a reader that
+ * can read and buffer data from an underlying transport. If the
+ * number of bytes read from the reader is more than required for
+ * the HTTP request then those bytes can be pushed back in to the
+ * cursor using the <code>reset</code> method. This will only allow
+ * the last read to be reset within the cursor safely.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.Transport
+ */
+class TransportReader implements ByteReader {
+
+ /**
+ * This is the underlying transport to read the bytes from.
+ */
+ private Transport transport;
+
+ /**
+ * This is used to store the bytes read from the transport.
+ */
+ private ByteBuffer buffer;
+
+ /**
+ * This is used to determine if the transport has been closed.
+ */
+ private boolean closed;
+
+ /**
+ * This represents the number of bytes that are ready to read.
+ */
+ private int count;
+
+ /**
+ * Constructor for the <code>TransportReader</code> object. This
+ * requires a transport to read the bytes from. By default this
+ * will create a buffer of two kilobytes to read the input in to
+ * which ensures several requests can be read at once.
+ *
+ * @param transport this is the underlying transport to use
+ */
+ public TransportReader(Transport transport) {
+ this(transport, 2048);
+ }
+
+ /**
+ * Constructor for the <code>TransportReader</code> object. This
+ * requires a transport to read the bytes from. By default this
+ * will create a buffer of of the specified size to read the
+ * input in to which enabled bytes to be buffered internally.
+ *
+ * @param transport this is the underlying transport to use
+ * @param size this is the size of the internal buffer to use
+ */
+ public TransportReader(Transport transport, int size) {
+ this.buffer = ByteBuffer.allocate(size);
+ this.transport = transport;
+ }
+
+ /**
+ * Determines whether the source is still open. The source is
+ * considered open if there are still bytes to read. If there is
+ * still bytes buffered and the underlying transport is closed
+ * then the source is still considered open.
+ *
+ * @return true if there is nothing more to be read from this
+ */
+ public boolean isOpen() throws IOException {
+ return count != -1;
+ }
+
+ /**
+ * Determines whether the source is ready for reading. When the
+ * source 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 {
+ return ready() > 0;
+ }
+
+ /**
+ * 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 {
+ if(count <= 0) { // has the channel ended
+ return count;
+ }
+ int size = Math.min(len, count); // get the minimum
+
+ if(size > 0) {
+ buffer.get(data, off, size); // get the bytes
+ count -= size;
+ }
+ return Math.max(0, size);
+ }
+
+ /**
+ * 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 source may perform a blocking read.
+ *
+ * @return the number of bytes that can be read without blocking
+ */
+ public int ready() throws IOException {
+ if(count < 0) {
+ return count;
+ }
+ if(count > 0) { // if the are ready bytes don't read
+ return count;
+ }
+ return peek();
+ }
+
+ /**
+ * 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 source may perform a blocking read.
+ *
+ * @return the number of bytes that can be read without blocking
+ */
+ private int peek() throws IOException {
+ if(count <= 0) { // reset the buffer for filling
+ buffer.clear();
+ }
+ if(count > 0) {
+ buffer.compact(); // compact the buffer
+ }
+ count += transport.read(buffer); // how many were read
+
+ if(count > 0) {
+ buffer.flip(); // if there is something then flip
+ }
+ if(count < 0) { // close when stream is fully read
+ close();
+ }
+ return count;
+ }
+
+ /**
+ * Moves the source 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 size this is the number of bytes to reset back
+ *
+ * @return this is the number of bytes that have been reset
+ */
+ public int reset(int size) throws IOException {
+ int mark = buffer.position();
+
+ if(size > mark) {
+ size = mark;
+ }
+ if(mark > 0) {
+ buffer.position(mark - size);
+ count += size;
+ }
+ return size;
+ }
+
+ /**
+ * This is used to close the underlying transport. This is used
+ * when the transport returns a negative value, indicating that
+ * the client has closed the connection on the other side. If
+ * this is invoked the read method returns -1 and the reader
+ * is no longer open, further bytes can no longer be read.
+ */
+ public void close() throws IOException {
+ if(!closed) {
+ transport.close();
+ closed = true;
+ count = -1;
+ }
+ }
+}
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportSocketProcessor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportSocketProcessor.java
new file mode 100644
index 00000000..04e3c9b9
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportSocketProcessor.java
@@ -0,0 +1,163 @@
+/*
+ * TransportSocketProcessor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+
+import org.simpleframework.common.thread.ConcurrentExecutor;
+import org.simpleframework.common.thread.Daemon;
+import org.simpleframework.transport.reactor.ExecutorReactor;
+import org.simpleframework.transport.reactor.Operation;
+import org.simpleframework.transport.reactor.Reactor;
+
+/**
+ * The <code>TransportSocketProcessor</code> is used to convert sockets
+ * to transports. This acts as an adapter to a transport processor
+ * which converts a connected socket to a <code>Transport</code> that
+ * can be used to read and write data. Depending on whether there is
+ * an <code>SSLEngine</code> associated with the socket or not, there
+ * could be an SSL handshake performed.
+ *
+ * @author Niall Gallagher
+ */
+public class TransportSocketProcessor implements SocketProcessor {
+
+ /**
+ * This is the executor used to execute the I/O operations.
+ */
+ private final ConcurrentExecutor executor;
+
+ /**
+ * This is the factory used to create the required operations.
+ */
+ private final OperationFactory factory;
+
+ /**
+ * This is the processor used to process transport objects.
+ */
+ private final Reactor reactor;
+
+ /**
+ * This is used to clean the internals of the processor.
+ */
+ private final Daemon cleaner;
+
+ /**
+ * Constructor for the <code>TransportSocketProcessor</code> object.
+ * The transport processor is used to process plain connections
+ * and wrap those connections in a <code>Transport</code> that
+ * can be used to send and receive data to and from.
+ *
+ * @param processor this is used to process transports
+ */
+ public TransportSocketProcessor(TransportProcessor processor) throws IOException {
+ this(processor, 8);
+ }
+
+ /**
+ * Constructor for the <code>TransportSocketProcessor</code> object.
+ * The transport processor is used to process plain connections
+ * and wrap those connections in a <code>Transport</code> that
+ * can be used to send and receive data to and from.
+ *
+ * @param processor this is used to process transports
+ * @param threads this is the number of threads this will use
+ */
+ public TransportSocketProcessor(TransportProcessor processor, int threads) throws IOException {
+ this(processor, threads, 4096);
+ }
+
+ /**
+ * Constructor for the <code>TransportSocketProcessor</code> object.
+ * The transport processor is used to process plain connections
+ * and wrap those connections in a <code>Transport</code> that
+ * can be used to send and receive data to and from.
+ *
+ * @param processor this is used to process transports
+ * @param threads this is the number of threads this will use
+ * @param buffer this is the initial size of the output buffer
+ */
+ public TransportSocketProcessor(TransportProcessor processor, int threads, int buffer) throws IOException {
+ this(processor, threads, buffer, 20480);
+ }
+
+ /**
+ * Constructor for the <code>TransportSocketProcessor</code> object.
+ * The transport processor is used to process plain connections
+ * and wrap those connections in a <code>Transport</code> that
+ * can be used to send and receive data to and from.
+ *
+ * @param processor this is used to process transports
+ * @param threads this is the number of threads this will use
+ * @param buffer this is the initial size of the output buffer
+ * @param threshold this is the maximum size of the output buffer
+ */
+ public TransportSocketProcessor(TransportProcessor processor, int threads, int buffer, int threshold) throws IOException {
+ this(processor, threads, buffer, threshold, false);
+ }
+
+ /**
+ * Constructor for the <code>TransportSocketProcessor</code> object.
+ * The transport processor is used to process plain connections
+ * and wrap those connections in a <code>Transport</code> that
+ * can be used to send and receive data to and from.
+ *
+ * @param processor this is used to process transports
+ * @param threads this is the number of threads this will use
+ * @param buffer this is the initial size of the output buffer
+ * @param threshold this is the maximum size of the output buffer
+ * @param client determines if the SSL handshake is for a client
+ */
+ public TransportSocketProcessor(TransportProcessor processor, int threads, int buffer, int threshold, boolean client) throws IOException {
+ this.executor = new ConcurrentExecutor(Operation.class, threads);
+ this.reactor = new ExecutorReactor(executor);
+ this.factory = new OperationFactory(processor, reactor, buffer, threshold, client);
+ this.cleaner = new ServerCleaner(processor, executor, reactor);
+ }
+
+ /**
+ * Used to connect the <code>Socket</code> which is a full duplex
+ * TCP connection to a higher layer the application. It is this
+ * layer that is responsible for interpreting a protocol or handling
+ * messages in some manner. In the case of HTTP this will initiate
+ * the consumption of a HTTP request after any SSL handshake is
+ * finished if the connection is secure.
+ *
+ * @param socket this is the connected HTTP pipeline to process
+ */
+ public void process(Socket socket) throws IOException {
+ Operation task = factory.getInstance(socket);
+
+ if(task != null) {
+ reactor.process(task);
+ }
+ }
+
+ /**
+ * This is implemented to shut down the server asynchronously. It
+ * will start a process to perform the shutdown. Asynchronous
+ * shutdown allows a server resource executed via a HTTP request
+ * can stop the server without any danger of killing itself or
+ * even worse causing a deadlock.
+ */
+ public void stop() throws IOException {
+ cleaner.start();
+ executor.stop();
+ }
+ }
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportWriter.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportWriter.java
new file mode 100644
index 00000000..c6eb436b
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/TransportWriter.java
@@ -0,0 +1,150 @@
+/*
+ * TransportWriter.java February 2007
+ *
+ * Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * The <code>TransportWriter</code> object is used to write bytes to
+ * and underlying transport. This is essentially an adapter between
+ * an <code>OutputStream</code> and the underlying transport. Each
+ * byte array segment written to the underlying transport is wrapped
+ * in a bytes buffer so that it can be sent by the transport layer.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.Transport
+ */
+public class TransportWriter implements ByteWriter {
+
+ /**
+ * This is used to determine if the transport has been closed.
+ */
+ private final AtomicBoolean closed;
+
+ /**
+ * This is the underlying transport to write the bytes to.
+ */
+ private final Transport transport;
+
+ /**
+ * Constructor for the <code>TransportWriter</code> object. This
+ * is used to create an adapter for the transport such that a
+ * byte array can be used to write bytes to the array.
+ *
+ * @param transport the underlying transport to write bytes to
+ */
+ public TransportWriter(Transport transport) {
+ this.closed = new AtomicBoolean();
+ this.transport = transport;
+ }
+
+ /**
+ * This method is used to deliver the provided array of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or write directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param array this is the array of bytes to write to the client
+ */
+ public void write(byte[] array) throws IOException {
+ write(array, 0, array.length);
+ }
+
+ /**
+ * This method is used to deliver the provided array of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or write directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param array this is the array of bytes to write to the client
+ * @param off this is the offset within the array to write 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 {
+ ByteBuffer buffer = ByteBuffer.wrap(array, off, len);
+
+ if(len > 0) {
+ write(buffer);
+ }
+ }
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or write directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param buffer this is the buffer of bytes to write to the client
+ */
+ public void write(ByteBuffer buffer) throws IOException {
+ int mark = buffer.position();
+ int size = buffer.limit();
+
+ if(mark > size) {
+ throw new IOException("Buffer position greater than limit");
+ }
+ write(buffer, 0, size - mark);
+ }
+
+ /**
+ * This method is used to deliver the provided buffer of bytes to
+ * the underlying transport. Depending on the connection type the
+ * array may be encoded for SSL transport or write directly. Any
+ * implementation may choose to buffer the bytes for performance.
+ *
+ * @param buffer this is the buffer of bytes to write to the client
+ * @param off this is the offset within the buffer to write 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 {
+ int mark = buffer.position();
+ int limit = buffer.limit();
+
+ if(limit - mark > len) {
+ buffer.limit(mark + len); // reduce usable size
+ }
+ transport.write(buffer);
+ buffer.limit(limit);
+ }
+
+ /**
+ * 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 writing the content an exception is thrown.
+ */
+ public void flush() throws IOException {
+ transport.flush();
+ }
+
+ /**
+ * This is used to close the writer and the underlying transport.
+ * If a close is performed on the writer then no more bytes can
+ * be read from or written to the transport and the client will
+ * received a connection close on their side.
+ */
+ public void close() throws IOException {
+ if(!closed.getAndSet(true)) {
+ transport.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/Connection.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/Connection.java
new file mode 100644
index 00000000..490ce6de
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/Connection.java
@@ -0,0 +1,73 @@
+/*
+ * Connection.java October 2002
+ *
+ * Copyright (C) 2002, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.SocketAddress;
+
+import javax.net.ssl.SSLContext;
+
+/**
+ * The <code>Connection</code> object is used to manage connections
+ * from a server socket. In order to achieve this it spawns a task
+ * to listen for incoming connect requests. When a TCP connection
+ * request arrives it hands off the <code>SocketChannel</code> to
+ * the <code>SocketProcessor</code> which processes the request.
+ * <p>
+ * This handles connections from a <code>ServerSocketChannel</code>
+ * object so that features such as SSL can be used by a server that
+ * uses this package. The background acceptor process will terminate
+ * if the connection is closed.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.SocketProcessor
+ */
+public interface Connection extends Closeable {
+
+ /**
+ * This creates a new background task that will listen to the
+ * specified <code>ServerAddress</code> for incoming TCP connect
+ * requests. When an connection is accepted it is handed to the
+ * internal <code>Server</code> implementation as a pipeline. The
+ * background task is a non daemon task to ensure the server is
+ * kept active, to terminate the connection this can be closed.
+ *
+ * @param address this is the address used to accept connections
+ *
+ * @return this returns the actual local address that is used
+ */
+ SocketAddress connect(SocketAddress address) throws IOException;
+
+ /**
+ * This creates a new background task that will listen to the
+ * specified <code>ServerAddress</code> for incoming TCP connect
+ * requests. When an connection is accepted it is handed to the
+ * internal <code>Server</code> implementation as a pipeline. The
+ * background task is a non daemon task to ensure the server is
+ * kept active, to terminate the connection this can be closed.
+ *
+ * @param address this is the address used to accept connections
+ * @param context this is used for secure SSL connections
+ *
+ * @return this returns the actual local address that is used
+ */
+ SocketAddress connect(SocketAddress address, SSLContext context) throws IOException;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionEvent.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionEvent.java
new file mode 100644
index 00000000..0a29ba82
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionEvent.java
@@ -0,0 +1,42 @@
+/*
+ * ConnectionEvent.java October 2012
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+/**
+ * The <code>ConnectionEvent</code> enum represents various events that
+ * can occur with a new connection. When a new connection is accepted
+ * then the accept event is dispatched to a <code>Trace</code> object
+ * if one has been associated with the connection.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.trace.Trace
+ */
+public enum ConnectionEvent {
+
+ /**
+ * This event occurs when the server accepts a new connection.
+ */
+ ACCEPT,
+
+ /**
+ * This event occurs when there is an error with the connection.
+ */
+ ERROR
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionException.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionException.java
new file mode 100644
index 00000000..87e5196d
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/ConnectionException.java
@@ -0,0 +1,58 @@
+/*
+ * ConnectionException.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import java.io.IOException;
+
+/**
+ * The <code>ConnectionException</code> is thrown if there is a problem
+ * establishing a connection to the server. Such a problem can occur
+ * if the server has been stopped when a new connection arrives. This
+ * can also be thrown if some other connection related issue occurs.
+ *
+ * @author Niall Gallagher
+ */
+class ConnectionException extends IOException {
+
+ /**
+ * Constructor for the <code>ConnectionException</code> object. This
+ * is used to represent an exception that is thrown when an error
+ * occurs during the connect process. Typically this is thrown if
+ * there is a problem connecting or accepting from a socket.
+ *
+ * @param message this is the message describing the exception
+ */
+ public ConnectionException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor for the <code>ConnectionException</code> object. This
+ * is used to represent an exception that is thrown when an error
+ * occurs during the connect process. Typically this is thrown if
+ * there is a problem connecting or accepting from a socket.
+ *
+ * @param message this is the message describing the exception
+ * @param cause this is the cause of the producer exception
+ */
+ public ConnectionException(String message, Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAcceptor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAcceptor.java
new file mode 100644
index 00000000..ca6fc92b
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAcceptor.java
@@ -0,0 +1,315 @@
+/*
+ * Acceptor.java October 2002
+ *
+ * Copyright (C) 2002, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import static org.simpleframework.transport.connect.ConnectionEvent.ACCEPT;
+import static org.simpleframework.transport.connect.ConnectionEvent.ERROR;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.SocketAddress;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+
+import org.simpleframework.transport.SocketProcessor;
+import org.simpleframework.transport.Socket;
+import org.simpleframework.transport.SocketWrapper;
+import org.simpleframework.transport.reactor.Operation;
+import org.simpleframework.transport.trace.Trace;
+import org.simpleframework.transport.trace.TraceAnalyzer;
+
+/**
+ * The <code>SocketAcceptor</code> object is used to accept incoming
+ * TCP connections from a specified socket address. This is used by
+ * the <code>Connection</code> object as a background process to
+ * accept the connections and hand them to a socket connector.
+ * <p>
+ * This is capable of processing SSL connections created by the
+ * internal server socket. All SSL connections are forced to finish
+ * the SSL handshake before being dispatched to the server. This
+ * ensures that there are no problems with reading the request.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.connect.SocketConnection
+ */
+class SocketAcceptor implements Operation {
+
+ /**
+ * This is the server socket channel used to accept connections.
+ */
+ private final ServerSocketChannel listener;
+
+ /**
+ * The handler that manages the incoming TCP connections.
+ */
+ private final SocketProcessor processor;
+
+ /**
+ * This is the server socket to bind the socket address to.
+ */
+ private final ServerSocket socket;
+
+ /**
+ * If provided the SSL context is used to create SSL engines.
+ */
+ private final SSLContext context;
+
+ /**
+ * This is the tracing analyzer used to trace accepted sockets.
+ */
+ private final TraceAnalyzer analyzer;
+
+ /**
+ * This is the local address to bind the listen socket to.
+ */
+ private final SocketAddress address;
+
+ /**
+ * This is used to collect trace events with the acceptor.
+ */
+ private final Trace trace;
+
+ /**
+ * Constructor for the <code>SocketAcceptor</code> object. This
+ * accepts new TCP connections from the specified server socket.
+ * Each of the connections that is accepted is configured for
+ * performance for the application.
+ *
+ * @param address this is the address to accept connections from
+ * @param processor this is used to initiate the HTTP processing
+ * @param analyzer this is the tracing analyzer to be used
+ */
+ public SocketAcceptor(SocketAddress address, SocketProcessor processor, TraceAnalyzer analyzer) throws IOException {
+ this(address, processor, analyzer, null);
+ }
+
+ /**
+ * Constructor for the <code>SocketAcceptor</code> object. This
+ * accepts new TCP connections from the specified server socket.
+ * Each of the connections that is accepted is configured for
+ * performance for the applications.
+ *
+ * @param address this is the address to accept connections from
+ * @param processor this is used to initiate the HTTP processing
+ * @param analyzer this is the tracing analyzer to be used
+ * @param context this is the SSL context used for secure HTTPS
+ */
+ public SocketAcceptor(SocketAddress address, SocketProcessor processor, TraceAnalyzer analyzer, SSLContext context) throws IOException {
+ this.listener = ServerSocketChannel.open();
+ this.trace = analyzer.attach(listener);
+ this.socket = listener.socket();
+ this.context = context;
+ this.analyzer = analyzer;
+ this.processor = processor;
+ this.address = address;
+ }
+
+ /**
+ * This is used to acquire the local socket address that this is
+ * listening to. This required in case the socket address that
+ * is specified is an emphemeral address, that is an address that
+ * is assigned dynamically when a port of 0 is specified.
+ *
+ * @return this returns the address for the listening address
+ */
+ public SocketAddress getAddress() {
+ return socket.getLocalSocketAddress();
+ }
+
+ /**
+ * 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 <code>SelectableChannel</code> which is used to
+ * determine if the operation should be executed. If the channel
+ * is ready for a given I/O event it can be run. For instance if
+ * the operation is used to perform some form of read operation
+ * it can be executed when ready to read data from the channel.
+ *
+ * @return this returns the channel used to govern execution
+ */
+ public SelectableChannel getChannel() {
+ return listener;
+ }
+
+ /**
+ * This is used to configure the server socket for non-blocking
+ * mode. It will also bind the server socket to the socket port
+ * specified in the <code>SocketAddress</code> object. Once done
+ * the acceptor is ready to accept newly arriving connections.
+ *
+ * @param address this is the server socket address to bind to
+ */
+ public void bind() throws IOException {
+ listener.configureBlocking(false);
+ socket.setReuseAddress(true);
+ socket.bind(address, 100);
+ }
+
+ /**
+ * This is used to accept a new TCP connections. When the socket
+ * is ready to accept a connection this method is invoked. It will
+ * then create a HTTP pipeline object using the accepted socket
+ * and if provided with an <code>SSLContext</code> it will also
+ * provide an <code>SSLEngine</code> which is handed to the
+ * processor to handle the HTTP requests.
+ */
+ public void run() {
+ try {
+ accept();
+ } catch(Exception cause) {
+ pause();
+ }
+ }
+
+ /**
+ * This is used to throttle the acceptor when there is an error
+ * such as exhaustion of file descriptors. This will prevent the
+ * CPU from being hogged by the acceptor on such occasions. If
+ * the thread can not be put to sleep then this will freeze.
+ */
+ private void pause() {
+ try {
+ Thread.sleep(10);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+
+ /**
+ * This is used to cancel the operation if the reactor decides to
+ * reject it for some reason. Typically this method will never be
+ * invoked as this operation never times out. However, should the
+ * reactor cancel the operation this will close the socket.
+ */
+ public void cancel() {
+ try {
+ close();
+ } catch(Throwable cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+
+ /**
+ * The main processing done by this object is done using a thread
+ * calling the <code>run</code> method. Here the TCP connections
+ * are accepted from the <code>ServerSocketChannel</code> which
+ * creates the socket objects. Each socket is then encapsulated in
+ * to a pipeline and dispatched to the processor for processing.
+ *
+ * @throws IOException if there is a problem accepting the socket
+ */
+ private void accept() throws IOException {
+ SocketChannel channel = listener.accept();
+
+ while(channel != null) {
+ Trace trace = analyzer.attach(channel);
+
+ configure(channel);
+
+ if(context == null) {
+ process(channel, trace, null);
+ } else {
+ process(channel, trace);
+ }
+ channel = listener.accept();
+ }
+ }
+
+ /**
+ * This method is used to configure the accepted channel. This
+ * will disable Nagles algorithm to improve the performance of the
+ * channel, also this will ensure the accepted channel disables
+ * blocking to ensure that it works within the processor object.
+ *
+ * @param channel this is the channel that is to be configured
+ */
+ private void configure(SocketChannel channel) throws IOException {
+ channel.socket().setTcpNoDelay(true);
+ channel.configureBlocking(false);
+ }
+
+ /**
+ * This method is used to dispatch the socket for processing. The
+ * socket will be configured and connected to the client, this
+ * will hand processing to the <code>Server</code> which will
+ * create the pipeline instance used to wrap the socket object.
+ *
+ * @param channel this is the connected socket to be processed
+ * @param trace this is the trace to associate with the socket
+ */
+ private void process(SocketChannel channel, Trace trace) throws IOException {
+ SSLEngine engine = context.createSSLEngine();
+
+ try {
+ process(channel, trace, engine);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ channel.close();
+ }
+ }
+
+ /**
+ * This method is used to dispatch the socket for processing. The
+ * socket will be configured and connected to the client, this
+ * will hand processing to the <code>Server</code> which will
+ * create the pipeline instance used to wrap the socket object.
+ *
+ * @param channel this is the connected socket to be processed
+ * @param trace this is the trace to associate with the socket
+ * @param engine this is the SSL engine used for secure HTTPS
+ */
+ private void process(SocketChannel channel, Trace trace, SSLEngine engine) throws IOException {
+ Socket socket = new SocketWrapper(channel, trace, engine);
+
+ try {
+ trace.trace(ACCEPT);
+ processor.process(socket);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ channel.close();
+ }
+ }
+
+ /**
+ * This is used to close the server socket channel so that the
+ * port that it is bound to is released. This allows the acceptor
+ * to close off the interface to the server. Ensuring the socket
+ * is closed allows it to be recreated at a later point.
+ *
+ * @throws IOException thrown if the socket can not be closed
+ */
+ public void close() throws IOException {
+ listener.close();
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAnalyzer.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAnalyzer.java
new file mode 100644
index 00000000..be6b95c7
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketAnalyzer.java
@@ -0,0 +1,84 @@
+/*
+ * SocketAnalyzer.java February 2012
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import java.nio.channels.SelectableChannel;
+
+import org.simpleframework.transport.trace.TraceAnalyzer;
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>SocketAnalyzer</code> is used to wrap an analyzer object.
+ * Wrapping an analyzer in this way ensures that even if the analyzer
+ * is badly written there is little chance that it will affect the
+ * operation of the server. All <code>Trace</code> objects returned
+ * from this will catch all exceptions within the created trace.
+ *
+ * @author Niall Gallagher
+ */
+class SocketAnalyzer implements TraceAnalyzer {
+
+ /**
+ * This is the analyzer that is used to create the trace objects.
+ */
+ private final TraceAnalyzer analyzer;
+
+ /**
+ * Constructor for the <code>SocketAnalyzer</code> object. This will
+ * be given the analyzer that is to be used to create traces. This
+ * can be a null value, in which case the trace provided will be
+ * a simple empty void that swallows all trace events.
+ *
+ * @param analyzer the analyzer that is to be wrapped by this
+ */
+ public SocketAnalyzer(TraceAnalyzer analyzer) {
+ this.analyzer = analyzer;
+ }
+
+ /**
+ * This method is used to attach a trace to the specified channel.
+ * Attaching a trace basically means associating events from that
+ * trace with the specified socket. It ensures that the events
+ * from a specific channel can be observed in isolation.
+ *
+ * @param channel this is the channel to associate with the trace
+ *
+ * @return this returns a trace associated with the channel
+ */
+ public Trace attach(SelectableChannel channel) {
+ Trace trace = null;
+
+ if(analyzer != null) {
+ trace = analyzer.attach(channel);
+ }
+ return new SocketTrace(trace);
+ }
+
+ /**
+ * This is used to stop the analyzer and clear all trace information.
+ * Stopping the analyzer is typically done when the server is stopped
+ * and is used to free any resources associated with the analyzer. If
+ * an analyzer does not hold information this method can be ignored.
+ */
+ public void stop() {
+ if(analyzer != null) {
+ analyzer.stop();
+ }
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketConnection.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketConnection.java
new file mode 100644
index 00000000..c462284b
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketConnection.java
@@ -0,0 +1,141 @@
+/*
+ * SocketConnection.java October 2002
+ *
+ * Copyright (C) 2002, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+
+import javax.net.ssl.SSLContext;
+
+import org.simpleframework.transport.SocketProcessor;
+import org.simpleframework.transport.trace.TraceAnalyzer;
+
+/**
+ * The <code>SocketConnection</code>is used to manage connections
+ * from a server socket. In order to achieve this it spawns a task
+ * to listen for incoming connect requests. When a TCP connection
+ * request arrives it hands off the <code>SocketChannel</code> to
+ * the <code>SocketProcessor</code> which processes the request.
+ * <p>
+ * This handles connections from a <code>ServerSocketChannel</code>
+ * object so that features such as SSL can be used by a server that
+ * uses this package. The background acceptor process will terminate
+ * if the connection is closed.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.SocketProcessor
+ */
+public class SocketConnection implements Connection {
+
+ /**
+ * This is used to maintain the active connection end points.
+ */
+ private SocketListenerManager manager;
+
+ /**
+ * The processor is used to process connected HTTP pipelines.
+ */
+ private SocketProcessor processor;
+
+ /**
+ * This is used to determine if the connection has been closed.
+ */
+ private boolean closed;
+
+ /**
+ * Constructor for the <code>SocketConnection</code> object. This
+ * will create a new connection that accepts incoming connections
+ * and hands these connections as <code>Socket</code> objects
+ * to the specified connector. This in turn will deliver request
+ * and response objects to the internal container.
+ *
+ * @param processor this is the connector that receives requests
+ */
+ public SocketConnection(SocketProcessor processor) throws IOException {
+ this(processor, null);
+ }
+
+ /**
+ * Constructor for the <code>SocketConnection</code> object. This
+ * will create a new connection that accepts incoming connections
+ * and hands these connections as <code>Socket</code> objects
+ * to the specified processor. This in turn will deliver request
+ * and response objects to the internal container.
+ *
+ * @param processor this is the connector that receives requests
+ * @param analyzer this is used to create a trace for the socket
+ */
+ public SocketConnection(SocketProcessor processor, TraceAnalyzer analyzer) throws IOException {
+ this.manager = new SocketListenerManager(processor, analyzer);
+ this.processor = processor;
+ }
+
+ /**
+ * This creates a new background task that will listen to the
+ * specified <code>ServerAddress</code> for incoming TCP connect
+ * requests. When an connection is accepted it is handed to the
+ * internal socket connector.
+ *
+ * @param address this is the address used to accept connections
+ *
+ * @return this returns the actual local address that is used
+ */
+ public SocketAddress connect(SocketAddress address) throws IOException {
+ if(closed) {
+ throw new ConnectionException("Connection is closed");
+ }
+ return manager.listen(address);
+ }
+
+ /**
+ * This creates a new background task that will listen to the
+ * specified <code>ServerAddress</code> for incoming TCP connect
+ * requests. When an connection is accepted it is handed to the
+ * internal socket connector.
+ *
+ * @param address this is the address used to accept connections
+ * @param context this is used for secure SSL connections
+ *
+ * @return this returns the actual local address that is used
+ */
+ public SocketAddress connect(SocketAddress address, SSLContext context) throws IOException {
+ if(closed) {
+ throw new ConnectionException("Connection is closed");
+ }
+ return manager.listen(address, context);
+ }
+
+ /**
+ * This is used to close the connection and the server socket
+ * used to accept connections. This will perform a close of all
+ * connected server sockets that have been created from using
+ * the <code>connect</code> method. The connection can be
+ * reused after the existing server sockets have been closed.
+ *
+ * @throws IOException thrown if there is a problem closing
+ */
+ public void close() throws IOException {
+ if(!closed) {
+ manager.close();
+ processor.stop();
+ }
+ closed = true;
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListener.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListener.java
new file mode 100644
index 00000000..57182738
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListener.java
@@ -0,0 +1,125 @@
+/*
+ * SocketListener.java October 2002
+ *
+ * Copyright (C) 2002, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import static java.nio.channels.SelectionKey.OP_ACCEPT;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.SocketAddress;
+
+import javax.net.ssl.SSLContext;
+
+import org.simpleframework.transport.SocketProcessor;
+import org.simpleframework.transport.reactor.SynchronousReactor;
+import org.simpleframework.transport.reactor.Reactor;
+import org.simpleframework.transport.trace.TraceAnalyzer;
+
+/**
+ * The <code>SocketListener</code> object is represents the interface
+ * to the server that the clients can connect to. This is responsible
+ * for making call backs to the <code>SocketAcceptor</code> when there
+ * is a new connection waiting to be accepted. When the connection
+ * is to be closed the interface object can be closed.
+ *
+ * @author Niall Gallagher
+ */
+class SocketListener implements Closeable {
+
+ /**
+ * This is the acceptor that is used to accept the connections.
+ */
+ private final SocketAcceptor acceptor;
+
+ /**
+ * This is the reactor used to notify the acceptor of sockets.
+ */
+ private final Reactor reactor;
+
+ /**
+ * Constructor for the <code>SocketListener</code> object. This
+ * needs a socket address and a processor to hand created sockets
+ * to. This creates a <code>Reactor</code> which will notify the
+ * acceptor when there is a new connection waiting to be accepted.
+ *
+ * @param address this is the address to listen for new sockets
+ * @param processor this is the processor that sockets are handed to
+ * @param analyzer this is used to create a trace to monitor events
+ */
+ public SocketListener(SocketAddress address, SocketProcessor processor, TraceAnalyzer analyzer) throws IOException {
+ this(address, processor, analyzer, null);
+ }
+
+ /**
+ * Constructor for the <code>SocketListener</code> object. This
+ * needs a socket address and a processor to hand created sockets
+ * to. This creates a <code>Reactor</code> which will notify the
+ * acceptor when there is a new connection waiting to be accepted.
+ *
+ * @param address this is the address to listen for new sockets
+ * @param processor this is the processor that sockets are handed to
+ * @param analyzer this is used to create a trace to monitor events
+ * @param context this is the SSL context used for secure HTTPS
+ */
+ public SocketListener(SocketAddress address, SocketProcessor processor, TraceAnalyzer analyzer, SSLContext context) throws IOException {
+ this.acceptor = new SocketAcceptor(address, processor, analyzer, context);
+ this.reactor = new SynchronousReactor();
+ }
+
+ /**
+ * This is used to acquire the local socket address that this is
+ * listening to. This required in case the socket address that
+ * is specified is an emphemeral address, that is an address that
+ * is assigned dynamically when a port of 0 is specified.
+ *
+ * @return this returns the address for the listening address
+ */
+ public SocketAddress getAddress() {
+ return acceptor.getAddress();
+ }
+
+ /**
+ * This is used to register the socket acceptor to listen for
+ * new connections that are ready to be accepted. Once this is
+ * registered it will remain registered until the interface is
+ * closed, at which point the socket is closed.
+ */
+ public void process() throws IOException {
+ try {
+ acceptor.bind();
+ reactor.process(acceptor, OP_ACCEPT);
+ } catch(Exception cause) {
+ throw new ConnectionException("Listen error", cause);
+ }
+ }
+
+ /**
+ * This is used to close the connection and the server socket
+ * used to accept connections. This will perform a close of the
+ * connected server socket and the dispatching thread.
+ */
+ public void close() throws IOException {
+ try {
+ acceptor.close();
+ reactor.stop();
+ } catch(Exception cause) {
+ throw new ConnectionException("Close error", cause);
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListenerManager.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListenerManager.java
new file mode 100644
index 00000000..b0330f18
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketListenerManager.java
@@ -0,0 +1,127 @@
+/*
+ * SocketListenerManager.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import javax.net.ssl.SSLContext;
+
+import org.simpleframework.transport.SocketProcessor;
+import org.simpleframework.transport.trace.TraceAnalyzer;
+
+/**
+ * The <code>SocketListenerManager</code> contains all the listeners
+ * that have been created for a connection. This set is used to hold
+ * and manage the listeners that have been created for a connection.
+ * All listeners will be closed if the listener manager is closed.
+ * This ensures all resources held by the manager can be released.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.connect.SocketConnection
+ */
+class SocketListenerManager implements Closeable {
+
+ /**
+ * This is the set of active socket listeners for this manager.
+ */
+ private final Set<SocketListener> listeners;
+
+ /**
+ * This is the processor that listeners will dispatch sockets to.
+ */
+ private final SocketProcessor processor;
+
+ /**
+ * This is the analyzer used to create a trace for the sockets.
+ */
+ private final TraceAnalyzer analyzer;
+
+ /**
+ * Constructor for the <code>SocketListenerManager</code> object.
+ * This is used to create a manager that will enable listeners to
+ * be created to listen to specified sockets for incoming TCP
+ * connections, which will be converted to socket objects.
+ *
+ * @param processor this is the processor to hand sockets to
+ * @param analyzer this is the agent used to trace socket events
+ */
+ public SocketListenerManager(SocketProcessor processor, TraceAnalyzer analyzer) {
+ this.listeners = new CopyOnWriteArraySet<SocketListener>();
+ this.analyzer = new SocketAnalyzer(analyzer);
+ this.processor = processor;
+ }
+
+ /**
+ * This creates a new background task that will listen to the
+ * specified <code>ServerAddress</code> for incoming TCP connect
+ * requests. When an connection is accepted it is handed to the
+ * internal socket connector.
+ *
+ * @param address this is the address used to accept connections
+ *
+ * @return this returns the actual local address that is used
+ */
+ public SocketAddress listen(SocketAddress address) throws IOException {
+ return listen(address, null);
+ }
+
+ /**
+ * This creates a new background task that will listen to the
+ * specified <code>ServerAddress</code> for incoming TCP connect
+ * requests. When an connection is accepted it is handed to the
+ * internal socket connector.
+ *
+ * @param address this is the address used to accept connections
+ * @param context this is used for secure SSL connections
+ *
+ * @return this returns the actual local address that is used
+ */
+ public SocketAddress listen(SocketAddress address, SSLContext context) throws IOException {
+ SocketListener listener = new SocketListener(address, processor, analyzer, context);
+
+ if(processor != null) {
+ listener.process();
+ listeners.add(listener);
+ }
+ return listener.getAddress();
+ }
+
+ /**
+ * This is used to close all the listeners that have been
+ * added to the connection. Closing all the listeners in the
+ * set ensures that there are no lingering threads or sockets
+ * consumed by the connection after the connection is closed.
+ *
+ * @throws IOException thrown if there is an error closing
+ */
+ public void close() throws IOException {
+ for(Closeable listener : listeners) {
+ listener.close();
+ }
+ if(analyzer != null) {
+ analyzer.stop();
+ }
+ listeners.clear();
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketTrace.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketTrace.java
new file mode 100644
index 00000000..18055337
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/connect/SocketTrace.java
@@ -0,0 +1,75 @@
+/*
+ * SocketTrace.java February 2012
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.connect;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>SocketTrace</code> is used to wrap an trace for safety.
+ * Wrapping an trace in this way ensures that even if the trace is
+ * badly written there is little chance that it will affect the
+ * operation of the server.
+ *
+ * @author Niall Gallagher
+ */
+class SocketTrace implements Trace {
+
+ /**
+ * This is the actual trace that is being wrapped by this.
+ */
+ private final Trace trace;
+
+ /**
+ * Constructor for the <code>SocketTrace</code> object. This will
+ * create a trace object that wraps the one provided. If the
+ * provided trace is null then this will simply ignore all events.
+ *
+ * @param trace this is the trace that is to be wrapped by this
+ */
+ public SocketTrace(Trace trace) {
+ this.trace = trace;
+ }
+
+ /**
+ * This method is used to accept an event that occurred on the socket
+ * associated with this trace. Typically the event is a symbolic
+ * description of the event such as an enum or a string.
+ *
+ * @param event this is the event that occurred on the socket
+ */
+ public void trace(Object event) {
+ if(trace != null) {
+ trace.trace(event);
+ }
+ }
+
+ /**
+ * This method is used to accept an event that occurred on the socket
+ * associated with this trace. Typically the event is a symbolic
+ * description of the event such as an enum or a string.
+ *
+ * @param event this is the event that occurred on the socket
+ * @param value provides additional information such as an exception
+ */
+ public void trace(Object event, Object value) {
+ if(trace != null) {
+ trace.trace(event, value);
+ }
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Action.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Action.java
new file mode 100644
index 00000000..4989eaeb
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Action.java
@@ -0,0 +1,71 @@
+/*
+ * Action.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.nio.channels.SelectableChannel;
+
+/**
+ * The <code>Action</code> object is used to represent an action that
+ * the distributor is to process. This contains the operation and
+ * the required I/O events as an integer bit mask. When an operation
+ * is considered ready it will be handed to an executor to execute.
+ *
+ * @author Niall Gallagher
+ */
+interface Action extends Runnable {
+
+ /**
+ * This is used to get the expiry for the operation. The expiry
+ * represents some static time in the future when the action will
+ * expire if it does not become ready. This is used to cancel the
+ * operation so that it does not remain in the distributor.
+ *
+ * @return the remaining time this operation will wait for
+ */
+ long getExpiry();
+
+ /**
+ * This returns the I/O operations that the action is interested
+ * in as an integer bit mask. When any of these operations are
+ * ready the distributor will execute the provided operation.
+ *
+ * @return the integer bit mask of interested I/O operations
+ */
+ int getInterest();
+
+ /**
+ * This is the <code>SelectableChannel</code> which is used to
+ * determine if the operation should be executed. If the channel
+ * is ready for a given I/O event it can be run. For instance if
+ * the operation is used to perform some form of read operation
+ * it can be executed when ready to read data from the channel.
+ *
+ * @return this returns the channel used to govern execution
+ */
+ SelectableChannel getChannel();
+
+ /**
+ * This is used to acquire the <code>Operation</code> that is to
+ * be executed when the required operations are ready. It is the
+ * responsibility of the distributor to invoke the operation.
+ *
+ * @return the operation to be executed when it is ready
+ */
+ Operation getOperation();
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionDistributor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionDistributor.java
new file mode 100644
index 00000000..65dc8d21
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionDistributor.java
@@ -0,0 +1,741 @@
+/*
+ * ActionDistributor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import static java.nio.channels.SelectionKey.OP_READ;
+import static java.nio.channels.SelectionKey.OP_WRITE;
+import static org.simpleframework.transport.reactor.ReactorEvent.CHANNEL_CLOSED;
+import static org.simpleframework.transport.reactor.ReactorEvent.CLOSE_SELECTOR;
+import static org.simpleframework.transport.reactor.ReactorEvent.ERROR;
+import static org.simpleframework.transport.reactor.ReactorEvent.EXECUTE_ACTION;
+import static org.simpleframework.transport.reactor.ReactorEvent.INVALID_KEY;
+import static org.simpleframework.transport.reactor.ReactorEvent.READ_INTEREST_READY;
+import static org.simpleframework.transport.reactor.ReactorEvent.REGISTER_INTEREST;
+import static org.simpleframework.transport.reactor.ReactorEvent.REGISTER_READ_INTEREST;
+import static org.simpleframework.transport.reactor.ReactorEvent.REGISTER_WRITE_INTEREST;
+import static org.simpleframework.transport.reactor.ReactorEvent.SELECT;
+import static org.simpleframework.transport.reactor.ReactorEvent.SELECT_CANCEL;
+import static org.simpleframework.transport.reactor.ReactorEvent.SELECT_EXPIRED;
+import static org.simpleframework.transport.reactor.ReactorEvent.UPDATE_INTEREST;
+import static org.simpleframework.transport.reactor.ReactorEvent.UPDATE_READ_INTEREST;
+import static org.simpleframework.transport.reactor.ReactorEvent.UPDATE_WRITE_INTEREST;
+import static org.simpleframework.transport.reactor.ReactorEvent.WRITE_INTEREST_READY;
+
+import java.io.IOException;
+import java.nio.channels.Channel;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
+
+import org.simpleframework.common.thread.Daemon;
+import org.simpleframework.transport.trace.Trace;
+
+ /**
+ * The <code>ActionDistributor</code> is used to execute operations
+ * that have an interested I/O event ready. This acts much like a
+ * scheduler would in that it delays the execution of the operations
+ * until such time as the associated <code>SelectableChannel</code>
+ * has an interested I/O event ready.
+ * <p>
+ * This distributor has two modes, one mode is used to cancel the
+ * channel once an I/O event has occurred. This means that the channel
+ * is removed from the <code>Selector</code> so that the selector
+ * does not break when asked to select again. cancelling the channel
+ * is useful when the operation execution may not fully read the
+ * payload or when the operation takes a significant amount of time.
+ *
+ * @see org.simpleframework.transport.reactor.ExecutorReactor
+ */
+class ActionDistributor extends Daemon implements OperationDistributor {
+
+ /**
+ * This is used to determine the operations that need cancelling.
+ */
+ private Map<Channel, ActionSet> executing;
+
+ /**
+ * This is used to keep track of actions currently in selection.
+ */
+ private Map<Channel, ActionSet> selecting;
+
+ /**
+ * This is the queue that is used to invalidate channels.
+ */
+ private Queue<Channel> invalid;
+
+ /**
+ * This is the queue that is used to provide the operations.
+ */
+ private Queue<Action> pending;
+
+ /**
+ * This is the selector used to select for interested events.
+ */
+ private ActionSelector selector;
+
+ /**
+ * This is used to execute the operations that are ready.
+ */
+ private Executor executor;
+
+ /**
+ * This is used to signal when the distributor has closed.
+ */
+ private Latch latch;
+
+ /**
+ * This is the duration in milliseconds the operation expires in.
+ */
+ private long expiry;
+
+ /**
+ * This is time in milliseconds when the next expiry will occur.
+ */
+ private long update;
+
+ /**
+ * This is used to determine the mode the distributor uses.
+ */
+ private boolean cancel;
+
+ /**
+ * Constructor for the <code>ActionDistributor</code> object. This
+ * will create a distributor that distributes operations when those
+ * operations show that they are ready for a given I/O event. The
+ * interested I/O events are provided as a bitmask taken from the
+ * actions of the <code>SelectionKey</code>. Distribution of the
+ * operations is passed to the provided executor object.
+ *
+ * @param executor this is the executor used to execute operations
+ */
+ public ActionDistributor(Executor executor) throws IOException {
+ this(executor, true);
+ }
+
+ /**
+ * Constructor for the <code>ActionDistributor</code> object. This
+ * will create a distributor that distributes operations when those
+ * operations show that they are ready for a given I/O event. The
+ * interested I/O events are provided as a bitmask taken from the
+ * actions of the <code>SelectionKey</code>. Distribution of the
+ * operations is passed to the provided executor object.
+ *
+ * @param executor this is the executor used to execute operations
+ * @param cancel should the channel be removed from selection
+ */
+ public ActionDistributor(Executor executor, boolean cancel) throws IOException {
+ this(executor, cancel, 120000);
+ }
+
+ /**
+ * Constructor for the <code>ActionDistributor</code> object. This
+ * will create a distributor that distributes operations when those
+ * operations show that they are ready for a given I/O event. The
+ * interested I/O events are provided as a bitmask taken from the
+ * actions of the <code>SelectionKey</code>. Distribution of the
+ * operations is passed to the provided executor object.
+ *
+ * @param executor this is the executor used to execute operations
+ * @param cancel should the channel be removed from selection
+ * @param expiry this the maximum idle time for an operation
+ */
+ public ActionDistributor(Executor executor, boolean cancel, long expiry) throws IOException {
+ this.selecting = new LinkedHashMap<Channel, ActionSet>();
+ this.executing = new LinkedHashMap<Channel, ActionSet>();
+ this.pending = new ConcurrentLinkedQueue<Action>();
+ this.invalid = new ConcurrentLinkedQueue<Channel>();
+ this.selector = new ActionSelector();
+ this.latch = new Latch();
+ this.executor = executor;
+ this.cancel = cancel;
+ this.expiry = expiry;
+ this.start();
+ }
+
+ /**
+ * This is used to process the <code>Operation</code> object. This
+ * will wake up the selector if it is currently blocked selecting
+ * and register the operations associated channel. Once the
+ * selector is awake it will acquire the operation from the queue
+ * and register the associated <code>SelectableChannel</code> for
+ * selection. The operation will then be executed when the channel
+ * is ready for the interested I/O events.
+ *
+ * @param task this is the task that is scheduled for distribution
+ * @param require this is the bit-mask value for interested events
+ */
+ public void process(Operation task, int require) throws IOException {
+ Action action = new ExecuteAction(task, require, expiry);
+
+ if(!isActive()) {
+ throw new IOException("Distributor is closed");
+ }
+ pending.offer(action);
+ selector.wake();
+ }
+
+ /**
+ * This is used to close the distributor such that it cancels all
+ * of the registered channels and closes down the selector. This
+ * is used when the distributor is no longer required, after the
+ * close further attempts to process operations will fail.
+ */
+ public void close() throws IOException {
+ stop();
+ selector.wake();
+ latch.close();
+ }
+
+ /**
+ * This returns the number of channels that are currently selecting
+ * with this distributor. When busy this can get quite high, however
+ * it must return to zero as soon as all tasks have completed.
+ *
+ * @return return the number of channels currently selecting
+ */
+ public int size() {
+ return selecting.size();
+ }
+
+ /**
+ * Performs the execution of the distributor. Each distributor runs
+ * on an asynchronous thread to the <code>Reactor</code> which is
+ * used to perform the selection on a set of channels. Each time
+ * there is a new operation to be processed this will take the
+ * operation from the ready queue, cancel all outstanding channels,
+ * and register the operations associated channel for selection.
+ */
+ public void run() {
+ try {
+ execute();
+ } finally {
+ purge();
+ }
+ }
+
+ /**
+ * Performs the execution of the distributor. Each distributor runs
+ * on an asynchronous thread to the <code>Reactor</code> which is
+ * used to perform the selection on a set of channels. Each time
+ * there is a new operation to be processed this will take the
+ * operation from the ready queue, cancel all outstanding channels,
+ * and register the operations associated channel for selection.
+ */
+ private void execute() {
+ while(isActive()) {
+ try {
+ register();
+ cancel();
+ expire();
+ distribute();
+ validate();
+ } catch(Exception cause) {
+ report(cause);
+ }
+ }
+ }
+
+ /**
+ * This will purge all the actions from the distributor when the
+ * distributor ends. If there are any threads waiting on the close
+ * to finish they are signalled when all operations are purged.
+ * This will allow them to return ensuring no operations linger.
+ */
+ private void purge() {
+ try {
+ register();
+ cancel();
+ clear();
+ } catch(Exception cause) {
+ report(cause);
+ }
+ }
+
+ /**
+ * This method is called to ensure that if there is a global
+ * error that each action will know about it. Such an issue could
+ * be file handle exhaustion or an out of memory error. It is
+ * also possible that a poorly behaving action could cause an
+ * issue which should be know the the entire system.
+ *
+ * @param cause this is the exception to report
+ */
+ private void report(Exception cause) {
+ Set<Channel> channels = selecting.keySet();
+
+ for(Channel channel : channels) {
+ ActionSet set = selecting.get(channel);
+ Action[] list = set.list();
+
+ for(Action action : list) {
+ Operation operation = action.getOperation();
+ Trace trace = operation.getTrace();
+
+ try {
+ trace.trace(ERROR, cause);
+ } catch(Exception e) {
+ invalid.offer(channel);
+ }
+ }
+ }
+ invalid.clear();
+ }
+
+ /**
+ * Here we perform an expire which will take all of the registered
+ * sockets and expire it. This ensures that the operations can be
+ * executed within the executor and the cancellation of the sockets
+ * can be performed. Once this method has finished then all of
+ * the operations will have been scheduled for execution.
+ */
+ private void clear() throws IOException {
+ List<ActionSet> sets = selector.registeredSets();
+
+ for(ActionSet set : sets) {
+ Action[] list = set.list();
+
+ for(Action action : list) {
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+
+ try {
+ trace.trace(CLOSE_SELECTOR);
+ expire(set, Long.MAX_VALUE);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+ }
+ selector.close();
+ latch.signal();
+ }
+
+ /**
+ * This method is used to expire registered operations that remain
+ * idle within the selector. Operations specify a time at which
+ * point they wish to be cancelled if the I/O event they wait on
+ * has not arisen. This will enables the cancelled operation to be
+ * cancelled so that the resources it occupies can be released.
+ */
+ private void expire() throws IOException {
+ List<ActionSet> sets = selector.registeredSets();
+
+ if(cancel) {
+ long time = System.currentTimeMillis();
+
+ if(update <= time) {
+ for(ActionSet set : sets) {
+ expire(set, time);
+ }
+ update = time +10000;
+ }
+ }
+ }
+
+ /**
+ * This method is used to expire registered operations that remain
+ * idle within the selector. Operations specify a time at which
+ * point they wish to be if the I/O event they wait on
+ * has not arisen. This will enables the cancelled operation to be
+ * cancelled so that the resources it occupies can be released.
+ *
+ * @param set this is the selection set check for expired actions
+ * @param time this is the time to check the expiry against
+ */
+ private void expire(ActionSet set, long time) throws IOException {
+ Action[] actions = set.list();
+ SelectionKey key = set.key();
+
+ if(key.isValid()) {
+ int mask = key.interestOps();
+
+ for(Action action : actions) {
+ int interest = action.getInterest();
+ long expiry = action.getExpiry();
+
+ if(expiry < time) {
+ expire(set, action);
+ mask &= ~interest;
+ }
+ }
+ update(set, mask);
+ }
+ }
+
+ /**
+ * This is used to update the interested operations of a set of
+ * actions. If there are no interested operations the set will be
+ * cancelled, otherwise the selection key will be updated with the
+ * new operations provided by the bitmask.
+ *
+ * @param set this is the action set that is to be updated
+ * @param interest this is the bitmask containing the operations
+ */
+ private void update(ActionSet set, int interest) throws IOException {
+ SelectionKey key = set.key();
+
+ if(interest == 0) {
+ Channel channel = key.channel();
+
+ selecting.remove(channel);
+ key.cancel();
+ } else {
+ key.interestOps(interest);
+ }
+ }
+
+ /**
+ * This method is used to expire registered operations that remain
+ * idle within the selector. Operations specify a time at which
+ * point they wish to be cancelled if the I/O event they wait on
+ * has not arisen. This will enables the cancelled operation to be
+ * cancelled so that the resources it occupies can be released.
+ *
+ * @param set this is the action set containing the actions
+ * @param action this is the actual action to be cancelled
+ */
+ private void expire(ActionSet set, Action action) throws IOException {
+ Action cancel = new CancelAction(action);
+
+ if(set != null) {
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+ int interest = action.getInterest();
+
+ try {
+ trace.trace(SELECT_EXPIRED, interest);
+ set.remove(interest);
+ execute(cancel);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+ }
+
+ /**
+ * This method is used to perform simple validation. It ensures
+ * that directly after the processing loop any channels that
+ * are registered that have been cancelled or are closed will
+ * be removed from the selecting map and rejected.
+ */
+ private void validate() throws IOException {
+ Set<Channel> channels = selecting.keySet();
+
+ for(Channel channel : channels) {
+ ActionSet set = selecting.get(channel);
+ SelectionKey key = set.key();
+
+ if(!key.isValid()) {
+ invalid.offer(channel);
+ }
+ }
+ for(Channel channel : invalid) {
+ invalidate(channel);
+ }
+ invalid.clear();
+ }
+
+ /**
+ * This method is used to remove the channel from the selecting
+ * registry. It is rare that this will every happen, however it
+ * is important that tasks are cleared out in this manner as it
+ * could lead to a memory leak if left for a long time.
+ *
+ * @param channel this is the channel being validated
+ */
+ private void invalidate(Channel channel) throws IOException {
+ ActionSet set = selecting.remove(channel);
+ Action[] list = set.list();
+
+ for(Action action : list) {
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+
+ try {
+ trace.trace(INVALID_KEY);
+ execute(action); // reject
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+ }
+
+ /**
+ * This is used to cancel any selection keys that have previously
+ * been selected with an interested I/O event. Performing a cancel
+ * here ensures that on a the next select the associated channel
+ * is not considered, this ensures the select does not break.
+ */
+ private void cancel() throws IOException {
+ Collection<ActionSet> list = executing.values();
+
+ for(ActionSet set : list) {
+ Action[] actions = set.list();
+
+ for(Action action : actions) {
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+
+ trace.trace(SELECT_CANCEL);
+ }
+ set.cancel();
+ set.clear();
+ }
+ executing.clear();
+ }
+
+ /**
+ * Here all the enqueued <code>Operation</code> objects will be
+ * registered for selection. Each operations channel is used for
+ * selection on the interested I/O events. Once the I/O event
+ * occurs for the channel the operation is scheduled for execution.
+ */
+ private void register() throws IOException {
+ while(!pending.isEmpty()) {
+ Action action = pending.poll();
+
+ if(action != null) {
+ SelectableChannel channel = action.getChannel();
+ ActionSet set = executing.remove(channel);
+
+ if(set == null) {
+ set = selecting.get(channel);
+ }
+ if(set != null) {
+ update(action, set);
+ } else {
+ register(action);
+ }
+ }
+ }
+ }
+
+ /**
+ * Here the specified <code>Operation</code> object is registered
+ * with the selector. If the associated channel had previously
+ * been cancelled it is removed from the cancel map to ensure it
+ * is not removed from the selector when cancellation is done.
+ *
+ * @param action this is the operation that is to be registered
+ */
+ private void register(Action action) throws IOException {
+ SelectableChannel channel = action.getChannel();
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+
+ try {
+ if(channel.isOpen()) {
+ trace.trace(SELECT);
+ select(action);
+ } else {
+ trace.trace(CHANNEL_CLOSED);
+ selecting.remove(channel);
+ execute(action); // reject
+ }
+ }catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+
+ /**
+ * Here the specified <code>Operation</code> object is registered
+ * with the selector. If the associated channel had previously
+ * been cancelled it is removed from the cancel map to ensure it
+ * is not removed from the selector when cancellation is done.
+ *
+ * @param action this is the operation that is to be registered
+ * @param set this is the action set to register the action with
+ */
+ private void update(Action action, ActionSet set) throws IOException {
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+ SelectionKey key = set.key();
+ int interest = action.getInterest();
+ int current = key.interestOps();
+ int updated = current | interest;
+
+ try {
+ if(OP_READ == (interest & OP_READ)) {
+ trace.trace(UPDATE_READ_INTEREST);
+ }
+ if(OP_WRITE == (interest & OP_WRITE)) {
+ trace.trace(UPDATE_WRITE_INTEREST);
+ }
+ trace.trace(UPDATE_INTEREST, updated);
+ key.interestOps(updated);
+ set.attach(action);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+
+ /**
+ * This method is used to perform an actual select on a channel. It
+ * will register the channel with the internal selector using the
+ * required I/O event bit mask. In order to ensure that selection
+ * is performed correctly the provided channel must be connected.
+ *
+ * @param action this is the operation that is to be registered
+ *
+ * @return this returns the selection key used for selection
+ */
+ private void select(Action action) throws IOException {
+ SelectableChannel channel = action.getChannel();
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+ int interest = action.getInterest();
+
+ if(interest > 0) {
+ ActionSet set = selector.register(channel, interest);
+
+ if(OP_READ == (interest & OP_READ)) {
+ trace.trace(REGISTER_READ_INTEREST);
+ }
+ if(OP_WRITE == (interest & OP_WRITE)) {
+ trace.trace(REGISTER_WRITE_INTEREST);
+ }
+ trace.trace(REGISTER_INTEREST, interest);
+ set.attach(action);
+ selecting.put(channel, set);
+ }
+ }
+
+ /**
+ * This method is used to perform the select and if required queue
+ * the operations that are ready for execution. If the selector
+ * is woken up without any ready channels then this will return
+ * quietly. If however there are a number of channels ready to be
+ * processed then they are handed to the executor object and
+ * marked as ready for cancellation.
+ */
+ private void distribute() throws IOException {
+ if(selector.select(5000) > 0) {
+ if(isActive()) {
+ process();
+ }
+ }
+ }
+
+ /**
+ * This will iterate over the set of selection keys and process each
+ * of them. The <code>Operation</code> associated with the selection
+ * key is handed to the executor to perform the channel operation.
+ * Also, if configured to cancel, this method will add the channel
+ * and the associated selection key to the cancellation map.
+ */
+ private void process() throws IOException{
+ List<ActionSet> ready = selector.selectedSets();
+
+ for(ActionSet set : ready) {
+ process(set);
+ remove(set);
+ }
+ }
+
+ /**
+ * This will use the specified action set to acquire the channel
+ * and <code>Operation</code> associated with it to hand to the
+ * executor to perform the channel operation.
+ *
+ * @param set this is the set of actions that are to be processed
+ */
+ private void process(ActionSet set) throws IOException {
+ Action[] actions = set.ready();
+
+ for(Action action : actions) {
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+ int interest = action.getInterest();
+
+ try {
+ if(OP_READ == (interest & OP_READ)) {
+ trace.trace(READ_INTEREST_READY, interest);
+ }
+ if(OP_WRITE == (interest & OP_WRITE)) {
+ trace.trace(WRITE_INTEREST_READY, interest);
+ }
+ execute(action);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+ }
+
+ /**
+ * This method ensures that references to the actions and channel
+ * are cleared from this instance. To ensure there are no memory
+ * leaks it is important to clear out all actions and channels.
+ * Also, if configured to cancel executing actions this will
+ * register the channel and actions to cancel on the next loop.
+ *
+ * @param set this is the set of actions that are to be removed
+ */
+ private void remove(ActionSet set) throws IOException {
+ Channel channel = set.channel();
+ SelectionKey key = set.key();
+
+ if(key.isValid()) {
+ int interest = set.interest();
+ int ready = key.readyOps();
+
+ if(cancel) {
+ int remaining = interest & ~ready;
+
+ if(remaining == 0) {
+ executing.put(channel, set);
+ } else {
+ key.interestOps(remaining);
+ }
+ set.remove(ready);
+ }
+ } else {
+ selecting.remove(channel);
+ }
+ }
+
+ /**
+ * This is where the action is handed off to the executor. Before
+ * the action is executed a trace event is generated, this will
+ * ensure that the entry and exit points can be tracked. It is
+ * also useful in debugging performance issues and memory leaks.
+ *
+ * @param action this is the action to execute
+ */
+ private void execute(Action action) {
+ Operation task = action.getOperation();
+ Trace trace = task.getTrace();
+ int interest = action.getInterest();
+
+ try {
+ trace.trace(EXECUTE_ACTION, interest);
+ executor.execute(action);
+ } catch(Exception cause) {
+ trace.trace(ERROR, cause);
+ }
+ }
+}
+
+ \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSelector.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSelector.java
new file mode 100644
index 00000000..a9e78e98
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSelector.java
@@ -0,0 +1,193 @@
+/*
+ * ActionSelector.java February 2013
+ *
+ * Copyright (C) 2013, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The <code>ActionSelector</code> object is used to perform socket
+ * based selection with the help of the <code>ActionSet</code> object.
+ * All registered channels have an associated action set. The action
+ * set contains a set of actions that should be executed when the
+ * selector selects the channel.
+ *
+ * @author Niall Gallagher
+ */
+class ActionSelector {
+
+ /**
+ * This is the selector used to perform the select operations.
+ */
+ private final Selector selector;
+
+ /**
+ * Constructor for the <code>ActionSelector</code> object. This is
+ * used to create a selector that can register socket channels
+ * with an associated <code>ActionSet</code>. The set can then be
+ * used to store actions to be executed upon selection.
+ */
+ public ActionSelector() throws IOException {
+ this.selector = Selector.open();
+ }
+
+ /**
+ * This is used to perform a select on the selector. This returns
+ * the number of channels that have been selected. If this returns
+ * a number greater than zero the <code>ready</code> method can
+ * be used to acquire the actions that are ready for execution.
+ *
+ * @param timeout this is the timeout associated with the select
+ *
+ * @return this returns the number of channels that are ready
+ */
+ public int select(long timeout) throws IOException {
+ return selector.select(timeout);
+ }
+
+ /**
+ * This performs the actual registration of the channel for selection
+ * based on the provided interest bitmask. When the channel has been
+ * registered for selection this returns an <code>ActionSet</code>
+ * for the selection key. The set can then be used to register the
+ * actions that should be executed when selection succeeds.
+ *
+ * @param channel this is the channel to register for selection
+ * @param interest this is the interested operations bitmask
+ *
+ * @return this is the action set associated with the registration
+ */
+ public ActionSet register(SelectableChannel channel, int interest) throws IOException {
+ SelectionKey key = channel.register(selector, interest);
+ Object value = key.attachment();
+
+ if(value == null) {
+ value = new ActionSet(key);
+ key.attach(value);
+ }
+ return (ActionSet)value;
+ }
+
+ /**
+ * This is used to acquire all the action sets that are associated
+ * with this selector. Only action sets that have a valid selection
+ * key are returned. Modification of the list will not affect the
+ * associated selector instance.
+ *
+ * @return this returns all the associated action sets for this
+ */
+ public List<ActionSet> registeredSets() {
+ Set<SelectionKey> keys = selector.keys();
+ Iterator<SelectionKey> ready = keys.iterator();
+
+ return registeredSets(ready);
+ }
+
+ /**
+ * This is used to acquire all the action sets that are associated
+ * with this selector. Only action sets that have a valid selection
+ * key are returned. Modification of the list will not affect the
+ * associated selector instance.
+ *
+ * @param keys the selection keys to get the associated sets from
+ *
+ * @return this returns all the associated action sets for this
+ */
+ private List<ActionSet> registeredSets(Iterator<SelectionKey> keys) {
+ List<ActionSet> sets = new LinkedList<ActionSet>();
+
+ while(keys.hasNext()) {
+ SelectionKey key = keys.next();
+ ActionSet actions = (ActionSet)key.attachment();
+
+ if(!key.isValid()) {
+ key.cancel();
+ } else {
+ sets.add(actions);
+ }
+ }
+ return sets;
+ }
+
+ /**
+ * This is used to acquire all the action sets that are selected
+ * by this selector. All action sets returned are unregistered from
+ * the selector and must be registered again to hear about further
+ * I/O events that occur on the associated channel.
+ *
+ * @return this returns all the selected action sets for this
+ */
+ public List<ActionSet> selectedSets() throws IOException {
+ Set<SelectionKey> keys = selector.selectedKeys();
+ Iterator<SelectionKey> ready = keys.iterator();
+
+ return selectedSets(ready);
+ }
+
+ /**
+ * This is used to acquire all the action sets that are selected
+ * by this selector. All action sets returned are unregistered from
+ * the selector and must be registered again to hear about further
+ * I/O events that occur on the associated channel.
+ *
+ * @param keys the selection keys to get the associated sets from
+ *
+ * @return this returns all the selected action sets for this
+ */
+ private List<ActionSet> selectedSets(Iterator<SelectionKey> keys) {
+ List<ActionSet> ready = new LinkedList<ActionSet>();
+
+ while(keys.hasNext()) {
+ SelectionKey key = keys.next();
+ ActionSet actions = (ActionSet)key.attachment();
+
+ if(key != null) {
+ keys.remove();
+ }
+ if(key != null) {
+ ready.add(actions);
+ }
+ }
+ return ready;
+ }
+
+ /**
+ * This is used to wake the selector if it is in the middle of a
+ * select operation. Waking up the selector in this manner is
+ * useful if further actions are to be registered with it.
+ */
+ public void wake() throws IOException {
+ selector.wakeup();
+ }
+
+ /**
+ * This is used to close the associated selector. Further attempts
+ * to register a channel with the selector will fail. All actions
+ * should be cancelled before closing the selector in this way.
+ */
+ public void close() throws IOException {
+ selector.close();
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSet.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSet.java
new file mode 100644
index 00000000..adc4ef79
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ActionSet.java
@@ -0,0 +1,269 @@
+/*
+ * ActionSet.java February 2013
+ *
+ * Copyright (C) 2013, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import static java.nio.channels.SelectionKey.OP_ACCEPT;
+import static java.nio.channels.SelectionKey.OP_CONNECT;
+import static java.nio.channels.SelectionKey.OP_READ;
+import static java.nio.channels.SelectionKey.OP_WRITE;
+
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+
+/**
+ * The <code>ActionSet</code> object represents a set of actions that
+ * are associated with a particular selection key. Here the set
+ * stores an <code>Action</code> for each of the interested operation
+ * types. In some situations a single action may be interested in
+ * several operations which must be remembered by the set.
+ *
+ * @author Niall Gallagher
+ */
+class ActionSet {
+
+ /**
+ * This is the selection key associated with the action set.
+ */
+ private final SelectionKey key;
+
+ /**
+ * This contains the the actions indexed by operation type.
+ */
+ private final Action[] set;
+
+ /**
+ * Constructor for the <code>ActionSet</code> object. This is
+ * used to create a set for storing actions keyed by operation
+ * type. Only one action is kept per operation type.
+ *
+ * @param key this is the associated selection key
+ */
+ public ActionSet(SelectionKey key) {
+ this.set = new Action[4];
+ this.key = key;
+ }
+
+ /**
+ * This provides the selection key associated with the action set.
+ * For each ready operation on the selection key the set contains
+ * an action that can be executed.
+ *
+ * @return this provides the selection key for this action set
+ */
+ public SelectionKey key() {
+ return key;
+ }
+
+ /**
+ * This provides the channel associated with the action set. This
+ * is the channel that is registered for selection using the
+ * interested operations for the set.
+ *
+ * @return this returns the selectable channel for the action set
+ */
+ public SelectableChannel channel() {
+ return key.channel();
+ }
+
+ /**
+ * This provides an iterator of the actions that exist within the
+ * action set. Regardless of whether a single action is interested
+ * is several operations this will return an iteration of unique
+ * actions. Modifications to the iterator do not affect the set.
+ *
+ * @return this returns an iterator of unique actions for the set
+ */
+ public Action[] list() {
+ Action[] actions = new Action[4];
+ int count = 0;
+
+ for(Action action : set) {
+ if(action != null) {
+ actions[count++] = action;
+ }
+ }
+ return copyOf(actions, count);
+ }
+
+ /**
+ * This is sued to acquire all actions that match the currently
+ * ready operations of the key. All actions returned by this will
+ * be executed and the interest will typically be removed.
+ *
+ * @return returns the array of ready operations for the set
+ */
+ public Action[] ready() {
+ int ready = key.readyOps();
+
+ if(ready != 0) {
+ return get(ready);
+ }
+ return new Action[]{};
+ }
+
+ /**
+ * This is used to attach an action to the set for a specific
+ * interest bitmask. If the bitmask contains several operations
+ * then the action is registered for each individual operation.
+ *
+ * @param action this is the action that is to be attached
+ * @param interest this is the interest for the action
+ */
+ public void attach(Action action) {
+ int interest = action.getInterest();
+
+ if((interest | OP_READ) == interest) {
+ set[0] = action;
+ }
+ if((interest | OP_WRITE) == interest) {
+ set[1] = action;
+ }
+ if((interest | OP_ACCEPT) == interest) {
+ set[2] = action;
+ }
+ if((interest | OP_CONNECT) == interest) {
+ set[3] = action;
+ }
+ }
+
+ /**
+ * This is used to remove interest from the set. Removal of
+ * interest from the set is performed by registering a null for
+ * the interest operation.
+ *
+ * @param interest this is the interest to be removed
+ */
+ public Action[] remove(int interest) {
+ Action[] actions = get(interest);
+
+ if((interest | OP_READ) == interest) {
+ set[0] = null;
+ }
+ if((interest | OP_WRITE) == interest) {
+ set[1] = null;
+ }
+ if((interest | OP_ACCEPT) == interest) {
+ set[2] = null;
+ }
+ if((interest | OP_CONNECT) == interest) {
+ set[3] = null;
+ }
+ return actions;
+ }
+
+ /**
+ * This is used to acquire the actions that match the bitmask of
+ * interest operations. If there are no actions representing the
+ * interest required an empty array will be returned.
+ *
+ * @param interest this is the interest to acquire actions for
+ *
+ * @return this will return an array of actions for the interest
+ */
+ public Action[] get(int interest) {
+ Action[] actions = new Action[4];
+ int count = 0;
+
+ if((interest | OP_READ) == interest) {
+ if(set[0] != null) {
+ actions[count++] = set[0];
+ }
+ }
+ if((interest | OP_WRITE) == interest) {
+ if(set[1] != null) {
+ actions[count++] = set[1];
+ }
+ }
+ if((interest | OP_ACCEPT) == interest) {
+ if(set[2] != null) {
+ actions[count++] = set[2];
+ }
+ }
+ if((interest | OP_CONNECT) == interest) {
+ if(set[3] != null) {
+ actions[count++] = set[3];
+ }
+ }
+ return copyOf(actions, count);
+ }
+
+ /**
+ * This is used to create a copy of the specified list with only
+ * the first few non null values. This ensures we can keep the
+ * internal array immutable and still use arrays.
+ *
+ * @param list this is the list that is to be copied to a new array
+ * @param count this is the number of entries to copy from the list
+ *
+ * @return a copy of the original list up to the specified count
+ */
+ private Action[] copyOf(Action[] list, int count) {
+ Action[] copy = new Action[count];
+
+ for(int i = 0; i < count; i++) {
+ copy[i] = list[i];
+ }
+ return copy;
+ }
+
+ /**
+ * This is used to acquire the operations that this is interested
+ * in. If there are currently no registered actions then this will
+ * return zero. Interest is represented by non-null actions only.
+ *
+ * @return this returns the interested operations for this
+ */
+ public int interest() {
+ int interest = 0;
+
+ if(set[0] != null) {
+ interest |= OP_READ;
+ }
+ if(set[1] != null) {
+ interest |= OP_WRITE;
+ }
+ if(set[2] != null) {
+ interest |= OP_ACCEPT;
+ }
+ if(set[3] != null) {
+ interest |= OP_CONNECT;
+ }
+ return interest;
+ }
+
+ /**
+ * This is used to clear all interest from the set. This will
+ * basically clear out any actions that have been registered with
+ * the set. After invocation the iterator will be empty.
+ */
+ public void clear() {
+ set[0] = set[1] =
+ set[2] = set[3] = null;
+ }
+
+ /**
+ * This is used to cancel the <code>SelectionKey</code> associated
+ * with the action set. Canceling the key in this manner ensures
+ * it is not returned in further selection operations.
+ */
+ public void cancel() {
+ key.cancel();
+ }
+}
+ \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/CancelAction.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/CancelAction.java
new file mode 100644
index 00000000..c4ad894f
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/CancelAction.java
@@ -0,0 +1,114 @@
+/*
+ * CancelAction.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.nio.channels.SelectableChannel;
+
+/**
+ * The <code>CancelAction</code> object is used to represent a task
+ * that can be executed to cancel an operation. This is used in the
+ * place of a normal <code>Operation</code> to pass for execution
+ * when the operation has expired before the I/O event is was
+ * interested in occurred. Before this is executed the operation is
+ * removed from selection.
+ *
+ * @author Niall Gallagher
+ */
+class CancelAction implements Action {
+
+ /**
+ * This is the operation that is to be canceled by this action.
+ */
+ private final Operation task;
+
+ /**
+ * This is the operation object that is to be canceled.
+ */
+ private final Action action;
+
+ /**
+ * Constructor for the <code>Cancellation</code> object. This is
+ * used to create a runnable task that delegates to the cancel
+ * method of the operation. This will be executed asynchronously
+ * by the executor after being removed from selection.
+ *
+ * @param action this is the task that is to be canceled by this
+ */
+ public CancelAction(Action action) {
+ this.task = action.getOperation();
+ this.action = action;
+ }
+
+ /**
+ * This method is executed by the <code>Executor</code> object
+ * if the operation expires before the required I/O event(s)
+ * have occurred. It is typically used to shutdown the socket
+ * and release any resources associated with the operation.
+ */
+ public void run() {
+ task.cancel();
+ }
+
+ /**
+ * This is used to get the expiry for the operation. The expiry
+ * represents some static time in the future when the action will
+ * expire if it does not become ready. This is used to cancel the
+ * operation so that it does not remain in the distributor.
+ *
+ * @return the remaining time this operation will wait for
+ */
+ public long getExpiry() {
+ return 0;
+ }
+
+ /**
+ * This returns the I/O operations that the action is interested
+ * in as an integer bit mask. When any of these operations are
+ * ready the distributor will execute the provided operation.
+ *
+ * @return the integer bit mask of interested I/O operations
+ */
+ public int getInterest() {
+ return action.getInterest();
+ }
+
+ /**
+ * This is the <code>SelectableChannel</code> which is used to
+ * determine if the operation should be executed. If the channel
+ * is ready for a given I/O event it can be run. For instance if
+ * the operation is used to perform some form of read operation
+ * it can be executed when ready to read data from the channel.
+ *
+ * @return this returns the channel used to govern execution
+ */
+ public SelectableChannel getChannel() {
+ return action.getChannel();
+ }
+
+ /**
+ * This is used to acquire the <code>Operation</code> that is to
+ * be executed when the required operations are ready. It is the
+ * responsibility of the distributor to invoke the operation.
+ *
+ * @return the operation to be executed when it is ready
+ */
+ public Operation getOperation() {
+ return task;
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecuteAction.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecuteAction.java
new file mode 100644
index 00000000..b92174c3
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecuteAction.java
@@ -0,0 +1,121 @@
+/*
+ * ExecuteAction.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.nio.channels.SelectableChannel;
+
+/**
+ * The <code>ExecuteAction</code> object is represents an action
+ * that the distributor is to process. This contains the operation
+ * and the required I/O events as an integer bit mask as well as the
+ * selectable channel used to register for selection. In order to
+ * ensure that the action does not remain within the distributor for
+ * too long the action has an expiry time.
+ *
+ * @author Niall Gallagher
+ */
+class ExecuteAction implements Action {
+
+ /**
+ * The task to execute when the required operations is ready.
+ */
+ private final Operation task;
+
+ /**
+ * This is the bit mask of required operations to be executed.
+ */
+ private final int require;
+
+ /**
+ * This is the time in the future that the event will expire in.
+ */
+ private final long expiry;
+
+ /**
+ * Constructor for the <code>Event</code> object. The actions are
+ * used to encapsulate the task to execute and the operations
+ * to listen to when some action is to be performed.
+ *
+ * @param task this is the task to be executed when it is ready
+ * @param require this is the required operations to listen to
+ */
+ public ExecuteAction(Operation task, int require, long expiry) {
+ this.expiry = System.currentTimeMillis() + expiry;
+ this.require = require;
+ this.task = task;
+ }
+
+ /**
+ * This is used to execute the operation for the action. This will
+ * be executed when the interested I/O event is ready for the
+ * associated <code>SelectableChannel</code> object. If the action
+ * expires before the interested I/O operation is ready this will
+ * not be executed, instead the operation is canceled.
+ */
+ public void run() {
+ task.run();
+ }
+
+ /**
+ * This is used to get the expiry for the operation. The expiry
+ * represents some static time in the future when the action will
+ * expire if it does not become ready. This is used to cancel the
+ * operation so that it does not remain in the distributor.
+ *
+ * @return the remaining time this operation will wait for
+ */
+ public long getExpiry() {
+ return expiry;
+ }
+
+ /**
+ * This is the <code>SelectableChannel</code> which is used to
+ * determine if the operation should be executed. If the channel
+ * is ready for a given I/O event it can be run. For instance if
+ * the operation is used to perform some form of read operation
+ * it can be executed when ready to read data from the channel.
+ *
+ * @return this returns the channel used to govern execution
+ */
+ public SelectableChannel getChannel() {
+ return task.getChannel();
+ }
+
+ /**
+ * This is used to acquire the <code>Operation</code> that is to
+ * be executed when the required operations are ready. It is the
+ * responsibility of the distributor to invoke the operation.
+ *
+ * @return the operation to be executed when it is ready
+ */
+ public Operation getOperation() {
+ return task;
+ }
+
+ /**
+ * This returns the I/O operations that the action is interested
+ * in as an integer bit mask. When any of these operations are
+ * ready the distributor will execute the provided operation.
+ *
+ * @return the integer bit mask of interested I/O operations
+ */
+ public int getInterest() {
+ return require;
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecutorReactor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecutorReactor.java
new file mode 100644
index 00000000..9d741d8a
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ExecutorReactor.java
@@ -0,0 +1,132 @@
+/*
+ * ExecutorReactor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+/**
+ * The <code>ExecutorReactor</code> is used to schedule operation for
+ * execution using an <code>Executor</code> implementation. This can be
+ * useful when the operations performed are time intensive. For example
+ * if the operations performed a read of the underlying channel and
+ * then had to parse the contents of the payload. Such operations would
+ * reduce the performance of the reactor if it could not delegate to
+ * some other form of executor, as it would delay their execution.
+ *
+ * @author Niall Gallagher
+ */
+public class ExecutorReactor implements Reactor {
+
+ /**
+ * This is used to distribute the ready operations for execution.
+ */
+ private final OperationDistributor exchange;
+
+ /**
+ * This is used to execute the operations that ready to run.
+ */
+ private final Executor executor;
+
+ /**
+ * Constructor for the <code>ExecutorReactor</code> object. This is
+ * used to create a reactor that can delegate to the executor. This
+ * also accepts the operations it is interested in, the value is
+ * taken from the <code>SelectionKey</code> object. A bit mask can
+ * be used to show interest in several operations at once.
+ *
+ * @param executor this is the executor used to run the operations
+ */
+ public ExecutorReactor(Executor executor) throws IOException {
+ this(executor, 1);
+ }
+
+ /**
+ * Constructor for the <code>ExecutorReactor</code> object. This is
+ * used to create a reactor that can delegate to the executor. This
+ * also accepts the operations it is interested in, the value is
+ * taken from the <code>SelectionKey</code> object. A bit mask can
+ * be used to show interest in several operations at once.
+ *
+ * @param executor this is the executor used to run the operations
+ * @param count this is the number of distributors to be used
+ */
+ public ExecutorReactor(Executor executor, int count) throws IOException {
+ this(executor, count, 120000);
+ }
+
+ /**
+ * Constructor for the <code>ExecutorReactor</code> object. This is
+ * used to create a reactor that can delegate to the executor. This
+ * also accepts the operations it is interested in, the value is
+ * taken from the <code>SelectionKey</code> object. A bit mask can
+ * be used to show interest in several operations at once.
+ *
+ * @param executor this is the executor used to run the operations
+ * @param count this is the number of distributors to be used
+ * @param expiry the length of time to maintain and idle operation
+ */
+ public ExecutorReactor(Executor executor, int count, long expiry) throws IOException {
+ this.exchange = new PartitionDistributor(executor, count, expiry);
+ this.executor = executor;
+ }
+
+ /**
+ * This method is used to execute the provided operation without
+ * the need to specifically check for I/O events. This is used if
+ * the operation knows that the <code>SelectableChannel</code> is
+ * ready, or if the I/O operation can be performed without knowing
+ * if the channel is ready. Typically this is an efficient means
+ * to perform a poll rather than a select on the channel.
+ *
+ * @param task this is the task to execute immediately
+ */
+ public void process(Operation task) throws IOException {
+ executor.execute(task);
+ }
+
+ /**
+ * This method is used to execute the provided operation when there
+ * is an I/O event that task is interested in. This will used the
+ * operations <code>SelectableChannel</code> object to determine
+ * the events that are ready on the channel. If this reactor is
+ * interested in any of the ready events then the task is executed.
+ *
+ * @param task this is the task to execute on interested events
+ * @param require this is the bit-mask value for interested events
+ */
+ public void process(Operation task, int require) throws IOException {
+ exchange.process(task, require);
+ }
+
+ /**
+ * This is used to stop the reactor so that further requests to
+ * execute operations does nothing. This will clean up all of
+ * the reactors resources and unregister any operations that are
+ * currently awaiting execution. This should be used to ensure
+ * any threads used by the reactor gracefully stop.
+ */
+ public void stop() throws IOException {
+ exchange.close();
+ }
+}
+
+
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Latch.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Latch.java
new file mode 100644
index 00000000..deaee988
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Latch.java
@@ -0,0 +1,71 @@
+/*
+ * Latch.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * The <code>Latch</code> is used to provide a simple latch that will
+ * allow a thread to block until it is signaled that it is ready.
+ * The latch will block on the <code>close</code> method and when the
+ * latch is signaled the close method will release all threads.
+ *
+ * @author Niall Gallagher
+ */
+class Latch extends CountDownLatch {
+
+ /**
+ * Constructor for the <code>Latch</code> object. This will
+ * create a count down latch that will block when it is
+ * closed. Any blocked threads will be released when the
+ * latch is signaled that it is ready.
+ */
+ public Latch() {
+ super(1);
+ }
+
+ /**
+ * This is used to signal that the latch is ready. Invoking
+ * this method will release all threads that are blocking on
+ * the close method. This method is used when the distributor
+ * is closed and all operations have been purged.
+ */
+ public void signal() throws IOException {
+ try {
+ countDown();
+ } catch(Exception e) {
+ throw new IOException("Thread interrupted");
+ }
+ }
+
+ /**
+ * This will block all threads attempting to close the latch.
+ * All threads will be release when the latch is signaled. This
+ * is used to ensure the distributor blocks until it has fully
+ * purged all registered operations that are registered.
+ */
+ public void close() throws IOException {
+ try {
+ await();
+ } catch(Exception e){
+ throw new IOException("Thread interrupted");
+ }
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Operation.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Operation.java
new file mode 100644
index 00000000..e97be617
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Operation.java
@@ -0,0 +1,69 @@
+/*
+ * Operation.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.nio.channels.SelectableChannel;
+
+import org.simpleframework.transport.trace.Trace;
+
+/**
+ * The <code>Operation</code> interface is used to describe a task
+ * that can be executed when the associated channel is ready for some
+ * operation. Typically the <code>SelectableChannel</code> is used to
+ * register with a selector with a set of given interested operations
+ * when those operations can be performed this is executed.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.reactor.Reactor
+ */
+public interface Operation extends Runnable {
+
+ /**
+ * 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
+ */
+ Trace getTrace();
+
+ /**
+ * This is the <code>SelectableChannel</code> which is used to
+ * determine if the operation should be executed. If the channel
+ * is ready for a given I/O event it can be run. For instance if
+ * the operation is used to perform some form of read operation
+ * it can be executed when ready to read data from the channel.
+ *
+ * @return this returns the channel used to govern execution
+ */
+ SelectableChannel getChannel();
+
+ /**
+ * This is used to cancel the operation if it has timed out. This
+ * is typically invoked when it has been waiting in a selector for
+ * an extended duration of time without any active operations on
+ * it. In such a case the reactor must purge the operation to free
+ * the memory and open channels associated with the operation.
+ */
+ void cancel();
+}
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/OperationDistributor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/OperationDistributor.java
new file mode 100644
index 00000000..3a839092
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/OperationDistributor.java
@@ -0,0 +1,62 @@
+/*
+ * Distributor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.io.IOException;
+
+/**
+ * The <code>Distributor</code> object is used to execute operations
+ * that have an interested I/O event ready. This acts much like a
+ * scheduler would in that it delays the execution of the operations
+ * until such time as the associated <code>SelectableChannel</code>
+ * has an interested I/O event ready.
+ * <p>
+ * This distributor has two modes, one mode is used to cancel the
+ * channel once an I/O event has occurred. This means that the channel
+ * is removed from the <code>Selector</code> so that the selector
+ * does not break when asked to select again. Canceling the channel
+ * is useful when the operation execution may not fully read the
+ * payload or when the operation takes a significant amount of time.
+ *
+ * @see org.simpleframework.transport.reactor.ActionDistributor
+ */
+interface OperationDistributor {
+
+ /**
+ * This is used to process the <code>Operation</code> object. This
+ * will wake up the selector if it is currently blocked selecting
+ * and register the operations associated channel. Once the
+ * selector is awake it will acquire the operation from the queue
+ * and register the associated <code>SelectableChannel</code> for
+ * selection. The operation will then be executed when the channel
+ * is ready for the interested I/O events.
+ *
+ * @param task this is the task that is scheduled for distribution
+ * @param require this is the bit-mask value for interested events
+ */
+ void process(Operation task, int require) throws IOException;
+
+ /**
+ * This is used to close the distributor such that it cancels all
+ * of the registered channels and closes down the selector. This
+ * is used when the distributor is no longer required, after the
+ * close further attempts to process operations will fail.
+ */
+ void close() throws IOException;
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/PartitionDistributor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/PartitionDistributor.java
new file mode 100644
index 00000000..b0d24ac7
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/PartitionDistributor.java
@@ -0,0 +1,136 @@
+/*
+ * PartitionDistributor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+import java.util.concurrent.Executor;
+
+/**
+ * The <code>PartitionDistributor</code> object is a distributor that
+ * partitions the selection process in to several threads. Each of
+ * the threads has a single selector, and operations are distributed
+ * amongst the threads using the hash code of the socket. Partitions
+ * ensure that several selector threads can share a higher load and
+ * respond to a more I/O events.
+ *
+ * @author Niall Gallagher
+ */
+class PartitionDistributor implements OperationDistributor {
+
+ /**
+ * This contains the distributors that represent a partition.
+ */
+ private final OperationDistributor[] list;
+
+ /**
+ * Constructor for the <code>PartitionDistributor</code> object.
+ * This will create a distributor that partitions the operations
+ * amongst a pool of selectors using the channels hash code.
+ *
+ * @param executor this is the executor used to run operations
+ * @param count this is the number of partitions to be used
+ */
+ public PartitionDistributor(Executor executor, int count) throws IOException {
+ this(executor, count, 120000);
+ }
+
+ /**
+ * Constructor for the <code>PartitionDistributor</code> object.
+ * This will create a distributor that partitions the operations
+ * amongst a pool of selectors using the channels hash code.
+ *
+ * @param executor this is the executor used to run operations
+ * @param count this is the number of partitions to be used
+ * @param expiry this is the expiry duration that is to be used
+ */
+ public PartitionDistributor(Executor executor, int count, long expiry) throws IOException {
+ this.list = new OperationDistributor[count];
+ this.start(executor, expiry);
+ }
+
+ /**
+ * This is used to create the partitions that represent a thread
+ * used for selection. Operations will index to a particular one
+ * using the hash code of the operations channel. If there is only
+ * one partition all operations will index to the partition.
+ *
+ * @param executor the executor used to run the operations
+ * @param expiry this is the expiry duration that is to be used
+ */
+ private void start(Executor executor, long expiry) throws IOException {
+ for(int i = 0; i < list.length; i++) {
+ list[i] = new ActionDistributor(executor, true, expiry);
+ }
+ }
+
+ /**
+ * This is used to process the <code>Operation</code> object. This
+ * will wake up the selector if it is currently blocked selecting
+ * and register the operations associated channel. Once the
+ * selector is awake it will acquire the operation from the queue
+ * and register the associated <code>SelectableChannel</code> for
+ * selection. The operation will then be executed when the channel
+ * is ready for the interested I/O events.
+ *
+ * @param task this is the task that is scheduled for distribution
+ * @param require this is the bit-mask value for interested events
+ */
+ public void process(Operation task, int require) throws IOException {
+ int length = list.length;
+
+ if(length == 1) {
+ list[0].process(task, require);
+ } else {
+ process(task, require, length);
+ }
+ }
+
+ /**
+ * This is used to process the <code>Operation</code> object. This
+ * will wake up the selector if it is currently blocked selecting
+ * and register the operations associated channel. Once the
+ * selector is awake it will acquire the operation from the queue
+ * and register the associated <code>SelectableChannel</code> for
+ * selection. The operation will then be executed when the channel
+ * is ready for the interested I/O events.
+ *
+ * @param task this is the task that is scheduled for distribution
+ * @param require this is the bit-mask value for interested events
+ * @param length this is the number of distributors to hash with
+ */
+ private void process(Operation task, int require, int length) throws IOException {
+ SelectableChannel channel = task.getChannel();
+ int hash = channel.hashCode();
+
+ list[hash % length].process(task, require);
+ }
+
+ /**
+ * This is used to close the distributor such that it cancels all
+ * of the registered channels and closes down the selector. This
+ * is used when the distributor is no longer required, after the
+ * close further attempts to process operations will fail.
+ */
+ public void close() throws IOException {
+ for(OperationDistributor entry : list) {
+ entry.close();
+ }
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Reactor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Reactor.java
new file mode 100644
index 00000000..a947cb5c
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/Reactor.java
@@ -0,0 +1,79 @@
+/*
+ * Reactor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.io.IOException;
+
+/**
+ * The <code>Reactor</code> interface is used to describe an object
+ * that is used to schedule asynchronous I/O operations. An operation
+ * is performed by handing it to the reactor, which will determine
+ * if an interested event has occurred. This allows the operation to
+ * perform the task in a manner that does not block.
+ * <p>
+ * Implementing an <code>Operation</code> object requires that the
+ * operation itself is aware of the I/O task it is performing. For
+ * example, if the operation is concerned with reading data from the
+ * underlying channel then the operation should perform the read, if
+ * there is more data required then that operation to register with
+ * the reactor again to receive further notifications.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.reactor.Operation
+ */
+public interface Reactor {
+
+ /**
+ * This method is used to execute the provided operation without
+ * the need to specifically check for I/O events. This is used if
+ * the operation knows that the <code>SelectableChannel</code> is
+ * ready, or if the I/O operation can be performed without knowing
+ * if the channel is ready. Typically this is an efficient means
+ * to perform a poll rather than a select on the channel.
+ *
+ * @param task this is the task to execute immediately
+ */
+ void process(Operation task) throws IOException;
+
+ /**
+ * This method is used to execute the provided operation when there
+ * is an I/O event that task is interested in. This will used the
+ * operations <code>SelectableChannel</code> object to determine
+ * the events that are ready on the channel. If this reactor is
+ * interested in any of the ready events then the task is executed.
+ *
+ * @param task this is the task to execute on interested events
+ * @param require this is the bitmask value for interested events
+ */
+ void process(Operation task, int require) throws IOException;
+
+ /**
+ * This is used to stop the reactor so that further requests to
+ * execute operations does nothing. This will clean up all of
+ * the reactors resources and unregister any operations that are
+ * currently awaiting execution. This should be used to ensure
+ * any threads used by the reactor gracefully stop.
+ */
+ void stop() throws IOException;
+}
+
+
+
+
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ReactorEvent.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ReactorEvent.java
new file mode 100644
index 00000000..529644e3
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/ReactorEvent.java
@@ -0,0 +1,120 @@
+/*
+ * ReactorEvent.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+/**
+ * The <code>ReactorEvent</code> enumeration is used for tracing the
+ * operations that occur within the reactor. This is useful when the
+ * performance of the system needs to be monitored or when there is a
+ * resource or memory consumption issue that needs to be debugged.
+ *
+ * @author Niall Gallagher
+ */
+public enum ReactorEvent {
+
+ /**
+ * This event indicates the registration of an I/O interest.
+ */
+ SELECT,
+
+ /**
+ * This indicates that the selected I/O interest has not occurred.
+ */
+ SELECT_EXPIRED,
+
+ /**
+ * This occurs when a selection key is cancelled for all interests.
+ */
+ SELECT_CANCEL,
+
+ /**
+ * This is used to indicate the channel is already selecting.
+ */
+ ALREADY_SELECTING,
+
+ /**
+ * This occurs rarely however it indicates an invalid registration.
+ */
+ INVALID_KEY,
+
+ /**
+ * This occurs upon the initial registration of an I/O interest.
+ */
+ REGISTER_INTEREST,
+
+ /**
+ * This occurs upon the initial registration of a read I/O interest.
+ */
+ REGISTER_READ_INTEREST,
+
+ /**
+ * This occurs upon the initial registration of a write I/O interest.
+ */
+ REGISTER_WRITE_INTEREST,
+
+ /**
+ * This is used to indicate the operation interest changed.
+ */
+ UPDATE_INTEREST,
+
+ /**
+ * This occurs upon the initial registration of a read I/O interest.
+ */
+ UPDATE_READ_INTEREST,
+
+ /**
+ * This occurs upon the initial registration of a write I/O interest.
+ */
+ UPDATE_WRITE_INTEREST,
+
+ /**
+ * This indicates that the I/O interest has been satisfied.
+ */
+ INTEREST_READY,
+
+ /**
+ * This indicates that the I/O read interest has been satisfied.
+ */
+ READ_INTEREST_READY,
+
+ /**
+ * This indicates that the I/O write interest has been satisfied.
+ */
+ WRITE_INTEREST_READY,
+
+ /**
+ * This is the final action of executing the action.
+ */
+ EXECUTE_ACTION,
+
+ /**
+ * This occurs on an attempt to register an closed channel.
+ */
+ CHANNEL_CLOSED,
+
+ /**
+ * This occurs when the selector has been shutdown globally.
+ */
+ CLOSE_SELECTOR,
+
+ /**
+ * This occurs if there is an error with the selection.
+ */
+ ERROR,
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/SynchronousReactor.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/SynchronousReactor.java
new file mode 100644
index 00000000..102829df
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/reactor/SynchronousReactor.java
@@ -0,0 +1,107 @@
+/*
+ * SynchronousReactor.java February 2007
+ *
+ * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.reactor;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+import org.simpleframework.common.thread.SynchronousExecutor;
+
+/**
+ * The <code>SynchronousReactor</code> object is used to execute the
+ * ready operations of within a single synchronous thread. This is
+ * used when the I/O operations to be performed do not require much
+ * time to execute and so will not block the execution thread.
+ *
+ * @author Niall Gallagher
+ */
+public class SynchronousReactor implements Reactor {
+
+ /**
+ * This is used to distribute the ready operations for execution.
+ */
+ private final OperationDistributor exchange;
+
+ /**
+ * This is used to execute the operations that ready to run.
+ */
+ private final Executor executor;
+
+ /**
+ * Constructor for the <code>SynchronousReactor</code> object. This
+ * is used to create a reactor that does not require thread pooling
+ * to execute the ready operations. All I/O operations are run
+ * in the selection thread and should complete quickly.
+ */
+ public SynchronousReactor() throws IOException {
+ this(false);
+ }
+
+ /**
+ * Constructor for the <code>SynchronousReactor</code> object. This
+ * is used to create a reactor that does not require thread pooling
+ * to execute the ready operations. All I/O operations are run
+ * in the selection thread and should complete quickly.
+ *
+ * @param cancel determines the selection key should be cancelled
+ */
+ public SynchronousReactor(boolean cancel) throws IOException {
+ this.executor = new SynchronousExecutor();
+ this.exchange = new ActionDistributor(executor, cancel);
+ }
+
+ /**
+ * This method is used to execute the provided operation without
+ * the need to specifically check for I/O events. This is used if
+ * the operation knows that the <code>SelectableChannel</code> is
+ * ready, or if the I/O operation can be performed without knowing
+ * if the channel is ready. Typically this is an efficient means
+ * to perform a poll rather than a select on the channel.
+ *
+ * @param task this is the task to execute immediately
+ */
+ public void process(Operation task) throws IOException {
+ executor.execute(task);
+ }
+
+ /**
+ * This method is used to execute the provided operation when there
+ * is an I/O event that task is interested in. This will used the
+ * operations <code>SelectableChannel</code> object to determine
+ * the events that are ready on the channel. If this reactor is
+ * interested in any of the ready events then the task is executed.
+ *
+ * @param task this is the task to execute on interested events
+ * @param require this is the bit-mask value for interested events
+ */
+ public void process(Operation task, int require) throws IOException {
+ exchange.process(task, require);
+ }
+
+ /**
+ * This is used to stop the reactor so that further requests to
+ * execute operations does nothing. This will clean up all of
+ * the reactors resources and unregister any operations that are
+ * currently awaiting execution. This should be used to ensure
+ * any threads used by the reactor graceful stop.
+ */
+ public void stop() throws IOException {
+ exchange.close();
+ }
+}
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/trace/Trace.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/trace/Trace.java
new file mode 100644
index 00000000..7e3711de
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/trace/Trace.java
@@ -0,0 +1,57 @@
+/*
+ * Trace.java October 2012
+ *
+ * Copyright (C) 2002, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.trace;
+
+/**
+ * The <code>Trace</code> interface represents an trace log for various
+ * connection events. A trace is not limited to low level I/O events
+ * it can also gather event data that relates to protocol specific
+ * events. Using a trace in this manner enables problems to be solved
+ * with connections as they arise.
+ * <p>
+ * When implementing a <code>Trace</code> there should be special
+ * attention paid to its affect on the performance of the server. The
+ * trace is used deep within the core and any delays experienced in
+ * the trace will be reflected in the performance of the server.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.trace.TraceAnalyzer
+ */
+public interface Trace {
+
+ /**
+ * This method is used to accept an event that occurred on the socket
+ * associated with this trace. Typically the event is a symbolic
+ * description of the event such as an enum or a string.
+ *
+ * @param event this is the event that occurred on the socket
+ */
+ void trace(Object event);
+
+ /**
+ * This method is used to accept an event that occurred on the socket
+ * associated with this trace. Typically the event is a symbolic
+ * description of the event such as an enum or a string.
+ *
+ * @param event this is the event that occurred on the socket
+ * @param value provides additional information such as an exception
+ */
+ void trace(Object event, Object value);
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/main/java/org/simpleframework/transport/trace/TraceAnalyzer.java b/simple/simple-transport/src/main/java/org/simpleframework/transport/trace/TraceAnalyzer.java
new file mode 100644
index 00000000..af3550bd
--- /dev/null
+++ b/simple/simple-transport/src/main/java/org/simpleframework/transport/trace/TraceAnalyzer.java
@@ -0,0 +1,59 @@
+/*
+ * TraceAnalyzer.java October 2012
+ *
+ * Copyright (C) 2002, Niall Gallagher <niallg@users.sf.net>
+ *
+ * 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.transport.trace;
+
+import java.nio.channels.SelectableChannel;
+
+/**
+ * The <code>TraceAnalyzer</code> object represents a tracing analyzer
+ * used to monitor events on a socket. Its primary responsibilities
+ * are to create <code>Trace</code> objects that are attached to a
+ * specific socket channel. When any event occurs on that channel the
+ * trace is notified and can forward the details on for analysis.
+ * <p>
+ * An analyzer implementation must make sure that it does not affect
+ * the performance of the server. If there are delays creating a trace
+ * or within the trace itself it will have an impact on performance.
+ *
+ * @author Niall Gallagher
+ *
+ * @see org.simpleframework.transport.trace.Trace
+ */
+public interface TraceAnalyzer {
+
+ /**
+ * This method is used to attach a trace to the specified channel.
+ * Attaching a trace basically means associating events from that
+ * trace with the specified socket. It ensures that the events
+ * from a specific channel can be observed in isolation.
+ *
+ * @param channel this is the channel to associate with the trace
+ *
+ * @return this returns a trace associated with the channel
+ */
+ Trace attach(SelectableChannel channel);
+
+ /**
+ * This is used to stop the agent and clear all trace information.
+ * Stopping the agent is typically done when the server is stopped
+ * and is used to free any resources associated with the agent. If
+ * an agent does not hold information this method can be ignored.
+ */
+ void stop();
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/MockSocket.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/MockSocket.java
new file mode 100644
index 00000000..bd1b5820
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/MockSocket.java
@@ -0,0 +1,45 @@
+
+package org.simpleframework.transport;
+
+import java.nio.channels.SocketChannel;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.net.ssl.SSLEngine;
+
+import org.simpleframework.transport.trace.MockTrace;
+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-transport/src/test/java/org/simpleframework/transport/ServerBuffer.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/ServerBuffer.java
new file mode 100644
index 00000000..34cdc0c2
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/ServerBuffer.java
@@ -0,0 +1,75 @@
+package org.simpleframework.transport;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ServerSocket;
+import java.util.concurrent.CountDownLatch;
+
+public class ServerBuffer extends Thread {
+
+ private ByteArrayOutputStream buffer;
+ private ServerSocket server;
+ private CountDownLatch latch;
+
+ public ServerBuffer() throws Exception {
+ this.buffer = new ByteArrayOutputStream();
+ this.latch = new CountDownLatch(1);
+ this.server = getSocket();
+ this.start();
+ }
+
+ public ByteArrayOutputStream getBuffer(){
+ return buffer;
+ }
+
+ public void awaitClose() throws Exception {
+ latch.await();
+ }
+
+ public int getPort() {
+ return server.getLocalPort();
+ }
+
+ private ServerSocket getSocket() throws Exception {
+ // Scan the ephemeral port range
+ for(int i = 2000; i < 10000; i++) { // keep trying to grab the socket
+ try {
+ ServerSocket socket = new ServerSocket(i);
+ System.out.println("port=["+socket.getLocalPort()+"]");
+ return socket;
+ } catch(Exception e) {
+ Thread.sleep(200);
+ }
+ }
+ // Scan a second time for good measure, maybe something got freed up
+ for(int i = 2000; i < 10000; i++) { // keep trying to grab the socket
+ try {
+ ServerSocket socket = new ServerSocket(i);
+ System.out.println("port=["+socket.getLocalPort()+"]");
+ return socket;
+ } catch(Exception e) {
+ Thread.sleep(200);
+ }
+ }
+ throw new IOException("Could not create a client socket");
+ }
+
+ public void run() {
+ try {
+ java.net.Socket socket = server.accept();
+ InputStream in = socket.getInputStream();
+ int count = 0;
+
+ while((count = in.read()) != -1) {
+ buffer.write(count);
+ System.err.write(count);
+ System.err.flush();
+ }
+ } catch(Exception e) {
+ e.printStackTrace();
+ } finally {
+ latch.countDown();
+ }
+ }
+} \ No newline at end of file
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketBufferTest.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketBufferTest.java
new file mode 100644
index 00000000..a893f04c
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketBufferTest.java
@@ -0,0 +1,86 @@
+package org.simpleframework.transport;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+
+import junit.framework.TestCase;
+
+import org.simpleframework.transport.trace.MockTrace;
+import org.simpleframework.transport.trace.Trace;
+
+public class SocketBufferTest extends TestCase {
+
+
+ public void testBulkWrite() throws Exception {
+ ServerBuffer reader = new ServerBuffer();
+ SocketAddress address = new InetSocketAddress("localhost", reader.getPort());
+ SocketChannel channel = SocketChannel.open();
+ channel.configureBlocking(false); // underlying socket must be non-blocking
+ channel.connect(address);
+
+ while(!channel.finishConnect()) { // wait to finish connection
+ Thread.sleep(10);
+ };
+ Trace trace = new MockTrace();
+ SocketWrapper wrapper = new SocketWrapper(channel, trace);
+ SocketBuffer builder = new SocketBuffer(wrapper, 100, 4096);
+
+ for(int i = 0; i < 10000; i++){
+ ByteBuffer buf = ByteBuffer.wrap(("message-"+i+"\n").getBytes());
+
+ if(i > 18) {
+ System.err.println("FAIL......."+i);
+ }
+ if(!builder.write(buf)){
+ while(!builder.flush()) {
+ System.err.println("FLUSHING!!!");
+ Thread.sleep(1);
+ }
+ }
+ }
+ while(!builder.flush()) {
+ System.err.println("FLUSHING!!!");
+ }
+ builder.close();
+ reader.awaitClose();
+
+ String data = reader.getBuffer().toString();
+ String[] list = data.split("\\n");
+
+ for(int i = 0; i < 10000; i++){
+ String msg = list[i];
+ if(!msg.equals("message-"+i)) {
+ System.err.println(list[i]);
+ }
+ assertEquals("At index " + i + " value="+list[i] +" expect message-"+i, list[i], "message-"+i);
+ }
+ }
+
+ public void testSimpleWrite() throws Exception {
+ ServerBuffer reader = new ServerBuffer();
+ SocketAddress address = new InetSocketAddress("localhost", reader.getPort());
+ SocketChannel channel = SocketChannel.open();
+ channel.configureBlocking(false); // underlying socket must be non-blocking
+ channel.connect(address);
+
+ while(!channel.finishConnect()) { // wait to finish connection
+ Thread.sleep(10);
+ };
+ Trace trace = new MockTrace();
+ SocketWrapper wrapper = new SocketWrapper(channel, trace);
+ SocketBuffer builder = new SocketBuffer(wrapper, 100, 4096);
+
+ builder.write(ByteBuffer.wrap("hello there ".getBytes()));
+ builder.write(ByteBuffer.wrap("this ".getBytes()));
+ builder.write(ByteBuffer.wrap("is ".getBytes()));
+ builder.write(ByteBuffer.wrap("a ".getBytes()));
+ builder.write(ByteBuffer.wrap("test".getBytes()));
+ builder.flush();
+ builder.close();
+ reader.awaitClose();
+
+ assertEquals(reader.getBuffer().toString(), "hello there this is a test");
+ }
+}
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportPipeTest.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportPipeTest.java
new file mode 100644
index 00000000..6654f31f
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportPipeTest.java
@@ -0,0 +1,92 @@
+package org.simpleframework.transport;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import junit.framework.TestCase;
+
+import org.simpleframework.common.thread.ConcurrentExecutor;
+import org.simpleframework.transport.reactor.ExecutorReactor;
+import org.simpleframework.transport.reactor.Reactor;
+import org.simpleframework.transport.trace.MockTrace;
+import org.simpleframework.transport.trace.Trace;
+
+public class SocketTransportPipeTest extends TestCase {
+
+ private static final int ITERATIONS = 100000;
+
+ public void testPipe() throws Exception {
+ ServerSocket server = new ServerSocket(0);
+ SocketAddress address = new InetSocketAddress("localhost", server.getLocalPort());
+ SocketChannel channel = SocketChannel.open();
+ channel.configureBlocking(false); // underlying socket must be non-blocking
+ channel.connect(address);
+ while(!channel.finishConnect()) { // wait to finish connection
+ Thread.sleep(10);
+ };
+ Trace trace = new MockTrace();
+ SocketWrapper wrapper = new SocketWrapper(channel, trace);
+ Executor executor = new ConcurrentExecutor(Runnable.class);
+ Reactor reactor = new ExecutorReactor(executor);
+ SocketTransport transport = new SocketTransport(wrapper,reactor);
+ java.net.Socket socket = server.accept();
+ final InputStream read = socket.getInputStream();
+ final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ final LinkedBlockingQueue<String> sent = new LinkedBlockingQueue<String>();
+ final LinkedBlockingQueue<String> received = new LinkedBlockingQueue<String>();
+ Thread thread = new Thread(new Runnable() {
+ public void run(){
+ try {
+ byte[] token = new byte[]{'\r','\n'};
+ int pos = 0;
+ int count = 0;
+ while((count = read.read()) != -1){
+ if(count != token[pos++]){
+ pos = 0;
+ }
+ if(pos == token.length) {
+ String value = buffer.toString().trim();
+ String expect = sent.take();
+
+ if(!value.equals(expect)) {
+ throw new Exception("Out of sequence expected " + expect + " but got " + value);
+ }
+ received.offer(value);
+ buffer.reset();
+ pos = 0;
+ } else {
+ buffer.write(count);
+ System.err.write(count);
+ System.err.flush();
+ }
+
+ }
+ }catch(Exception e){
+ e.printStackTrace();
+ }
+ }
+ });
+ thread.start();
+ for(int i = 0; i < ITERATIONS; i++) {
+ String message = "message-"+i;
+ transport.write(ByteBuffer.wrap((message+"\r\n").getBytes()));
+ sent.offer(message);
+ }
+ transport.flush();
+ transport.close();
+
+ for(int i = 0; i < ITERATIONS; i++) {
+ assertEquals(received.take(), "message-"+i);
+ }
+ assertTrue(sent.isEmpty());
+ assertTrue(received.isEmpty());
+
+ }
+}
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportTest.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportTest.java
new file mode 100644
index 00000000..1e09cd03
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/SocketTransportTest.java
@@ -0,0 +1,51 @@
+package org.simpleframework.transport;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.Executor;
+
+import junit.framework.TestCase;
+
+import org.simpleframework.common.thread.ConcurrentExecutor;
+import org.simpleframework.transport.reactor.ExecutorReactor;
+import org.simpleframework.transport.reactor.Reactor;
+import org.simpleframework.transport.trace.MockTrace;
+import org.simpleframework.transport.trace.Trace;
+
+public class SocketTransportTest extends TestCase {
+
+
+ public void testBulkWrite() throws Exception {
+ ServerBuffer reader = new ServerBuffer();
+ SocketAddress address = new InetSocketAddress("localhost", reader.getPort());
+ SocketChannel channel = SocketChannel.open();
+ channel.configureBlocking(false); // underlying socket must be non-blocking
+ channel.connect(address);
+
+ while(!channel.finishConnect()) { // wait to finish connection
+ Thread.sleep(10);
+ };
+ Trace trace = new MockTrace();
+ SocketWrapper wrapper = new SocketWrapper(channel, trace);
+ Executor executor = new ConcurrentExecutor(Runnable.class);
+ Reactor reactor = new ExecutorReactor(executor);
+ SocketTransport transport = new SocketTransport(wrapper,reactor);
+ for(int i = 0; i < 10000; i++){
+ transport.write(ByteBuffer.wrap(("message-"+i+"\n").getBytes()));
+ }
+ transport.close();
+ reader.awaitClose();
+
+ String data = reader.getBuffer().toString();
+ String[] list = data.split("\\n");
+
+ for(int i = 0; i < 10000; i++){
+ if(!list[i].equals("message-"+i)) {
+ System.err.println(list[i]);
+ }
+ assertEquals("At index " + i + " value="+list[i] +" expect message-"+i, list[i], "message-"+i);
+ }
+ }
+}
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/StreamTransport.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/StreamTransport.java
new file mode 100644
index 00000000..75f999e7
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/StreamTransport.java
@@ -0,0 +1,66 @@
+package org.simpleframework.transport;
+
+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.trace.MockTrace;
+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-transport/src/test/java/org/simpleframework/transport/TransportCursorTest.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/TransportCursorTest.java
new file mode 100644
index 00000000..161115f2
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/TransportCursorTest.java
@@ -0,0 +1,83 @@
+package org.simpleframework.transport;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import junit.framework.TestCase;
+
+public class TransportCursorTest extends TestCase {
+
+ private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz";
+ private static final String SOURCE = ALPHABET + "\r\n";
+
+ public void testCursor() throws IOException {
+ byte[] data = SOURCE.getBytes("ISO-8859-1");
+ InputStream source = new ByteArrayInputStream(data);
+ Transport transport = new StreamTransport(source, System.out);
+ ByteCursor cursor = new TransportCursor(transport);
+ byte[] buffer = new byte[1024];
+
+ assertEquals(cursor.ready(), data.length);
+ assertEquals(26, cursor.read(buffer, 0, 26));
+ assertEquals(26, cursor.reset(26));
+ assertEquals(new String(buffer, 0, 26), ALPHABET);
+
+ assertEquals(cursor.ready(), data.length);
+ assertEquals(26, cursor.read(buffer, 0, 26));
+ assertEquals(26, cursor.reset(26));
+ assertEquals(new String(buffer, 0, 26), ALPHABET);
+
+ assertEquals(cursor.ready(), data.length);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(4, cursor.reset(26));
+ assertEquals(new String(buffer, 0, 4), "abcd");
+
+ assertEquals(cursor.ready(), data.length);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(4, cursor.reset(26));
+ assertEquals(new String(buffer, 0, 4), "abcd");
+
+ assertEquals(cursor.ready(), data.length);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(new String(buffer, 0, 4), "abcd");
+
+ assertEquals(cursor.ready(), data.length - 4);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(new String(buffer, 0, 4), "efgh");
+
+ assertEquals(cursor.ready(), data.length - 8);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(new String(buffer, 0, 4), "ijkl");
+
+ assertEquals(cursor.ready(), data.length - 12);
+ assertEquals(12, cursor.reset(12));
+ assertEquals(10, cursor.read(buffer, 0, 10));
+ assertEquals(new String(buffer, 0, 10), "abcdefghij");
+
+ cursor.push("1234".getBytes("ISO-8859-1"));
+ cursor.push("5678".getBytes("ISO-8859-1"));
+ cursor.push("90".getBytes("ISO-8859-1"));
+
+ assertEquals(cursor.ready(), 10);
+ assertEquals(2, cursor.read(buffer, 0, 2));
+ assertEquals(new String(buffer, 0, 2), "90");
+
+ assertEquals(cursor.ready(), 8);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(new String(buffer, 0, 4), "5678");
+
+ assertEquals(cursor.ready(), 4);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(new String(buffer, 0, 4), "1234");
+
+ assertEquals(4, cursor.reset(4));
+ assertEquals(cursor.ready(), 4);
+ assertEquals(4, cursor.read(buffer, 0, 4));
+ assertEquals(new String(buffer, 0, 4), "1234");
+
+ assertEquals(8, cursor.read(buffer, 0, 8));
+ assertEquals(new String(buffer, 0, 8), "klmnopqr");
+ }
+
+}
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/TransportTest.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/TransportTest.java
new file mode 100644
index 00000000..1a144317
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/TransportTest.java
@@ -0,0 +1,404 @@
+package org.simpleframework.transport;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicLong;
+
+import junit.framework.TestCase;
+
+import org.simpleframework.transport.reactor.ExecutorReactor;
+import org.simpleframework.transport.reactor.Reactor;
+
+/**
+ * Measure the performance of the transports to ensure that the perform
+ * well and that they send the correct sequence of bytes and that the
+ * blocks sent are in the correct order. This also performs a comparison
+ * with direct socket output streams to ensure there is a reasonable
+ * performance difference.
+ *
+ * @author Niall Gallagher
+ */
+public class TransportTest extends TestCase {
+
+ private static final int REPEAT = 1000;
+
+ public void testTransport() throws Exception {
+ testTransport(REPEAT);
+ }
+
+ public void testTransport(int repeat) throws Exception {
+ for(int i = 1; i < 7; i++) { // just do some random sizes
+ testTransport(i, 100);
+ }
+ for(int i = 4092; i < 4102; i++) {
+ testTransport(i, 100);
+ }
+ for(int i = 8190; i < 8200; i++) {
+ testTransport(i, 100);
+ }
+ for(int i = 11282; i < 11284; i++) {
+ testTransport(i, 1000);
+ }
+ for(int i = 204800; i < 204805; i++) {
+ testTransport(i, 1000);
+ }
+ testTransport(16, repeat);
+ testTransport(64, repeat);
+ testTransport(256, repeat);
+ testTransport(1024, repeat);
+ testTransport(2048, repeat);
+ testTransport(4096, repeat);
+ testTransport(4098, repeat);
+ testTransport(8192, repeat);
+ testTransport(8197, repeat);
+ }
+
+ // Test blocking transport
+ private void testTransport(int size, int repeat) throws Exception {
+ // ThreadDumper dumper = new ThreadDumper();
+ SocketConsumer consumer = new SocketConsumer(size, repeat);
+ SocketAddress address = new InetSocketAddress("localhost", consumer.getPort());
+ SocketChannel channel = SocketChannel.open();
+ channel.configureBlocking(false); // underlying socket must be non-blocking
+ channel.connect(address);
+
+ while(!channel.finishConnect()) { // wait to finish connection
+ Thread.sleep(10);
+ };
+ ExecutorService executor = Executors.newFixedThreadPool(20);
+ Reactor reactor = new ExecutorReactor(executor);
+ // Transport transport = new SocketTransport(channel, reactor, 2, 3);//XXX bug
+ MockSocket pipeline = new MockSocket(channel);
+ Transport transport = new SocketTransport(pipeline, reactor, 8192);
+ OutputStream out = new TransportOutputStream(transport);
+
+ // dumper.start();
+ testOutputStream(consumer, out, size, repeat);
+
+ out.close();
+ executor.shutdown();
+ channel.close();
+ reactor.stop();
+ // dumper.kill();
+ Thread.sleep(100);
+ }
+
+ public void s_testSocket() throws Exception {
+ s_testSocket(REPEAT);
+ }
+
+ public void s_testSocket(int repeat) throws Exception {
+ testSocket(16, repeat);
+ testSocket(64, repeat);
+ testSocket(256, repeat);
+ testSocket(1024, repeat);
+ testSocket(2048, repeat);
+ testSocket(4098, repeat);
+ testSocket(8192, repeat);
+ }
+
+ // Test blocking socket
+ private void testSocket(int size, int repeat) throws Exception {
+ // ThreadDumper dumper = new ThreadDumper();
+ SocketConsumer consumer = new SocketConsumer(size, repeat);
+ Socket socket = new Socket("localhost", consumer.getPort());
+ OutputStream out = socket.getOutputStream();
+
+ //dumper.start();
+ testOutputStream(consumer, out, size, repeat);
+
+ out.close();
+ socket.close();
+ //dumper.kill();
+ Thread.sleep(100);
+ }
+
+ private class AlpahbetIterator {
+
+ private byte[] alphabet = "abcdefghijklmnopqstuvwxyz".getBytes();
+
+ private int off;
+
+ public byte next() {
+ if(off == alphabet.length) {
+ off = 0;
+ }
+ return alphabet[off++];
+ }
+
+ public void reset() {
+ off = 0;
+ }
+ }
+
+ private void testOutputStream(SocketConsumer consumer, OutputStream out, int size, int repeat) throws Exception {
+ byte[] block = new byte[size]; // write size
+ AlpahbetIterator it = new AlpahbetIterator(); // write known data
+
+ for(int i = 1; i < block.length; i++) {
+ block[i] = it.next();
+ }
+ AtomicLong count = new AtomicLong();
+ PerformanceMonitor monitor = new PerformanceMonitor(consumer, count, out.getClass().getSimpleName(), size);
+
+ for(int i = 0; i < repeat; i++) {
+ block[0] = (byte) i; // mark the first byte in the block to be sure we get blocks in sequence
+ //System.err.println("["+i+"]"+new String(block,"ISO-8859-1"));
+ out.write(block); // manipulation of the underlying buffer is taking place when the compact is invoked, this is causing major problems as the next packet will be out of sequence
+ count.addAndGet(block.length);
+ }
+ Thread.sleep(2000); // wait for all bytes to flush through to consumer
+ monitor.kill();
+ }
+
+ private class PerformanceMonitor extends Thread {
+ private AtomicLong count;
+
+ private volatile boolean dead;
+
+ private SocketConsumer consumer;
+
+ private String name;
+
+ private int size;
+
+ public PerformanceMonitor(SocketConsumer consumer, AtomicLong count, String name, int size) {
+ this.consumer = consumer;
+ this.count = count;
+ this.name = name;
+ this.size = size;
+ this.start();
+ }
+
+ public void run() {
+ int second = 0;
+ while(!dead) {
+ try {
+ long octets = count.longValue();
+ System.out.printf("%s,%s,%s,%s,%s%n", name, size, second++, octets, consumer.getWindow());
+ Thread.sleep(1000);
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void kill() throws Exception {
+ dead = true;
+ }
+ }
+
+ private class SocketConsumer extends Thread {
+
+ private ServerSocket server;
+
+ private Window window;
+
+ private long repeat;
+
+ private long size;
+
+ public SocketConsumer(int size, int repeat) throws Exception {
+ this.window = new Window(20);
+ this.server = getSocket();
+ this.repeat = repeat;
+ this.size = size;
+ this.start();
+ }
+
+ public int getPort() {
+ return server.getLocalPort();
+ }
+
+ public String getWindow() {
+ return window.toString();
+ }
+
+ private ServerSocket getSocket() throws Exception {
+ // Scan the ephemeral port range
+ for(int i = 2000; i < 10000; i++) { // keep trying to grab the socket
+ try {
+ ServerSocket socket = new ServerSocket(i);
+ System.out.println("port=["+socket.getLocalPort()+"]");
+ return socket;
+ } catch(Exception e) {
+ Thread.sleep(200);
+ }
+ }
+ // Scan a second time for good measure, maybe something got freed up
+ for(int i = 2000; i < 10000; i++) { // keep trying to grab the socket
+ try {
+ ServerSocket socket = new ServerSocket(i);
+ System.out.println("port=["+socket.getLocalPort()+"]");
+ return socket;
+ } catch(Exception e) {
+ Thread.sleep(200);
+ }
+ }
+ throw new IOException("Could not create a client socket");
+ }
+
+ public void run() {
+ long count = 0;
+ int windowOctet = 0;
+ int expectWindowOctet = 0;
+
+ try {
+ Socket socket = server.accept();
+ InputStream in = socket.getInputStream();
+ InputStream source = new BufferedInputStream(in);
+ AlpahbetIterator it = new AlpahbetIterator();
+
+ scan: for(int i = 0; i < repeat; i++) {
+ int octet = source.read(); // check first byte in the block to make sure its correct in sequence
+
+ if(octet == -1) {
+ break scan;
+ }
+ count++; // we have read another byte
+ windowOctet = octet & 0x000000ff;
+ expectWindowOctet = i & 0x000000ff;
+
+ if((byte) octet != (byte) i) {
+ throw new Exception("Wrong sequence of blocks sent, was "
+ + (byte)octet + " should have been " + (byte)i + " count is "+count+" window is "+window+" compare "+explore(it, source, 5));
+ }
+ window.recieved(octet);
+
+ for(int j = 1, k = 0; j < size; j++, k++) {
+ octet = source.read();
+
+ if(octet == -1) {
+ break scan;
+ }
+ byte next = it.next();
+
+ if((byte) octet != next) {
+ throw new Exception("Invalid data received expected "+((byte)octet)+"("+((char)octet)+
+ ") but was "+next+"("+((char)next)+") total count is "+count+" block count is "+k+" window is expected "+
+ expectWindowOctet+"("+((char)expectWindowOctet)+")("+((byte)expectWindowOctet)+") got "+windowOctet+"("+
+ ((char)windowOctet)+")("+((byte)windowOctet)+") "+window+" compare "+explore(it, source, 5));
+ }
+ count++;
+ }
+ it.reset();
+ }
+ } catch(Throwable e) {
+ e.printStackTrace();
+ }
+ if(count != size * repeat) {
+ new Exception("Invalid number of bytes read, was " + count
+ + " should have been " + (size * repeat)).printStackTrace();
+ }
+ try {
+ // server.close();
+ }catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private String explore(AlpahbetIterator it, InputStream source, int count) throws IOException {
+ StringBuffer buf = new StringBuffer();
+ buf.append("expected (");
+ for(int i = 0; i < count; i++) {
+ buf.append( (char)it.next() );
+ }
+ buf.append(") is (");
+ for(int i = 0; i < count; i++) {
+ buf.append( (char)source.read() );
+ }
+ buf.append(")");
+ return buf.toString();
+ }
+ }
+
+
+ private static class TransportOutputStream extends OutputStream {
+
+ private Transport transport;
+
+ public TransportOutputStream(Transport transport) {
+ this.transport = transport;
+ }
+
+ public void write(int octet) throws IOException {
+ byte[] data = new byte[] { (byte) octet };
+ write(data);
+ }
+
+ public void write(byte[] data, int off, int len) throws IOException {
+ try {
+ ByteBuffer buffer = ByteBuffer.wrap(data, off, len);
+ ByteBuffer safe = buffer.asReadOnlyBuffer();
+
+ if(len > 0) {
+ transport.write(safe);
+ }
+ } catch(Exception e) {
+ e.printStackTrace();
+ throw new IOException("Write failed");
+ }
+ }
+
+ public void flush() throws IOException {
+ try {
+ transport.flush();
+ } catch(Exception e) {
+ e.printStackTrace();
+ throw new IOException("Flush failed");
+ }
+ }
+
+ public void close() throws IOException {
+ try {
+ transport.close();
+ } catch(Exception e) {
+ e.printStackTrace();
+ throw new IOException("Close failed");
+ }
+ }
+
+ }
+
+ private static class Window {
+
+ private final LinkedList<String> window;
+ private final int size;
+
+ public Window(int size) {
+ this.window = new LinkedList<String>();
+ this.size = size;
+ }
+
+ public synchronized void recieved(int sequence) {
+ window.addLast(String.valueOf(sequence));
+
+ if(window.size() > size) {
+ window.removeFirst();
+ }
+ }
+
+ public synchronized String toString() {
+ StringBuilder builder = new StringBuilder("[");
+ String delim = "";
+ for(String b : window) {
+ builder.append(delim).append(b);
+ delim=", ";
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+}
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/reactor/DistributorTest.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/reactor/DistributorTest.java
new file mode 100644
index 00000000..30326928
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/reactor/DistributorTest.java
@@ -0,0 +1,269 @@
+package org.simpleframework.transport.reactor;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.TestCase;
+
+import org.simpleframework.transport.trace.MockTrace;
+import org.simpleframework.transport.trace.Trace;
+
+
+public class DistributorTest 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 class Client extends Thread {
+
+ private CountDownLatch latch;
+ private String message;
+ private int requests;
+ private int port;
+
+ public Client(CountDownLatch latch, String message, int port, int requests) throws Exception {
+ this.message = message;
+ this.requests = requests;
+ this.port = port;
+ this.latch = latch;
+ this.start();
+ }
+
+ public void run() {
+ try {
+ latch.await();
+
+ Socket socket = new Socket("localhost", port);
+ OutputStream out = socket.getOutputStream();
+ byte[] payload = message.getBytes();
+
+ for(int i = 0; i < requests; i++){
+ out.write(payload);
+ }
+ out.close();
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public class Worker implements Operation {
+
+ private BlockingQueue<Worker> done;
+ private Reactor reactor;
+ private SocketChannel channel;
+ private ByteBuffer buffer;
+ private String payload;
+ private int accumulate;
+ private long finish;
+ private long start;
+ private int id;
+
+ public Worker(BlockingQueue<Worker> done, Reactor reactor, SocketChannel channel, String payload, int id) throws Exception {
+ this.buffer = ByteBuffer.allocate(8192);
+ this.start = System.currentTimeMillis();
+ this.finish = start + 60000;
+ this.payload = payload;
+ this.channel = channel;
+ this.reactor = reactor;
+ this.done = done;
+ this.id = id;
+ }
+
+ public Trace getTrace() {
+ return new MockTrace();
+ }
+
+ public long getExpiry(TimeUnit unit) {
+ return unit.convert(finish - System.currentTimeMillis(), MILLISECONDS);
+ }
+
+ public int getAccumulate() {
+ return accumulate;
+ }
+
+ // XXX should this be executed in a thread!!!!???? yes...
+ public void cancel() {
+ System.err.println("############################# Worker has been canceled");
+ }
+
+ public void run() {
+ try {
+ // N.B Fundamental to performance
+ buffer.clear();
+
+ if(channel.isOpen()) {
+ int count = channel.read(buffer);
+ accumulate += count;
+
+ System.err.println("Worker-"+id+" read ["+count +"] of payload sized ["+payload.length()+"] took ["+(System.currentTimeMillis() -start)+"]");
+
+ if(count != -1) {
+ reactor.process(this, SelectionKey.OP_READ);
+ } else {
+ channel.close();
+ done.offer(this);
+ System.err.println("Worker-"+id+" Channel is closed after time ["+(System.currentTimeMillis() - start)+"] and read ["+accumulate+"]");
+ }
+ } else {
+ System.err.println("Worker-"+id+" Channel is closed after time ["+(System.currentTimeMillis() - start)+"] and read ["+accumulate+"]");
+ done.offer(this);
+ }
+ }catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public SocketChannel getChannel() {
+ return channel;
+ }
+
+ }
+
+ public class Server extends Thread {
+
+ private BlockingQueue<SocketChannel> ready;
+ private CountDownLatch latch;
+ private ServerSocketChannel server;
+ private Selector selector;
+ private int port;
+
+ public Server(CountDownLatch latch, BlockingQueue<SocketChannel> ready, int port) throws Exception {
+ this.server = ServerSocketChannel.open();
+ this.selector = Selector.open();
+ this.latch = latch;
+ this.port = port;
+ this.ready = ready;
+ this.start();
+ }
+
+ private void configure() throws Exception {
+ server.socket().bind(new InetSocketAddress(port));
+ server.configureBlocking(false);
+ }
+
+ public void run() {
+ try {
+ configure();
+ execute();
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void execute() throws Exception {
+ SelectionKey serverKey = server.register(selector, SelectionKey.OP_ACCEPT);
+
+ latch.countDown();
+
+ while(true){
+ selector.select();
+ Set keys = selector.selectedKeys();
+
+ for(Iterator i = keys.iterator(); i.hasNext();){
+ SelectionKey key = (SelectionKey) i.next();
+ i.remove();
+
+ if(key != serverKey) {
+ return;
+ }
+ if(key.isAcceptable()) {
+ SocketChannel channel = server.accept();
+ channel.configureBlocking(false);
+ ready.offer(channel);
+ }
+ }
+ }
+ }
+ }
+
+ public static void main(String[] list) throws Exception {
+ new DistributorTest().testReactor();
+ }
+
+ public void testReactor() throws Exception {
+ testReactor(PAYLOAD, 200, 100, 10, 8123);
+ }
+
+ private void testReactor(String payload, int clients, int requests, int threads, int port) throws Exception {
+ BlockingQueue<Worker> done = new LinkedBlockingQueue<Worker>();
+ BlockingQueue<SocketChannel> ready = new LinkedBlockingQueue<SocketChannel>();
+ CountDownLatch latch = new CountDownLatch(1);
+ Server server = new Server(latch, ready, port);
+ Executor executor = Executors.newFixedThreadPool(10);
+ Reactor reactor = new ExecutorReactor(executor, 1);
+
+ long start = System.currentTimeMillis();
+
+ for(int i = 0; i < clients; i++) {
+ new Client(latch, payload, port, requests);
+ }
+ for(int i = 0; i < clients; i++) {
+ SocketChannel channel = ready.take();
+ Worker worker = new Worker(done, reactor, channel, payload, i);
+
+ reactor.process(worker);
+ }
+ int total = 0;
+
+ for(int i = 0; i < clients; i++) {
+ Worker worker = done.take();
+ int accumulate = worker.getAccumulate();
+ total += accumulate;
+ System.err.println("Accumulated ["+accumulate+"] of ["+(requests*payload.length())+"] closed ["+worker.getChannel().socket().isClosed()+"]");
+ }
+ System.err.println("Accumulated ["+total+"] of ["+(clients*requests*payload.length())+"]");
+ System.err.println("Total time to process ["+(clients*requests)+"] payloads from ["+clients+"] clients took ["+(System.currentTimeMillis() - start)+"]");
+ }
+
+
+
+
+}
+
+
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/trace/CompareQueueTest.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/trace/CompareQueueTest.java
new file mode 100644
index 00000000..ca2f5db6
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/trace/CompareQueueTest.java
@@ -0,0 +1,174 @@
+package org.simpleframework.transport.trace;
+
+import java.text.DecimalFormat;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+
+import junit.framework.TestCase;
+
+import org.simpleframework.common.thread.ConcurrentExecutor;
+
+public class CompareQueueTest extends TestCase {
+
+ private static final int TEST_DURATION = 10000;
+ private static final int THREAD_COUNT = 100;
+
+ private final Executor blockingReadExecutor = new ConcurrentExecutor(BlockingConsumer.class, THREAD_COUNT);
+ private final Executor concurrentReadExecutor = new ConcurrentExecutor(ConcurrentConsumer.class, THREAD_COUNT);
+ private final Executor writeExecutor = new ConcurrentExecutor(Producer.class, THREAD_COUNT);
+
+ public void testLinkedBlockingQueue() throws Exception {
+ BlockingQueue<Object> queue = new LinkedBlockingQueue<Object>();
+ AtomicBoolean active = new AtomicBoolean(true);
+ AtomicLong writeCount = new AtomicLong();
+ AtomicLong readCount = new AtomicLong();
+ CountDownLatch startLatch = new CountDownLatch(THREAD_COUNT);
+ CountDownLatch stopLatch = new CountDownLatch(THREAD_COUNT);
+ DecimalFormat format = new DecimalFormat("###,###,###");
+
+ for(int i = 0; i < THREAD_COUNT; i++) {
+ BlockingConsumer consumer = new BlockingConsumer(queue, stopLatch, active, readCount);
+ blockingReadExecutor.execute(consumer);
+ }
+ Thread.sleep(1000);
+
+ for(int i = 0; i < THREAD_COUNT; i++) {
+ Producer producer = new Producer(queue, startLatch, active, writeCount);
+ writeExecutor.execute(producer);
+ }
+ Thread.sleep(TEST_DURATION);
+ active.set(false);
+ stopLatch.await();
+
+ System.err.printf("read=%s write=%s%n", format.format(readCount.get()), format.format(writeCount.get()));
+ }
+
+ public void testConcurrentQueue() throws Exception {
+ Queue<Object> queue = new ConcurrentLinkedQueue<Object>();
+ AtomicBoolean active = new AtomicBoolean(true);
+ AtomicLong writeCount = new AtomicLong();
+ AtomicLong readCount = new AtomicLong();
+ CountDownLatch startLatch = new CountDownLatch(THREAD_COUNT);
+ CountDownLatch stopLatch = new CountDownLatch(THREAD_COUNT);
+ DecimalFormat format = new DecimalFormat("###,###,###");
+
+ for(int i = 0; i < THREAD_COUNT; i++) {
+ ConcurrentConsumer consumer = new ConcurrentConsumer(queue, stopLatch, active, readCount);
+ concurrentReadExecutor.execute(consumer);
+ }
+ Thread.sleep(1000);
+
+ for(int i = 0; i < THREAD_COUNT; i++) {
+ Producer producer = new Producer(queue, startLatch, active, writeCount);
+ writeExecutor.execute(producer);
+ }
+ Thread.sleep(TEST_DURATION);
+ active.set(false);
+ stopLatch.await();
+
+ System.err.printf("read=%s write=%s%n", format.format(readCount.get()), format.format(writeCount.get()));
+ }
+
+ private static class Producer implements Runnable {
+
+ private final Queue<Object> queue;
+ private final AtomicBoolean active;
+ private final AtomicLong count;
+ private final CountDownLatch latch;
+
+ public Producer(Queue<Object> queue, CountDownLatch latch, AtomicBoolean active, AtomicLong count) {
+ this.queue = queue;
+ this.active = active;
+ this.count = count;
+ this.latch = latch;
+ }
+
+ public void run() {
+ try {
+ latch.countDown();
+ latch.await();
+
+ while(active.get()) {
+ Long value = count.getAndIncrement();
+ queue.offer(value);
+ }
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ private static class ConcurrentConsumer implements Runnable {
+
+ private final Queue<Object> queue;
+ private final AtomicBoolean active;
+ private final AtomicLong count;
+ private final CountDownLatch latch;
+
+ public ConcurrentConsumer(Queue<Object> queue, CountDownLatch latch, AtomicBoolean active, AtomicLong count) {
+ this.queue = queue;
+ this.active = active;
+ this.count = count;
+ this.latch = latch;
+ }
+
+ public void run() {
+ try {
+ while(active.get()) {
+ Object value = queue.poll();
+ if(value != null) {
+ count.getAndIncrement();
+ } else {
+ LockSupport.parkNanos(100);
+ }
+ }
+ latch.countDown();
+ latch.await();
+ }catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private static class BlockingConsumer implements Runnable {
+
+ private final BlockingQueue<Object> queue;
+ private final AtomicBoolean active;
+ private final AtomicLong count;
+ private final CountDownLatch latch;
+
+ public BlockingConsumer(BlockingQueue<Object> queue, CountDownLatch latch, AtomicBoolean active, AtomicLong count) {
+ this.queue = queue;
+ this.active = active;
+ this.count = count;
+ this.latch = latch;
+ }
+
+ public void run() {
+ try {
+ while(active.get()) {
+ try {
+ Object value = queue.take();
+ if(value != null) {
+ count.getAndIncrement();
+ }
+ }catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ latch.countDown();
+ latch.await();
+ }catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/simple/simple-transport/src/test/java/org/simpleframework/transport/trace/MockTrace.java b/simple/simple-transport/src/test/java/org/simpleframework/transport/trace/MockTrace.java
new file mode 100644
index 00000000..583326e0
--- /dev/null
+++ b/simple/simple-transport/src/test/java/org/simpleframework/transport/trace/MockTrace.java
@@ -0,0 +1,6 @@
+package org.simpleframework.transport.trace;
+
+public class MockTrace implements Trace{
+ public void trace(Object event) {}
+ public void trace(Object event, Object value) {}
+}