* @version 0.0.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2015 Hub Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.shipsimu.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ class PackageAssembler extends BaseHubSystem implements Assembler, Registerable, Visitable { /** * Name for stacker holding raw data of multiple messages */ const STACKER_NAME_MULTIPLE_MESSAGE = 'multiple_message'; /** * Pending data */ private $pendingData = ''; /** * Private call-back methods */ private $callbacks = array(); /** * Protected constructor * * @return void */ protected function __construct () { // Call parent constructor parent::__construct(__CLASS__); } /** * Creates an instance of this class * * @param $packageInstance An instance of a Receivable class * @return $assemblerInstance An instance of an Assembler class */ public static final function createPackageAssembler (Receivable $packageInstance) { // Get new instance $assemblerInstance = new PackageAssembler(); // Set package instance here $assemblerInstance->setPackageInstance($packageInstance); // Create an instance of a raw data input stream $streamInstance = ObjectFactory::createObjectByConfiguredName('node_raw_data_input_stream_class'); // And set it $assemblerInstance->setInputStreamInstance($streamInstance); // Now get a chunk handler instance $handlerInstance = ChunkHandlerFactory::createChunkHandlerInstance(); // Set handler instance $assemblerInstance->setHandlerInstance($handlerInstance); // Get stacker instance $stackInstance = ObjectFactory::createObjectByConfiguredName('multiple_message_stacker_class'); // Initialize the only one stack $stackInstance->initStack(self::STACKER_NAME_MULTIPLE_MESSAGE); // And add it $assemblerInstance->setStackInstance($stackInstance); // Return the prepared instance return $assemblerInstance; } /** * Checks whether the input buffer (stacker to be more preceise) is empty. * * @return $isInputBufferEmpty Whether the input buffer is empty */ private function ifInputBufferIsEmpty () { // Check it $isInputBufferEmpty = $this->getPackageInstance()->getStackInstance()->isStackEmpty(NetworkPackage::STACKER_NAME_DECODED_HANDLED); // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: isInputBufferEmpty=' . intval($isInputBufferEmpty)); // Return it return $isInputBufferEmpty; } /** * Checks whether given package content is completed (start/end markers are found) * * @param $packageContent An array with two elements: 'raw_data' and 'error_code' * @return $isCompleted Whether the given package content is completed */ private function isPackageContentCompleted (array $packageContent) { // Check both $isCompleted = $this->ifStartEndMarkersSet($packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA]); // Return status return $isCompleted; } /** * Assembles the content from $packageContent. This method does only * initialize the whole process by creating a call-back which will then * itself (99.9% of all cases) "explode" the decoded data stream and add * it to a chunk assembler queue. * * If the call-back method or this would attempt to assemble the package * chunks and (maybe) re-request some chunks from the sender, this would * take to much time and therefore slow down this node again. * * @param $packageContent An array with two elements: 'raw_data' and 'error_code' * @return void * @throws UnsupportedPackageCodeHandlerException If the package code handler is not implemented */ public function chunkPackageContent (array $packageContent) { // Validate the package content array again assert( (isset($packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA])) && (isset($packageContent[BaseRawDataHandler::PACKAGE_ERROR_CODE])) ); // Construct call-back name from package error code $this->callbacks[$packageContent[BaseRawDataHandler::PACKAGE_ERROR_CODE]] = 'handlePackageBy' . self::convertToClassName($packageContent[BaseRawDataHandler::PACKAGE_ERROR_CODE]); // Abort if the call-back method is not there if (!method_exists($this, $this->callbacks[$packageContent[BaseRawDataHandler::PACKAGE_ERROR_CODE]])) { // Throw an exception throw new UnsupportedPackageCodeHandlerException(array($this, $this->callbacks[$packageContent[BaseRawDataHandler::PACKAGE_ERROR_CODE]], $packageContent), BaseListener::EXCEPTION_UNSUPPORTED_PACKAGE_CODE_HANDLER); } // END - if // Call it back call_user_func(array($this, $this->callbacks[$packageContent[BaseRawDataHandler::PACKAGE_ERROR_CODE]]), $packageContent); } /************************************************************************** * Call-back methods for above method * **************************************************************************/ /** * Call-back handler to handle unhandled package data. This method * "explodes" the string with the chunk separator from PackageFragmenter * class, does some low checks on it and feeds it into another queue for * verification and re-request for bad chunks. * * @param $packageContent An array with two elements: 'raw_data' and 'error_code' * @return void */ private function handlePackageByUnhandledPackage (array $packageContent) { // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: packageData[' . BaseRawDataHandler::PACKAGE_RAW_DATA . ']=' . $packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA]); // Check for some conditions if ((!$this->ifInputBufferIsEmpty()) || (!$this->isPackageContentCompleted($packageContent))) { // Last chunk is not valid, so wait for more $this->pendingData .= $packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA]; // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: Partial data received. Waiting for more ... ( ' . strlen($packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA]) . ' bytes)'); } else { // Debug message //* DEBUG */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ': packageContent=' . print_r($packageContent, TRUE) . ',chunks='.print_r($chunks, TRUE)); } } /** * Checks whether the assembler's pending data is empty which means it has * no pending data left for handling ... ;-) * * @return $ifPendingDataIsEmpty Whether pending data is empty */ public function isPendingDataEmpty () { // A simbple check $ifPendingDataIsEmpty = empty($this->pendingData); // Return it return $ifPendingDataIsEmpty; } /** * Checks whether the assembler has multiple messages pending * * @return $isPending Whether the assembler has multiple messages pending */ public function ifMultipleMessagesPending () { // Determine it $isPending = (!$this->getStackInstance()->isStackEmpty(self::STACKER_NAME_MULTIPLE_MESSAGE)); // Return it //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ': isPending=' . intval($isPending)); return $isPending; } /** * Handles the assembler's pending data * * @return void */ public function handlePendingData () { // Debug output //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: Going to decode ' . strlen($this->pendingData) . ' Bytes of pending data. pendingData=' . $this->pendingData); // Assert on condition assert(!$this->isPendingDataEmpty()); // No markers set? if (!$this->ifStartEndMarkersSet($this->pendingData)) { // This will cause an assertition in next call, so simply wait for more data //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: Pending data of ' . strlen($this->pendingData) . ' Bytes are incomplete, waiting for more ...'); //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: this->pendingData=' . $this->pendingData); return; } elseif (substr_count($this->pendingData, BaseRawDataHandler::STREAM_START_MARKER) > 1) { /* * Multiple messages found, so split off first message as the input * stream can only handle one message per time. */ foreach (explode(BaseRawDataHandler::STREAM_START_MARKER, $this->pendingData) as $message) { // Prepend start marker again as it is needed to decode the message. $message = BaseRawDataHandler::STREAM_START_MARKER . $message; // Push it on stack $this->getStackInstance()->pushNamed(self::STACKER_NAME_MULTIPLE_MESSAGE, $message); } // END - foreach // Clear pending data $this->clearPendingData(); // ... and exit here return; } // Init fake array $packageContent = array( BaseRawDataHandler::PACKAGE_RAW_DATA => $this->getInputStreamInstance()->streamData($this->pendingData), BaseRawDataHandler::PACKAGE_ERROR_CODE => BaseRawDataHandler::SOCKET_ERROR_UNHANDLED ); /* * Clear pending data as it has been processed and will be handled some * lines below. */ $this->clearPendingData(); // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: Last block of partial data received. A total of ' . strlen($packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA]) . ' bytes has been received.'); // Make sure last CHUNK_SEPARATOR is not there if (substr($packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA], -1, 1) == PackageFragmenter::CHUNK_SEPARATOR) { // Remove it $packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA] = substr($packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA], 0, -1); } // END - if /* * "explode" the string from 'raw_data' with chunk separator to get an * array of chunks. These chunks must then be verified by their * checksums. Also the final chunk must be handled. */ $chunks = explode(PackageFragmenter::CHUNK_SEPARATOR, $packageContent[BaseRawDataHandler::PACKAGE_RAW_DATA]); // Add all chunks because the last final chunk is found //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-ASSEMBLER[' . __METHOD__ . ':' . __LINE__ . ']: Going to add ' . count($chunks) . ' to chunk handler ...'); $this->getHandlerInstance()->addAllChunksWithFinal($chunks); } /** * Handles multiple messages. * * @return void */ public function handleMultipleMessages () { // Assert on condition assert($this->ifMultipleMessagesPending()); assert($this->isPendingDataEmpty()); // "Pop" next entry from stack and set it as new pending data $this->pendingData = $this->getStackInstance()->popNamed(self::STACKER_NAME_MULTIPLE_MESSAGE); // And handle it $this->handlePendingData(); } /** * Accepts the visitor to process the visit "request" * * @param $visitorInstance An instance of a Visitor class * @return void */ public function accept (Visitor $visitorInstance) { // Visit the assembler $visitorInstance->visitAssembler($this); } /** * Clears pending data * * @return void */ public function clearPendingData () { // Clear it $this->pendingData = ''; } } // [EOF] ?>