<?php
namespace QuickBooksOnline\API\XSD2PHP\src\com\mikebevz\xsd2php;

use QuickBooksOnline\API\Core\CoreConstants;

/**
 * Copyright 2010 Mike Bevz <myb@mikebevz.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Bind XML to PHP binding
 *
 * @author Mike Bevz <myb@mikebevz.com>
 * @version 0.0.5
 */
class Bind extends Common
{

    //protected $dom;
    //protected $namespaces;

    /**
     * __construct
     *
     * @param string  $classPrefix Prefix for generated class names
     *
     * @return void
     */
    public function __construct($classPrefix)
    {
        parent::__construct($classPrefix);
    }

    /**
     * Unmarshal XML string to corresponding PHP binding
     *
     * @param string $xml XML string to unmarshal
     *
     * @return object PHP binding object
     */
    public function unmarshal($xml)
    {
        // Get all namespaces
        $this->dom = new \DOMDocument();
        if ($this->debug) {
            $this->dom->formatOutput = true;
        }
        $this->dom->loadXML($xml);

        $this->namespaces =  $this->getDocNamespaces($this->dom);

        // Find targetNamespace

        $xpath = new \DOMXPath($this->dom);
        $query = ".";
        $root = $xpath->query($query);
        $ns = $code = "";
        foreach ($root as $rt) {
            list($ns, $name) = $this->parseQName($rt->nodeName, true);
        }

        $className = $this->urnToPhpName($ns)."\\".$this->classPrefix.$name;

        if (!class_exists($className, CoreConstants::USE_AUTOLOADER)) {
            throw new \RuntimeException('Class '.$className. ' is not found. Make sure it was included');
        }

        $binding = new $className();

        return $this->bindXml($xml, $binding);

        // Instantiate class
        // Assign corresponding nodes to properties/classes
        // return PHP binding
    }

    /**
     * Bind XML file to model
     * @param string $xml   XML source
     * @param object $model PHP Model to bind to
     *
     * @return object PHP model
     */
    public function bindXml($xml, $model)
    {

        //print_r($xml."\n ".get_class($model));

        $this->dom = new \DOMDocument();
        $this->dom->loadXML($xml);

        $refl  = new \ReflectionClass(get_class($model));

        // In the following code we ignore any elements
        // of IntuitAnyType since the contents are unknown and cannot be deserialized by
        // the algorithm below.
        // Schema for IntuitAnyType uses element wildcard xs:any,
        // <xs:complexType name="IntuitAnyType">
        //     <xs:annotation>
        //         <xs:documentation>
        //             Product: ALL
        //             Description: Definition of IntuitAnyType to add and extend new elements to the existing entities.
        //         </xs:documentation>
        //     </xs:annotation>
        //     <xs:sequence>
        //         <xs:any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
        //     </xs:sequence>
        // </xs:complexType>
        // Adding namesapce now for
        //echo "Class name is:" . $refl->name . "\n";
        if ($refl->name != 'QuickBooksOnline\API\Data\IPPIntuitAnyType') {
            $xpath = new \DOMXPath($this->dom);

            $query = "child::*";
            $childs = $xpath->query($query);

            foreach ($childs as $child) {
                list($ns, $name) = $this->parseQName($child->nodeName, true);
                //$className = $this->urnToPhpName($ns)."\\".$name;
                try {
                    $propertyDocs = $refl->getProperty($name)->getDocComment();
                } catch (\ReflectionException $e) {
                    throw new \RuntimeException($e->getMessage() . ". Class " . get_class($model));
                }
                $docs = $this->parseDocComments($propertyDocs);
                $className = $docs['var'];

                if ($this->hasChild($child)) {
                    if (property_exists($model, $name)) {
                        $doc = new \DOMDocument();
                        $doc->appendChild($doc->importNode($child, true));

                        if ($this->overrideAsSingleNamespace) {
                            $nsLastName = array_reverse(explode('\\', $className));
                            $className = $nsLastName[0];
                        }
                        /**
                         *Vish Singh: Solution to Bug # IPP-4748
                         *Add Check if the class Exists. @Hao
                         */
                         if (class_exists($className, CoreConstants::USE_AUTOLOADER)) {
                             //Do nothing
                         } elseif (class_exists(CoreConstants::NAMEPSACE_DATA_PREFIX . $className)) {
                             $className = CoreConstants::NAMEPSACE_DATA_PREFIX . $className;
                         } else {
                             throw new \Exception("Can't find corresponding CLASS for className" . $className . " in Bind.php");
                         }

                        if ($model->{$name}) { //If this property has been seen before then turn this into an array if it's not already
                            if (!is_array($model->{$name})) { //If this property is not already an array, then turn it into one
                                $model->{$name} = array($model->{$name}, $this->bindXml($doc->saveXml(), new $className()));
                            } else { //If it is already an array then, just append to this array.
                                array_push($model->{$name}, $this->bindXml($doc->saveXml(), new $className()));
                            }
                        } //If this property hasn't been seen before then just treat it normally
                        else {
                            $model->{$name} = $this->bindXml($doc->saveXml(), new $className());
                        }
                        // end of fix
                    } else {
                        throw new \RuntimeException('Class ' . get_class($model) . ' does not have property ' . $name);
                    }
                } else {
                    if (!property_exists($model, $name)) {
                        throw new \RuntimeException("Model " . get_class($model) . " does not have property " . $name);
                    }
                    if (!class_exists($className)) {
                        //print_r($className."\n");
                        $propertyDocs = $refl->getProperty($name)->getDocComment();
                        $docs = $this->parseDocComments($propertyDocs);
                        $type = $docs['xmlType'];
                        //print_r("Type: ". $type."\n");
                        if ($type == 'attribute') {
                            $model->{$name} = $child->nodeValue;
                        } elseif ($type == 'element') {
                            $model->{$name} = $child->nodeValue;
                        } // elseif ($type == 'value') {
                        //    $model->value = $child->nodeValue;
                        //}
                        else {
                            throw new \RuntimeException('Class ' . $className . ' does not exist');
                        }
                    } else {
                        //$name = $child->nodeName;
                        $cModel = new $className();
                        $cModel->value = $child->nodeValue;
                        $model->{$name} = $cModel;
                    }
                }
            }
        }

        return $model;
    }

    private function hasChild($node)
    {
        if ($node->hasChildNodes()) {
            foreach ($node->childNodes as $c) {
                if ($c->nodeType === XML_ELEMENT_NODE) {
                    return true;
                }
            }
        }
        return false;
    }

    public function bindXmlRec($node, $dom, $modelName)
    {
        $model = '';

        if (class_exists($modelName)) {
            $model = new $modelName();
        } else {
            throw new \RuntimeException('Class '.$modelName.' does not exist');
        }

        $refl = new \ReflectionClass($model);

        $xpath = new \DOMXPath($dom);
        $query = "child::*";
        $childs = $xpath->query($query, $node);
        foreach ($childs as $child) {
            if ($this->hasChild($child)) {
                if (property_exists($model, $child->nodeName)) {
                    $model->{$child->nodeName} = $this->bindXMlRec($child, $dom, $child->nodeName);
                }
            } else {
                if (!property_exists($model, $child->nodeName)) {
                    throw new \RuntimeException("Model does not have property ".$child->nodeName);
                }
                if (!class_exists($child->nodeName)) {
                    $propertyDocs = $refl->getProperty($child->nodeName)->getDocComment();
                    $docs = $this->parseDocComments($propertyDocs);
                    $type = $docs['xmlType'];
                    if ($type == 'attribute') {
                        $model->{$child->nodeName} = $child->nodeValue;
                    } else {
                        //print_r($propertyType);
                     throw new \RuntimeException('Class '.$child->nodeName.' does not exist');
                    }
                } else {
                    $name = $child->nodeName;
                    $cModel = new $name();
                    $cModel->value = $child->nodeValue;
                    $model->{$child->nodeName} = $cModel;
                }
            }
        }

        return $model;
    }
}


<?php

namespace QuickBooksOnline\API\Core;

use QuickBooksOnline\API\Core\Http\Serialization\XmlObjectSerializer;
use QuickBooksOnline\API\Core\Http\Serialization\JsonObjectSerializer;
use QuickBooksOnline\API\Core\Http\Serialization\IEntitySerializer;
use QuickBooksOnline\API\Core\Http\Serialization\SerializationFormat;
use QuickBooksOnline\API\Core\Http\Compression\CompressionFormat;
use QuickBooksOnline\API\Core\Http\Compression\GZipCompressor;
use QuickBooksOnline\API\Core\Http\Compression\DeflateCompressor;
use QuickBooksOnline\API\Core\Http\Compression\CompressorBase;
use QuickBooksOnline\API\Diagnostics\TraceLevel;
use QuickBooksOnline\API\Exception\IdsException;
use QuickBooksOnline\API\Diagnostics\LogRequestsToDisk;

/**
 * Helper class.
 */
 class CoreHelper
 {
     /**
     * Gets the serializer mechanism using the service context and the depending on the request and response.
     * @param ServiceContext serviceContext The service context object.
     * @param bool isRequest Specifies whether to return serializer mechanism for request or response.
     * @return IEntitySerializer The Serializer mechanism.
     */
    public static function GetSerializer($serviceContext, $isRequest)
    {
        $serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "GetSerializer");

        $serializer = null;
        if ($isRequest) {
            switch ($serviceContext->IppConfiguration->Message->Request->SerializationFormat) {
                case SerializationFormat::Xml:
                              $serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "GetSerializer(Request): Xml");
                    $serializer = new XmlObjectSerializer();
                    break;
                case SerializationFormat::Json:
                              $serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "GetSerializer(Request): JSON");
                    $serializer = new JsonObjectSerializer();
                    break;
                case SerializationFormat::Custom:
                    // TODO: check whtether this is possible
                    // $this->serializer = $serviceContext->IppConfiguration->Message->Request->CustomSerializer;
                              $serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "GetSerializer(Request): Custom");
                    break;
            }
        } else {
            switch ($serviceContext->IppConfiguration->Message->Response->SerializationFormat) {
                case SerializationFormat::Xml:
                              $serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "GetSerializer(Response): XML");
                    $serializer = new XmlObjectSerializer();
                    break;
                case SerializationFormat::Json:
                              $serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "GetSerializer(Response): JSON");
                    $serializer = new JsonObjectSerializer();
                    break;
                case SerializationFormat::Custom:
                    // TODO: check whtether this is possible
                    // $this->serializer = $serviceContext->IppConfiguration->Message->Response->CustomSerializer;
                              $serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "GetSerializer(Response): Custom");
                    break;
            }
        }

        return $serializer;
    }

    /**
     * This function is deprecated use simplexml_load_string() instead.
     * @param string response The response string
     * @throws BadFunctionCallException
     * @return SimpleXMLElement The SimpleXMLElement object.
     * @deprecated since version v2.1
     */
    public static function ParseResponseIntoXml()
    {
        throw new \BadFunctionCallException(__METHOD__ . " has been removed.");
    }

    /**
     * TO DO: Rewrite Late - Hao
     * Checks whether the reponse is null or empty and throws communication exception.
     * @param string response The response from the query service.
     */
    public static function CheckNullResponseAndThrowException($response)
    {
        if (!$response) {
            $messageToWrite = 'Response Null or Empty';
            $backTrace =  debug_backtrace();
            $callerFileName = $backTrace[0]['file'];
            $callerFileLineNumber = $backTrace[0]['line'];
            $callerFunctionName = $backTrace[0]['function'];
            $logMessage = implode(" - ", array(date('Y-m-d H:i:s'),
                                               $callerFileName,
                                               $callerFileLineNumber,
                                               $callerFunctionName,
                                               $messageToWrite));
            throw new IdsException($logMessage);
        }
    }

    /**
     * Gets the compression mechanism using the service context and the depending on the request and response.
     * @param ServiceContext serviceContext The service context object.
     * @param bool isRequest Specifies whether to return compression mechanism for reqeust or response.
     * @return CompressorBase The Compression mechanism.
     */
    public static function GetCompressor($serviceContext, $isRequest)
    {
        $compressor = null;
        if ($isRequest) {
            switch ($serviceContext->IppConfiguration->Message->Request->CompressionFormat) {
                case CompressionFormat::GZip:
                    $compressor = new GZipCompressor();
                    break;
                case CompressionFormat::Deflate:
                    $compressor = new DeflateCompressor();
                    break;
            }
        } else {
            switch ($serviceContext->IppConfiguration->Message->Response->CompressionFormat) {
                case CompressionFormat::GZip:
                    $compressor = new GZipCompressor();
                    break;
                case CompressionFormat::Deflate:
                    $compressor = new DeflateCompressor();
                    break;
            }
        }

        return $compressor;
    }

    /**
     * Gets the Request Response Logging mechanism.
     * @param ServiceContext serviceContext The serivce context object.
     * @return LogRequestsToDisk Returns value which specifies the request response logging mechanism.
     */
    public static function GetRequestLogging($serviceContext)
    {
        $requestLogger = null;
        try {
            if (isset($serviceContext->IppConfiguration) &&
                isset($serviceContext->IppConfiguration->Logger) &&
                isset($serviceContext->IppConfiguration->Logger->RequestLog) &&
                isset($serviceContext->IppConfiguration->Logger->RequestLog->EnableRequestResponseLogging) &&
                isset($serviceContext->IppConfiguration->Logger->RequestLog->ServiceRequestLoggingLocation)) {
                $requestLogger = new LogRequestsToDisk(
                    $serviceContext->IppConfiguration->Logger->RequestLog->EnableRequestResponseLogging,
                    $serviceContext->IppConfiguration->Logger->RequestLog->ServiceRequestLoggingLocation);
            } else {
                $requestLogger = new LogRequestsToDisk(false, null);
            }
        } catch (\Exception $e) {
            $requestLogger = new LogRequestsToDisk(false, null);
        }

        return $requestLogger;
    }
 }


<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Soap
 * @subpackage Wsdl
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Composite.php 20096 2010-01-06 02:05:09Z bkarwin $
 */

/**
 * @see Zend_Soap_Wsdl_Strategy_Interface
 */
require_once "Zend/Soap/Wsdl/Strategy/Interface.php";

/**
 * Zend_Soap_Wsdl_Strategy_Composite
 *
 * @category   Zend
 * @package    Zend_Soap
 * @subpackage Wsdl
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Soap_Wsdl_Strategy_Composite implements Zend_Soap_Wsdl_Strategy_Interface
{
    /**
     * Typemap of Complex Type => Strategy pairs.
     *
     * @var array
     */
    protected $_typeMap = array();

    /**
     * Default Strategy of this composite
     *
     * @var string|Zend_Soap_Wsdl_Strategy_Interface
     */
    protected $_defaultStrategy;

    /**
     * Context WSDL file that this composite serves
     *
     * @var Zend_Soap_Wsdl|null
     */
    protected $_context;

    /**
     * Construct Composite WSDL Strategy.
     *
     * @throws Zend_Soap_Wsdl_Exception
     * @param array $typeMap
     * @param string|Zend_Soap_Wsdl_Strategy_Interface $defaultStrategy
     */
    public function __construct(array $typeMap=array(), $defaultStrategy="Zend_Soap_Wsdl_Strategy_DefaultComplexType")
    {
        foreach ($typeMap as $type => $strategy) {
            $this->connectTypeToStrategy($type, $strategy);
        }
        $this->_defaultStrategy = $defaultStrategy;
    }

    /**
     * Connect a complex type to a given strategy.
     *
     * @throws Zend_Soap_Wsdl_Exception
     * @param  string $type
     * @param  string|Zend_Soap_Wsdl_Strategy_Interface $strategy
     * @return Zend_Soap_Wsdl_Strategy_Composite
     */
    public function connectTypeToStrategy($type, $strategy)
    {
        if (!is_string($type)) {
            /**
             * @see Zend_Soap_Wsdl_Exception
             */
            require_once "Zend/Soap/Wsdl/Exception.php";
            throw new Zend_Soap_Wsdl_Exception("Invalid type given to Composite Type Map.");
        }
        $this->_typeMap[$type] = $strategy;
        return $this;
    }

    /**
     * Return default strategy of this composite
     *
     * @throws Zend_Soap_Wsdl_Exception
     * @param  string $type
     * @return Zend_Soap_Wsdl_Strategy_Interface
     */
    public function getDefaultStrategy()
    {
        $strategy = $this->_defaultStrategy;
        if (is_string($strategy) && class_exists($strategy)) {
            $strategy = new $strategy;
        }
        if (!($strategy instanceof Zend_Soap_Wsdl_Strategy_Interface)) {
            /**
             * @see Zend_Soap_Wsdl_Exception
             */
            require_once "Zend/Soap/Wsdl/Exception.php";
            throw new Zend_Soap_Wsdl_Exception(
                "Default Strategy for Complex Types is not a valid strategy object."
            );
        }
        $this->_defaultStrategy = $strategy;
        return $strategy;
    }

    /**
     * Return specific strategy or the default strategy of this type.
     *
     * @throws Zend_Soap_Wsdl_Exception
     * @param  string $type
     * @return Zend_Soap_Wsdl_Strategy_Interface
     */
    public function getStrategyOfType($type)
    {
        if (isset($this->_typeMap[$type])) {
            $strategy = $this->_typeMap[$type];

            if (is_string($strategy) && class_exists($strategy)) {
                $strategy = new $strategy();
            }

            if (!($strategy instanceof Zend_Soap_Wsdl_Strategy_Interface)) {
                /**
                 * @see Zend_Soap_Wsdl_Exception
                 */
                require_once "Zend/Soap/Wsdl/Exception.php";
                throw new Zend_Soap_Wsdl_Exception(
                    "Strategy for Complex Type '".$type."' is not a valid strategy object."
                );
            }
            $this->_typeMap[$type] = $strategy;
        } else {
            $strategy = $this->getDefaultStrategy();
        }
        return $strategy;
    }

    /**
     * Method accepts the current WSDL context file.
     *
     * @param Zend_Soap_Wsdl $context
     */
    public function setContext(Zend_Soap_Wsdl $context)
    {
        $this->_context = $context;
        return $this;
    }

    /**
     * Create a complex type based on a strategy
     *
     * @throws Zend_Soap_Wsdl_Exception
     * @param  string $type
     * @return string XSD type
     */
    public function addComplexType($type)
    {
        if (!($this->_context instanceof Zend_Soap_Wsdl)) {
            /**
             * @see Zend_Soap_Wsdl_Exception
             */
            require_once "Zend/Soap/Wsdl/Exception.php";
            throw new Zend_Soap_Wsdl_Exception(
                "Cannot add complex type '".$type."', no context is set for this composite strategy."
            );
        }

        $strategy = $this->getStrategyOfType($type);
        $strategy->setContext($this->_context);
        return $strategy->addComplexType($type);
    }
}


<?php
namespace QuickBooksOnline\API\Utility;

/*Build in class for PHP */
use ReflectionClass;
use ReflectionProperty;
use InvalidArgumentException;
use UnexpectedValueException;
use RuntimeException;
use QuickBooksOnline\API\Core\Http\Serialization\AbstractEntity;
use QuickBooksOnline\API\Core\Http\Serialization\ObjectEntity;

//THis is a incomplete Class. Complete this Class late for Converting Response to JSON Deserialization
//-Hao Feb.7th.2017
/**
 * Converts stdClass object into domain object
 *
 * @author amatiushkin
 */

class DomainEntityBuilder
{
    /**
     * Contains original class name or type of converted object
     * @var string
     */
    private $originalClassName = null;

    /**
     * List of values which new entity must have
     * @var array
     */
    private $definedValues = null;

    /**
     * Reflection of class
     * @var ReflectionClass
     */
    private $reflection = null;

    /**
     * Array of ReflectionProperties instances for the specified class
     * @var array
     */
    private $properties = null;

    /**
     * Array of AbstractEntity instances, which contains complete meta data
     * @var array
     */
    private $model = null;

    /**
     * Requires type or class of target instance
     * @param string $name
     */
    public function __construct($name)
    {
        $this->originalClassName = $name;
    }

    /**
     * Create reflection based on class name
     * @throws InvalidArgumentException
     */
    private function makeReflection()
    {
        if (!class_exists($this->getOriginalClassName())) {
            throw new InvalidArgumentException('Class name ' . $this->getOriginalClassName() . ' not exists');
        }

        $this->reflection = new ReflectionClass($this->getOriginalClassName());
    }

    /**
     * Returns reflection object for target class
     * @return ReflectionClass
     */
    private function getReflection()
    {
        return $this->reflection;
    }

    /**
     * Collects properties of specified class
     */
    private function populateProperties()
    {
        $this->properties = $this->getReflection()->getProperties();
        $this->populatePropertyComments();
    }

    /**
     * Collect comments for defined properties. It will be used to extract meta data
     * @return mixed
     * @throws UnexpectedValueException
     */
    private function populatePropertyComments()
    {
        if (is_null($this->properties)) {
            throw new UnexpectedValueException('Properties are expected here');
        }

        if (!is_array($this->properties)) {
            throw new UnexpectedValueException('Properties should be provided as array');
        }
        // if nothing to do here
        if (is_array($this->properties) && empty($this->properties)) {
            return null;
        }

        $extractor = new MetadataExtractor();
        $this->model = $extractor->processComments($this->properties);
    }

    /**
     * Returns target class name or type of new instance
     * @return string
     */
    public function getOriginalClassName()
    {
        return $this->originalClassName;
    }

    /**
     * Adds properties and their values, which will be copied into new instance
     * @param array $values
     */
    public function usePropertyValues($values)
    {
        $this->definedValues = $values;
    }

    /**
     * Creates new instance with values
     * @return mixed returns domain model instance
     */
    public function createInstance()
    {
        $this->makeReflection();
        $this->populateProperties();
        return $this->forgeInstance($this->getRootInstance());
    }

    /**
     * Creates new instance of target class with specified values
     * It also adjusts any nested type based on suplied metadata (@var annotation)
     * @return mixed returns domain model instance
     */
    private function forgeInstance($instance)
    {
        $reflection = new ReflectionClass($instance);
        foreach ($reflection->getProperties() as $key => $property) {
            if (!$property instanceof ReflectionProperty) {
                continue;
            }
            $entity = $this->getEntityFromModel($key, $property->getName());
            $value = $property->getValue($instance);

            if (is_array($value)) {
                $this->processPropertyValues($instance, $property, $entity, $value);
            } else {
                $this->processPropertyValue($instance, $property, $entity, $value);
            }
        }
        return $instance;
    }

    private function getRootInstance()
    {
        return new $this->originalClassName($this->definedValues);
    }

    /**
     * Converts object values into domain model entities.
     * This function is array-compatible version of @see processPropertyValue
     * @param mixed $instance - contains empty of almost ready domain model entity
     * @param ReflectionProperty $property
     * @param AbstractEntity $model
     * @param array stdClass $values
     */
    private function processPropertyValues($instance, $property, $model, $values)
    {
        $changed = false;

        foreach ($values as &$value) {
            if (!$this->isMorhing($model, $value)) {
                continue;
            }
            $newType = $model->getType();
            //$value = new $newType( (array) $value );
            $value = static::create($newType, (array) $value);
            $changed = true;
        }
        if ($changed) {
            $property->setValue($instance, $values);
        }
    }

    /**
     * Creates instance of specified type and returns it as result.
     *
     * @param string $type
     * @param array $values
     * @return mixed
     */
    public static function create($type, $values)
    {
        $i = new static($type);
        $i->usePropertyValues($values);
        return $i->createInstance();
    }

    /**
     * Returns true if value can be converted into domain model instance
     * @param AbstractEntity $entity
     * @param \stdClass $value
     * @return boolean
     */
    private function isMorhing($entity, $value)
    {
        if (!$entity instanceof ObjectEntity) {
            return false;
        }
       //String, numeric are fine
       if (!is_object($value)) {
           return false;
       }
       // if object has same type already (it's wierd, but ok)
       if (get_class($value) === $entity->getType()) {
           return false;
       }
       // we expect stdClass here only
       if (!$value instanceof \stdClass) {
           return false;
       }
        return true;
    }

    /**
     * Converts value into domain model object
     * @param mixed $instance - contains empty of almost ready domain model entity
     * @param ReflectionProperty $property
     * @param AbstractEntity $model
     * @param \stdClass $value
     */
    private function processPropertyValue($instance, $property, $model, $value)
    {
        if ($this->isMorhing($model, $value)) {
            $newType = $model->getType();
            $new = static::create($newType, (array) $value);
            $property->setValue($instance, $new);
        }
    }

    /**
     * Returns metadata description of property from model
     *
     * @param string $index
     * @param string $propertyName
     * @return AbstractEntity
     * @throws RuntimeException
     */
    private function getEntityFromModel($index, $propertyName)
    {
        $entity = $this->model[$index];
        if ($entity->getName() === $propertyName) {
            return $entity;
        }
        // TODO Do we need to implement search by name in model?
        throw new RuntimeException("Unexpected $propertyName by the given index.");
    }
}


<?php
/*******************************************************************************
 * Copyright (c) 2017 Intuit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
namespace QuickBooksOnline\API\DataService;

use QuickBooksOnline\API\Core\CoreHelper;
use QuickBooksOnline\API\Core\Http\Serialization\IEntitySerializer;
use QuickBooksOnline\API\Core\HttpClients\FaultHandler;
use QuickBooksOnline\API\Core\HttpClients\RestHandler;
use QuickBooksOnline\API\Core\ServiceContext;
use QuickBooksOnline\API\Core\CoreConstants;
use QuickBooksOnline\API\Core\HttpClients\SyncRestHandler;
use QuickBooksOnline\API\Core\HttpClients\RequestParameters;
use QuickBooksOnline\API\Core\Http\Serialization\JsonObjectSerializer;
use QuickBooksOnline\API\Core\Http\Serialization\SerializationFormat;
use QuickBooksOnline\API\Data\IPPAttachable;
use QuickBooksOnline\API\Data\IPPIntuitEntity;
use QuickBooksOnline\API\Data\IPPTaxService;
use QuickBooksOnline\API\Data\IPPid;
use QuickBooksOnline\API\Exception\IdsException;
use QuickBooksOnline\API\Exception\ServiceException;
use QuickBooksOnline\API\Exception\IdsExceptionManager;
use QuickBooksOnline\API\Exception\SdkException;
use QuickBooksOnline\API\Diagnostics\TraceLevel;
use QuickBooksOnline\API\Diagnostics\ContentWriter;
use QuickBooksOnline\API\XSD2PHP\src\com\mikebevz\xsd2php\Php2Xml;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2LoginHelper;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
use QuickBooksOnline\API\Core\HttpClients\ClientFactory;
use QuickBooksOnline\API\Data\IPPCompanyInfo;
use QuickBooksOnline\API\Data\IPPPreferences;

/**
 * Class DataServicd
 *
 * This file contains DataService performs CRUD operations on IPP REST APIs.
 */
class DataService
{

    /**
     * The METHOD UPDATE.
     * @var String
     */
    const UPDATE = 'update';

    /**
     * The METHOD FINDBYID.
     * @var String
     */
    const FINDBYID = 'FindById';

    /**
     * The METHOD ADD.
     * @var String
     */
    const ADD = 'Add';

    /**
     * The METHOD Delete.
     * @var String
     */
    const DELETE = 'Delete';

    /**
     * The METHOD VOID.
     * @var String
     */
    const VOID = 'Void';

    /**
     * The METHOD UPLOAD.
     * @var String
     */
    const UPLOAD = 'upload';

    /**
     * The METHOD SENDEMAIL.
     * @var String
     */
    const SENDEMAIL = 'SendEmail';

    /**
     * The Service context object.
     * @var ServiceContext The service Context of the request
     */
    private $serviceContext;

    /**
     * Rest Request Handler for actually sending the request
     * @var SyncRestHandler
     */
    private $restHandler;

    /**
     * Serializer needs to be used fore responce object
     * @var IEntitySerializer
     */
    private $responseSerializer;

    /**
     * Serializer needs to be used for request object
     * @var IEntitySerializer
     */
    private $requestSerializer;

    /**
     * If true, indicates a desire to echo verbose output
     * @var bool
     */
    private $verbose;

    /**
     * If not false, the request from last dataService did not return 2xx
     * @var FaultHandler
     */
    private $lastError = false;

    /**
     * The OAuth 2 Login helper for get RefreshToken
     * @var OAuth2LoginHelper
     */
    private $OAuth2LoginHelper;

    /**
     * A boolean value to decide if excetion will be thrown on non-200 request
     * @var Boolean
     */
    private $throwExceptionOnError = false;

    /**
     * The client to be used for HTTP request. You can choose either defaukt(cURL) or GuzzleHttpClient if that is available
     * @var String
     */
    private $clientName = CoreConstants::CLIENT_CURL;

    /**
     * Initializes a new instance of the DataService class. The old way to construct the dataService. Used by PHP SDK < 3.0.0
     *
     * @param ServiceContext $serviceContext      IPP Service Context
     * @throws SdkException
     */
    public function __construct($serviceContext)
    {
        if (null == $serviceContext || !is_object($serviceContext)) {
            throw new SdkException('Undefined ServiceContext. DataService constructor has NULL or Non_Object ServiceContext as Constructor');
        }
        $this->updateServiceContextSettingsForOthers($serviceContext);
    }

    /**
     * Set the corresponding settings for the dataService based on ServiceContext
     * @var ServiceContext $serviceContext        The service Context for this DataService
     */
    public function updateServiceContextSettingsForOthers($serviceContext)
    {
        $this->setupServiceContext($serviceContext);
        $this->setupSerializers();
        $this->useMinorVersion();
        $this->setupRestHandler($serviceContext);
    }

    /**
     * Set or Update the ServiceContext of this DataService.
     *
     * @var ServiceContext $serviceContext The new ServiceContext passed by.
     * @return $this
     */
    private function setupServiceContext($serviceContext)
    {
        $this->serviceContext = $serviceContext;
        return $this;
    }

    /**
     * Return the ServiceContext of this DataService
     *
     * @return ServiceContext
     * @throws \Exception ServiceContext is NULL.
     */
    public function getServiceContext()
    {
        if (isset($this->serviceContext)) {
            return $this->serviceContext;
        } else {
            throw new SdkException("Trying to Return an Empty Service Context.");
        }
    }

    /**
     * Set the SyncRest Handler for the DataService. If the client Name changed, the underlying Client that SyncRestHandler used will also changed.
     *
     * @var ServiceContext $serviceContext         The service Context for this DataService
     * @return $this
     *
     */
    protected function setupRestHandler($serviceContext)
    {
       if(isset($serviceContext)){
          $client = ClientFactory::createClient($this->getClientName());
          $this->restHandler = new SyncRestHandler($serviceContext, $client);
       }else{
          throw new SdkException("Can not set the Rest Client based on null ServiceContext.");
       }
       return $this;
    }

    /**
     * Return the current Client Name used by DataService
     * @return String clientName
     */
    public function getClientName(){
        return $this->clientName;
    }
    /**
     * PHP SDK currently only support XML for Object Serialization and Deserialization, except for Report Service
     *
     * @return $this
     */
    public function useXml()
    {
        $serviceContext = $this->getServiceContext();
        $serviceContext->useXml();
        $this->updateServiceContextSettingsForOthers($serviceContext);
        return $this;
    }

    /**
     * PHP SDK currently only support XML for Object Serialization and Deserialization, except for Report Service
     *
     * @return $this
     */
    public function useJson()
    {
        $serviceContext = $this->getServiceContext();
        $serviceContext->useJson();
        $this->updateServiceContextSettingsForOthers($serviceContext);
        return $this;
    }

    /**
     * Set a new directory for request and response log
     *
     * @param String $new_log_location     The directory path for storing request and response log
     *
     * @return $this
     */
    public function setLogLocation($new_log_location)
    {
        $restHandler = $this->restHandler;
        $loggerUsedByRestHandler = $restHandler->getRequestLogger();
        $loggerUsedByRestHandler->setLogDirectory($new_log_location);
        return $this;
    }

    /**
     * Set a new Minor Version
     *
     * @param String $newMinorVersion     The new minor version that passed
     *
     * @return $this
     */
    public function setMinorVersion($newMinorVersion)
    {
        $serviceContext = $this->getServiceContext();
        $serviceContext->setMinorVersion($newMinorVersion);
        $this->updateServiceContextSettingsForOthers($serviceContext);
        return $this;
    }

    /**
     * Disable the logging function
     *
     * @return $this
     */
    public function disableLog()
    {
        $restHandler = $this->restHandler;
        $loggerUsedByRestHandler = $restHandler->getRequestLogger();
        $loggerUsedByRestHandler->setLogStatus(false);
        return $this;
    }

    /**
     * Enable the logging function
     *
     * @return $this
     */
    public function enableLog()
    {
        $restHandler = $this->restHandler;
        $loggerUsedByRestHandler = $restHandler->getRequestLogger();
        $loggerUsedByRestHandler->setLogStatus(true);
        return $this;
    }

    /**
     * Choose if want to throw exception when there is an non-200 http status code returned.
     * @param Boolean $bool                 Turn on exception throwing error or not
     */
    public function throwExceptionOnError($bool){
        $this->throwExceptionOnError = $bool;
        return $this;
    }

    /**
     * Return the settings for thrown exception on non-200 error code
     * @return Boolean   Thrown Exception on Error or not
     */
    public function isThrownExceptionOnError(){
        return $this->throwExceptionOnError;
    }

    /**
     * Return the client Name used by this DataSerivce
     * @return String the Client Name. It can be curl or GuzzleHttpClient
     * @deprecated since version 5.0.4
     * @see $this->getClientName()
     */
    public function getClinetName(){
       return $this->getClientName();
    }

    /**
     * The client Name can be either 'curl', 'guzzle', or 'guzzlehttp'.
     *
     * @param String $clientName              The client Name used by the service
     *
     * @return $this
     */
    public function setClientName($clientName){
       $this->clientName = $clientName;
       $serviceContext = $this->getServiceContext();
       $this->setupRestHandler($serviceContext);
       return $this;
    }

    /**
     * New Static function for static Reading from Config or Passing Array
     * The config needs to include
     *
     * @param $settings
     * @return DataService
     * @throws SdkException
     * @throws SdkException
     */
    public static function Configure($settings)
    {
        if (isset($settings)) {
            if (is_array($settings)) {
                $ServiceContext = ServiceContext::ConfigureFromPassedArray($settings);
                if (!isset($ServiceContext)) {
                    throw new SdkException('Construct ServiceContext from OAuthSettigs failed.');
                }
                $DataServiceInstance = new DataService($ServiceContext);

            } elseif (is_string($settings)) {
                $ServiceContext = ServiceContext::ConfigureFromLocalFile($settings);
                if (!isset($ServiceContext)) {
                    throw new SdkException('Construct ServiceContext from File failed.');
                }
                $DataServiceInstance = new DataService($ServiceContext);

            }

            if($ServiceContext->IppConfiguration->OAuthMode == CoreConstants::OAUTH2)
            {
                $oauth2Config = $ServiceContext->IppConfiguration->Security;
                if($oauth2Config instanceof OAuth2AccessToken){
                    $DataServiceInstance->configureOAuth2LoginHelper($oauth2Config, $settings);
                }else{
                    throw new SdkException("SDK Error. OAuth mode is not OAuth 2.");
                }
            }

            return $DataServiceInstance;

        } else {
            throw new SdkException("Passed Null to Configure method. It expects either a file path for the config file or an array containing OAuth settings and BaseURL.");
        }
    }

    /**
     * After the ServiceContext is complete, also set the LoginHelper based on the ServiceContext.
     * @param OAuth2AccessToken $oauth2Conifg      OAuth 2 Token related information
     * @param Array $settings                      The array that include the redirectURL, scope, state information
     */
    private function configureOAuth2LoginHelper($oauth2Conifg, $settings)
    {
          $refreshToken = CoreConstants::getRefreshTokenFromArray($settings);
          if(isset($refreshToken)){
               //Login helper for refresh token API call
               $this->OAuth2LoginHelper = new OAuth2LoginHelper(null,
                                                                null,
                                                                null,
                                                                null,
                                                                null,
                                                                $this->getServiceContext());
          }else{
                $redirectURL = CoreConstants::getRedirectURL($settings);
                $scope = array_key_exists('scope', $settings) ? $settings['scope'] : null;
                $state = array_key_exists('state', $settings) ? $settings['state'] : null;
                $this->OAuth2LoginHelper = new OAuth2LoginHelper($oauth2Conifg->getClientID(),
                                                                 $oauth2Conifg->getClientSecret(),
                                                                 $redirectURL,
                                                                 $scope,
                                                                 $state);
          }
    }

    /**
     * Return the OAuth 2 Login Helper. The OAuth 2 Login helper can be used to generate OAuth code, get refresh Token, etc.
     * @return $OAuth2LoginHelper      A helper to get OAuth 2 related values.
     */
    public function getOAuth2LoginHelper()
    {
        return $this->OAuth2LoginHelper;
    }

    /**
     * Update the OAuth 2 Token that will be used for API calls later.
     *
     * @param OAuth2AccessToken $newOAuth2AccessToken   The OAuth 2 Access Token that will be used later.
     *
     * @return $this
     */
    public function updateOAuth2Token($newOAuth2AccessToken)
    {
        try{
          $this->serviceContext->updateOAuth2Token($newOAuth2AccessToken);
          $realmID = $newOAuth2AccessToken->getRealmID();
          $this->serviceContext->realmId = $realmID;
          $this->setupRestHandler($this->serviceContext);
        } catch (SdkException $e){
            $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Error, "Encountered an error while updating OAuth2Token." . $e->getMessage());
            $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Error, "Stack Trace: " . $e->getTraceAsString());
        }
        return $this;
    }

    /**
     * Get the error from last request
     *
     * @return FaultHandler lastError
     */
    public function getLastError()
    {
        return $this->lastError;
    }

    /**
     * Setups serializers objects for responces and requests based on service context
     */
    public function setupSerializers()
    {
        $this->responseSerializer = CoreHelper::GetSerializer($this->serviceContext, false);
        $this->requestSerializer = CoreHelper::GetSerializer($this->serviceContext, true);
    }

    private function useMinorVersion()
    {
        $version = $this->serviceContext->IppConfiguration->minorVersion;
        if (is_numeric($version) && !empty($version)) {
            $this->serviceContext->minorVersion = $version;
        }
        return $this;
    }

    /**
     * @return string
     */
    public function getMinorVersion()
    {
        return $this->serviceContext->minorVersion;
    }

    /**
     * Force json serializers for request and response
     */
    public function forceJsonSerializers()
    {
        $this->requestSerializer = new JsonObjectSerializer();
        $this->responseSerializer = new JsonObjectSerializer();
    }

    /**
     * Returns serializer for responce objects
     * @return IEntitySerializer
     */
    protected function getResponseSerializer()
    {
        return $this->responseSerializer;
    }

    /**
     * Returns serializer for request objects
     * @return IEntitySerializer
     */
    protected function getRequestSerializer()
    {
        return $this->requestSerializer;
    }

    /**
     * Marshall a POPO object to XML, presumably for inclusion on an IPP v3 API call
     *
     *
     *
     * @param object $phpObj inbound POPO object
     * @return string XML output derived from POPO object
     * @deprecated since version ?
     */
    private function getXmlFromObj($phpObj)
    {
        if (!$phpObj) {
            $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Error, "getXmlFromObj NULL arg.");

            return false;
        }

        $php2xml = new Php2Xml(CoreConstants::PHP_CLASS_PREFIX);
        $php2xml->overrideAsSingleNamespace = 'http://schema.intuit.com/finance/v3';

        try {
            return $php2xml->getXml($phpObj);
        } catch (\Exception $e) {
            $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Error, "Encountered an error parsing Object to XML." . $e->getMessage());
            $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Error, "Stack Trace: " . $e->getTraceAsString());

            return false;
        }
    }

    /**
     * Decorate an IPP v3 Entity name (like 'Class') to be a POPO class name (like 'IPPClass')
     *
     * @param string Intuit Entity name
     * @return string POPO class name
     */
    private static function decorateIntuitEntityToPhpClassName($intuitEntityName)
    {
        $className = CoreConstants::PHP_CLASS_PREFIX . $intuitEntityName;
        $className = trim($className);

        return $className;
    }

    //Since we add the namespace, this one needs to be changed as well.
    private static function getEntityResourceName($entity)
    {
        return strtolower(self::cleanPhpClassNameToIntuitEntityName(get_class($entity)));
    }

    /**
     * Clean a POPO class name (like 'IPPClass') to be an IPP v3 Entity name (like 'Class')
     *
     * @param string $phpClassName POPO class name
     * @return string|null Intuit Entity name
     */
    private static function cleanPhpClassNameToIntuitEntityName($phpClassName)
    {
        $phpClassName = self::removeNameSpaceFromPhpClassName($phpClassName);
        if (0 == strpos($phpClassName, CoreConstants::PHP_CLASS_PREFIX)) {
            return substr($phpClassName, strlen(CoreConstants::PHP_CLASS_PREFIX));
        }

        return null;
    }

    /**
     * Remove the Namespace from a php class name
     *
     * @param string $phpClassName QuickBooksOnline\API\Data\...
     * @return string ipp...
     */
    private static function removeNameSpaceFromPhpClassName($phpClassName)
    {
        $lists = explode('\\', $phpClassName);
        $ippEntityName = end($lists);

        return $ippEntityName;
    }

    /**
     *  Using the @entity and @uri to generate Request.
     *  Response will parsed. It will store any Error Code in 3xx to 5xx level.
     *
     * @param IPPIntuitEntity $entity
     * @param string $uri
     * @param string $httpsPostBody
     * @param string $CALLINGMETHOD
     * @param string|null $boundaryString
     * @param string|null $email
     * @return null|string
     */
    private function sendRequestParseResponseBodyAndHandleHttpError($entity, $uri, $httpsPostBody, $CALLINGMETHOD, $boundaryString = null, $email = null)
    {
        switch ($CALLINGMETHOD) {
            case DataService::DELETE:
            case DataService::ADD:
            case DataService::VOID:
            case DataService::UPDATE:
                $requestParameters = $this->initPostRequest($entity, $uri);
                break;
            case DataService::FINDBYID:
                if ($this->serviceContext->IppConfiguration->Message->Request->SerializationFormat == SerializationFormat::Json) {
                    $requestParameters = new RequestParameters($uri, 'GET', CoreConstants::CONTENTTYPE_APPLICATIONJSON, null);
                } else {
                    $requestParameters = new RequestParameters($uri, 'GET', CoreConstants::CONTENTTYPE_APPLICATIONXML, null);
                }
                break;
            case DataService::UPLOAD:
                if (!isset($boundaryString)) {
                    throw new \Exception("Upload Image has unset value: boundaryString.");
                }
                // Creates request parameters
                $requestParameters = $this->getPostRequestParameters($uri, "multipart/form-data; boundary={$boundaryString}");
                break;
            case DataService::SENDEMAIL:
                $requestParameters = $this->getPostRequestParameters($uri . (is_null($email) ? '' : '?sendTo=' . urlencode($email)), CoreConstants::CONTENTTYPE_OCTETSTREAM);
                break;
        }
        $restRequestHandler = $this->getRestHandler();
        list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, $httpsPostBody, null, $this->isThrownExceptionOnError());
        $faultHandler = $restRequestHandler->getFaultHandler();
        if ($faultHandler) {
            $this->lastError = $faultHandler;
            return null;
        } else {
            $this->lastError = false;
            if (strcmp($CALLINGMETHOD, DataService::ADD) == 0) {
                $responseBody = $this->fixTaxServicePayload($entity, $responseBody);
            }
            try {
                $parsedResponseBody = $this->getResponseSerializer()->Deserialize($responseBody, true);
            } catch (\Exception $e) {
                return new Excepiton("Exception in deserialize ResponseBody.");
            }

            $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Finished Executing Method " . $CALLINGMETHOD);

            return $parsedResponseBody;
        }
    }

    /**
     * Updates an entity under the specified realm. The realm must be set in the context.
     *
     * @param IPPIntuitEntity $entity Entity to Update.
     * @return IPPIntuitEntity Returns an updated version of the entity with updated identifier and sync token.
     * @throws IdsException
     */
    public function Update($entity)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method: Update.");

        // Validate parameter
        if (!$entity) {
            $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Error, "Argument Null Exception");
            throw new IdsException('Argument Null Exception');
        }
        $this->verifyOperationAccess($entity, __FUNCTION__);

        $httpsPostBody = $this->executeObjectSerializer($entity, $urlResource);

        // Builds resource Uri
        // Handle some special cases
        if ((strtolower('preferences') == strtolower($urlResource)) &&
            (CoreConstants::IntuitServicesTypeQBO == $this->serviceContext->serviceType)
        ) {
            // URL format for *QBO* prefs request is different than URL format for *QBD* prefs request
            $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource));
        }
        //We no longer support QBD on PHP SDK The code is removed.
        /*else if ((strtolower('company') == strtolower($urlResource)) &&
                (CoreConstants::IntuitServicesTypeQBO == $this->serviceContext->serviceType)) {
            // URL format for *QBD* companyinfo request is different than URL format for *QBO* companyinfo request
            $urlResource = 'companyInfo';
            $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource . '?operation=update'));
        }*/ else {
            // Normal case
            $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource . '?operation=update'));
        }

        // Send Request and return response
        return $this->sendRequestParseResponseBodyAndHandleHttpError($entity, $uri, $httpsPostBody, DataService::UPDATE);
    }

    /**
     * The Read request. You can either pass an object that contains the Id that you want to read, or
     * pass the Entity Name and the Id.
     * Before v4.0.0, it supports the read of CompanyInfo and Preferences.
     * After v4.0.0, it DOES NOT support read of CompanyInfo or Preferences. Please use getCompanyInfo() or getCompanyPreferences() method instead.
     * Only use this one to do READ request with ID.
     *
     * Developer has two ways to call the GET request. Take Invoice for an example:
     * 1) FindById($invoice);
     * or
     * 2) FindById("invoice", 1);
     *
     * @param object|String $entity Entity to Find, or the String Name of the Entity
     * @return IPPIntuitEntity Returns an entity of specified Id.
     * @throws IdsException
     */
    public function FindById($entity, $Id = null)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method FindById.");
        if(is_object($entity)){
           $httpsPostBody = $this->executeObjectSerializer($entity, $urlResource);
           // Validate parameter
           if (!$entity || !$entity->Id) {
              $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Error, "Argument Null Exception");
              throw new IdsException('Argument Null Exception when calling FindById for Endpoint:' . get_class($entity));
          }
          $this->verifyOperationAccess($entity, __FUNCTION__);
          $entityId = $this->getIDString($entity->Id);
          // Normal case
          $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource, $entityId));
          // Send request
          return $this->sendRequestParseResponseBodyAndHandleHttpError($entity, $uri, null, DataService::FINDBYID);
        }else if(is_string($entity) && isset($Id)){
          $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, strtolower($entity), $Id));
          $requestParameters = new RequestParameters($uri, 'GET', CoreConstants::CONTENTTYPE_APPLICATIONXML, null);
          $restRequestHandler = $this->getRestHandler();
          list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, null, null, $this->isThrownExceptionOnError());
          $faultHandler = $restRequestHandler->getFaultHandler();
          //$faultHandler now is true or false
          if ($faultHandler) {
              $this->lastError = $faultHandler;
              return null;
          } else {
              //clean the error
              $this->lastError = false;
              $parsedResponseBody = $this->getResponseSerializer()->Deserialize($responseBody, true);
              return $parsedResponseBody;
          }
        }
    }

    /**
     * Creates an entity under the specified realm. The realm must be set in the context.
     *
     * @param IPPIntuitEntity $entity Entity to Create.
     * @return IPPIntuitEntity Returns the created version of the entity.
     * @throws IdsException
     */
    public function Add($entity)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method Add.");

        // Validate parameter
        if (!$entity) {
            $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Error, "Argument Null Exception");
            throw new IdsException('Argument Null Exception');
        }
        $this->verifyOperationAccess($entity, __FUNCTION__);
        if ($this->isJsonOnly($entity)) {
            $this->forceJsonSerializers();
        }
        $httpsPostBody = $this->executeObjectSerializer($entity, $urlResource);

        // Builds resource Uri
        $resourceURI = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource));

        $uri = $this->handleTaxService($entity, $resourceURI);

        // Send request
        return $this->sendRequestParseResponseBodyAndHandleHttpError($entity, $uri, $httpsPostBody, DataService::ADD);
    }

    /**
     * Deletes an entity under the specified realm. The realm must be set in the context.
     *
     * @param IPPIntuitEntity $entity Entity to Delete.
     * @return null|string
     * @throws IdsException
     */
    public function Delete($entity)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method Delete.");

        // Validate parameter
        if (!$entity) {
            $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Error, "Argument Null Exception");
            throw new IdsException('Argument Null Exception');
        }
        $this->verifyOperationAccess($entity, __FUNCTION__);

        // Builds resource Uri
        $httpsPostBody = $this->executeObjectSerializer($entity, $urlResource);
        $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource . '?operation=delete'));

        // Creates request
        return $this->sendRequestParseResponseBodyAndHandleHttpError($entity, $uri, $httpsPostBody, DataService::DELETE);
    }

    /**
     * Voids an entity under the specified realm. The realm must be set in the context.
     *
     * @param IPPIntuitEntity $entity Entity to Void.
     * @return null|string
     * @throws IdsException
     */
    public function Void($entity)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method Void.");

        // Validate parameter
        if (!$entity) {
            $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Error, "Argument Null Exception");
            throw new IdsException('Argument Null Exception');
        }
        $this->verifyOperationAccess($entity, __FUNCTION__);

        // Builds resource Uri
        $httpsPostBody = $this->executeObjectSerializer($entity, $urlResource);
        $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource . '?operation=void'));

        // Creates request
        return $this->sendRequestParseResponseBodyAndHandleHttpError($entity, $uri, $httpsPostBody, DataService::VOID);
    }

    /**
     * Uploads an attachment to an Entity on QuickBooks Online. For security reason, text file is not supported for uploading.
     *
     * @param string $bits                        Encoded Base64 bytes for the attachment
     * @param string $fileName                    Filename to use for this file
     * @param string $mimeType                    MIME type to send in the HTTP Headers
     * @param IPPAttachable $objAttachable        Entity including the attachment, it can be invoice, bill, etc
     * @return array                              Returns an array of entities fulfilling the query.
     * @throws IdsException
     */
    public function Upload($imgBits, $fileName, $mimeType, $objAttachable)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method Upload.");

        // Validate parameter
        if (!$imgBits || !$mimeType || !$fileName) {
            $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Error, "Argument Null Exception");
            throw new IdsException('Argument Null Exception');
        }
        // Builds resource Uri
        $urlResource = "upload";
        $uri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, $urlResource));

        $boundaryString = md5(time());

        $MetaData = $this->executeObjectSerializer($objAttachable, $urlResource);

        $desiredIdentifier = '0';
        $newline = "\r\n";
        $dataMultipart = '';
        $dataMultipart .= '--' . $boundaryString . $newline;
        $dataMultipart .= "Content-Disposition: form-data; name=\"file_metadata_{$desiredIdentifier}\"" . $newline;
        $dataMultipart .= "Content-Type: " . CoreConstants::CONTENTTYPE_APPLICATIONXML . '; charset=UTF-8' . $newline;
        $dataMultipart .= 'Content-Transfer-Encoding: 8bit' . $newline . $newline;
        $dataMultipart .= $MetaData;
        $dataMultipart .= '--' . $boundaryString . $newline;
        $dataMultipart .= "Content-Disposition: form-data; name=\"file_content_{$desiredIdentifier}\"; filename=\"{$fileName}\"" . $newline;
        $dataMultipart .= "Content-Type: {$mimeType}" . $newline;
        $dataMultipart .= 'Content-Transfer-Encoding: base64' . $newline . $newline;
        $dataMultipart .= chunk_split(base64_encode($imgBits)) . $newline;
        $dataMultipart .= "--" . $boundaryString . "--" . $newline . $newline; // finish with two eol's!!

        return $this->sendRequestParseResponseBodyAndHandleHttpError(null, $uri, $dataMultipart, DataService::UPLOAD, $boundaryString);
    }

    /**
     * Returns PDF for entities which can be downloaded as PDF
     * @param IPPIntuitEntity $entity
     * @param Directory a writable directory for the PDF to be saved.
     * @return boolean
     * @throws IdsException, SdkException
     *
     */
    public function DownloadPDF($entity, $dir=null)
    {
        $this->validateEntityId($entity);
        $this->verifyOperationAccess($entity, __FUNCTION__);

        //Find the ID
        $entityID =  $this->getIDString($entity->Id);
        $uri = implode(CoreConstants::SLASH_CHAR, array('company',
                $this->serviceContext->realmId,
                self::getEntityResourceName($entity),
                $entityID,
                CoreConstants::getType(CoreConstants::CONTENTTYPE_APPLICATIONPDF)));
        $requestParameters = $this->getGetRequestParameters($uri, CoreConstants::CONTENTTYPE_APPLICATIONPDF);
        $restRequestHandler = $this->getRestHandler();

        list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, null, null, $this->isThrownExceptionOnError());
        $faultHandler = $restRequestHandler->getFaultHandler();
        if ($faultHandler) {
            $this->lastError = $faultHandler;
            //Add allow for through exception if users set it up
            return null;
        } else {
            $this->lastError = false;
            return $this->processDownloadedContent(new ContentWriter($responseBody), $responseCode, $this->getExportFileNameForPDF($entity, "pdf"), $dir);
        }
    }

    /**
     * Sends entity by email for entities that have this operation
     *
     * @param IPPIntuitEntity $entity
     * @param string|null $email
     * @return boolean
     * @throws IdsException, SdkException
     *
     */
    public function SendEmail($entity, $email = null)
    {
        $this->validateEntityId($entity);
        $this->verifyOperationAccess($entity, __FUNCTION__);

        $entityId=$this->getIDString($entity->Id);
        $uri = implode(CoreConstants::SLASH_CHAR, array('company',
                $this->serviceContext->realmId,
                self::getEntityResourceName($entity),
                $entityId,
                'send'));

        if (is_null($email)) {
            $this->logInfo("Entity " . get_class($entity) . " with id=" . $entityId . " is using default email");
        } else {
            $this->logInfo("Entity " . get_class($entity) . " with id=" . $entityId . " is using $email");
            if (!$this->verifyEmailAddress($email)) {
                $this->logError("Valid email is expected, but received $email");
                throw new SdkException("Valid email is expected, but received $email");
            }
        }

        return $this->sendRequestParseResponseBodyAndHandleHttpError($entity, $uri, null, DataService::SENDEMAIL, null, $email);
    }

    /**
     * Retrieves specified entities based passed page number and page size and query
     *
     * @param string $query Query to issue
     * @param int $startPosition Starting page number
     * @param int $maxResults Page size
     * @return array Returns an array of entities fulfilling the query. If the response is Empty, it will return NULL
     */
    public function Query($query, $startPosition = null, $maxResults = null)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method Query.");

        if ('QBO' == $this->serviceContext->serviceType) {
            $httpsContentType = CoreConstants::CONTENTTYPE_APPLICATIONTEXT;
        } else {
            $httpsContentType = CoreConstants::CONTENTTYPE_TEXTPLAIN;
        }

        $httpsUri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, 'query'));
        $httpsPostBody = $this->appendPaginationInfo($query, $startPosition, $maxResults);

        $requestParameters = $this->getPostRequestParameters($httpsUri, $httpsContentType);
        $restRequestHandler = $this->getRestHandler();
        list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, $httpsPostBody, null, $this->isThrownExceptionOnError());
        $faultHandler = $restRequestHandler->getFaultHandler();
        if ($faultHandler) {
            $this->lastError = $faultHandler;
            return null;
        } else {
            $this->lastError = false;
            $parsedResponseBody = null;
            try {
                $responseXmlObj = simplexml_load_string($responseBody);
                if ($responseXmlObj && $responseXmlObj->QueryResponse) {
                    $tmpXML = $responseXmlObj->QueryResponse->asXML();
                }
                $parsedResponseBody = $this->responseSerializer->Deserialize($tmpXML, false);
                $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Info, $parsedResponseBody);

            } catch (\Exception $e) {
                throw new \Exception("Exception appears in converting Response to XML.");
            }

            return $parsedResponseBody;
        }
    }

    /**
     * Append the Pagination Data to the Query string if it is not appended
     * @param Integer StartPostion
     * @param Integer MaxResults
     * @return String The query string
     */
    private function appendPaginationInfo($query, $startPosition, $maxResults){
       $query = trim($query);
       if(isset($startPosition) && !empty($startPosition)){
           if(stripos($query, "STARTPOSITION") === false){
              if(stripos($query, "MAXRESULTS") !== false){
                //In MaxResult is defined,we don't set startPosition
              }else{
                $query = $query . " " . "STARTPOSITION " . $startPosition;
              }
           }else{
             //Ignore the startPosition if it is already used on the query
           }
       }

       if(isset($maxResults) && !empty($maxResults)){
           if(stripos($query, "MAXRESULTS") === false){
                $query = $query . " " . "MAXRESULTS " . $maxResults;
           }else{
             //Ignore the maxResults if it is already used on the query
           }
       }

       return $query;

    }

    /**
     * Retrieves all entities by name
     *
     * @param string $entityName
     * @param int $pageNumber
     * @param int $pageSize
     * @return array Returns an array of entities of specified type.
     */
    public function FindAll($entityName, $pageNumber = 0, $pageSize = 500)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, "Called Method FindAll.");

        $phpClassName = DataService::decorateIntuitEntityToPhpClassName($entityName);

        // Handle some special cases
        if (strtolower('company') == strtolower($entityName)) {
            $entityName = 'CompanyInfo';
        }

        if ('QBO' == $this->serviceContext->serviceType) {
            $httpsContentType = CoreConstants::CONTENTTYPE_APPLICATIONTEXT;
        } else {
            $httpsContentType = CoreConstants::CONTENTTYPE_TEXTPLAIN;
        }

        $httpsUri = implode(CoreConstants::SLASH_CHAR, array('company', $this->serviceContext->realmId, 'query'));
        $httpsPostBody = "select * from $entityName startPosition $pageNumber maxResults $pageSize";

        $requestParameters = $this->getPostRequestParameters($httpsUri, $httpsContentType);
        $restRequestHandler = $this->getRestHandler();
        list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, $httpsPostBody, null, $this->isThrownExceptionOnError());
        $faultHandler = $restRequestHandler->getFaultHandler();
        if ($faultHandler) {
            $this->lastError = $faultHandler;
            return null;
        } else {
            $this->lastError = false;
            $parsedResponseBody = null;
            try {
                $responseXmlObj = simplexml_load_string($responseBody);
                if ($responseXmlObj && $responseXmlObj->QueryResponse) {
                    $parsedResponseBody = $this->responseSerializer->Deserialize($responseXmlObj->QueryResponse->asXML(), false);
                }
            } catch (\Exception $e) {
                throw new \Exception("Exception appears in converting Response to XML.");
            }

            return $parsedResponseBody;
        }
    }

    /**
     * Returns List of entities changed after certain time.
     *
     * @param array entityList List of entity.
     * @param long changedSince DateTime of timespan after which entities were changed.
     * @return IntuitCDCResponse Returns an IntuitCDCResponse.
     */
    public function CDC($entityList, $changedSince)
    {
        $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Info, "Called Method CDC.");

        // Validate parameter
        if (count($entityList) <= 0) {
            $exception = new IdsException('ParameterNotNullMessage');
            $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Error, "ParameterNotNullMessage");
            IdsExceptionManager::HandleException($exception);
        }

        $entityString = implode(",", $entityList);

        $query = null;
        $uri = null;

        if(is_string($changedSince)){
           $formattedChangedSince = trim($changedSince);
        }else{
            $formattedChangedSince = date("Y-m-d\TH:i:s", $this->verifyChangedSince($changedSince));
        }

        $query = "entities=" . $entityString . "&changedSince=" . $formattedChangedSince;
        $uri = "company/{1}/cdc?{2}";
        //$uri = str_replace("{0}", CoreConstants::VERSION, $uri);
        $uri = str_replace("{1}", $this->serviceContext->realmId, $uri);
        $uri = str_replace("{2}", $query, $uri);

        // Creates request parameters
        $requestParameters = $this->getGetRequestParameters($uri, CoreConstants::CONTENTTYPE_APPLICATIONXML);
        $restRequestHandler = $this->getRestHandler();

        list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, null, null, $this->isThrownExceptionOnError());
        $faultHandler = $restRequestHandler->getFaultHandler();
        if ($faultHandler) {
            $this->lastError = $faultHandler;
            return null;
        } else {
            $this->lastError = false;
            $returnValue = new IntuitCDCResponse();
            try {
                $xmlObj = simplexml_load_string($responseBody);
                $responseArray = $xmlObj->CDCResponse->QueryResponse;
                if(sizeof($responseArray) != sizeof($entityList)){
                    throw new ServiceException("The number of Entities requested on CDC does not match the number of Response.");
                }

                for($i = 0; $i < sizeof($responseArray); $i++){
                    $currentResponse = $responseArray[$i];
                    $currentEntityName = $entityList[$i];
                    $entities = $this->responseSerializer->Deserialize($currentResponse->asXML(), false);
                    $entityName = $currentEntityName;
                    //If we find the actual name, update it.
                    foreach ($currentResponse->children() as $currentResponseChild) {
                        $entityName = (string)$currentResponseChild->getName();
                        break;
                    }
                    $returnValue->entities[$entityName] = $entities;
                }
            } catch (\Exception $e) {
                IdsExceptionManager::HandleException($e);
            }

            $this->serviceContext->IppConfiguration->Logger->CustomLogger->Log(TraceLevel::Info, "Finished Executing Method CDC.");
            return $returnValue;
        }
    }

    /**
     * Returns an entity under the specified realm. The realm must be set in the context.
     *
     * @param object $entity Entity to Find
     * @return IPPIntuitEntity Returns an entity of specified Id.
     */
    public function Retrieve($entity)
    {
        $this->verifyOperationAccess($entity, __FUNCTION__);

        return $this->FindById($entity);
    }

    /**
     * Serializes oblect into specified format
     * @param IPPIntuitEntity $entity
     * @param String $urlResource
     * @return object
     */
    protected function executeObjectSerializer($entity, &$urlResource)
    {
        //
        $result = $this->getRequestSerializer()->Serialize($entity);
        $urlResource = $this->getRequestSerializer()->getResourceURL();

        return $result;
    }

    /**
     * Returns post request, depends on configuration and entity rule "jsonOnly"
     * @param IPPIntuitEntity $entity
     * @param string $uri
     * @return RequestParameters
     */
    protected function initPostRequest($entity, $uri)
    {
        return $this->isJsonOnly($entity)
            ? $this->getPostJsonRequest($uri)
            : $this->getPostRequest($uri);
    }

    /**
     * Returns content type depends from serialization format
     * @return string
     */
    private function getContentType()
    {
        return ($this->getSerializationFormat() == SerializationFormat::Json)
            ? CoreConstants::CONTENTTYPE_APPLICATIONJSON
            : CoreConstants::CONTENTTYPE_APPLICATIONXML;
    }

    /**
     * Returns post request with pre-defined POST method and JSON serialization
     * @param string $uri
     * @return RequestParameters
     */
    private function getPostJsonRequest($uri)
    {
        return $this->getPostRequestParameters($uri, CoreConstants::CONTENTTYPE_APPLICATIONJSON);
    }

    /**
     * Return true if specified entity can work with JSON only serialization
     * @param IPPIntuitEntity $entity
     * @return boolean
     */
    private function isJsonOnly($entity)
    {
        return $this->isAllowed($entity, "jsonOnly");
    }

    /**
     * Returns pre-defined request with POST method and content type from settings
     * @param string $uri
     * @return RequestParameters
     */
    private function getPostRequest($uri)
    {
        return $this->getPostRequestParameters($uri, $this->getContentType());
    }

    /**
     * Returns pre-defined request with GET method
     * @param string $uri
     * @param string $type
     * @return RequestParameters
     */
    private function getGetRequestParameters($uri, $type)
    {
        return $this->getRequestParameters($uri, 'GET', $type);
    }

    /**
     * Returns pre-defined request with POST method
     * @param string $uri
     * @param string $type
     * @return RequestParameters
     */
    private function getPostRequestParameters($uri, $type)
    {
        return $this->getRequestParameters($uri, 'POST', $type);
    }

    /**
     * Wraps and returns RequestParameters object
     * @param string $uri
     * @param string $method
     * @param string $type
     * @param string $apiName
     * @return RequestParameters
     */
    protected function getRequestParameters($uri, $method, $type, $apiName = null)
    {
        return new RequestParameters($uri, $method, $type, $apiName);
    }

    /**
     * Returns current serialization format
     *
     * @return string
     */
    protected function getSerializationFormat()
    {
        return $this->serviceContext->IppConfiguration->Message->Request->SerializationFormat;
    }

    /**
     * Handles unusual URL mapping for TaxService
     * @param IPPIntuitEntity $entity
     * @param string $uri
     * @return string
     */
    private function handleTaxService($entity, $uri)
    {
        if ($this->isTaxServiceSafe($entity)) {
            return $uri . '/taxcode';
        }

        return $uri;
    }

    /**
     * Verifies that entity has TaxService type
     * update the TaxService with namespace added
     * If this class is not available on include_path or wab't loaded the method will return false
     *
     * @param IPPTaxService $entity
     * @return bool
     */
    private function isTaxServiceSafe($entity)
    {
        $IPPTaxServiceClassWIthNameSpace = "QuickBooksOnline\\API\\Data\\IPPTaxService";

        return class_exists($IPPTaxServiceClassWIthNameSpace) && ($entity instanceof $IPPTaxServiceClassWIthNameSpace);
    }

    /**
     * Methods provides workaround to successfully process TaxService response
     * @param $entity
     * @param $content
     * @return string
     */
    private function fixTaxServicePayload($entity, $content)
    {
        if ($this->isTaxServiceSafe($entity)) {
            //get first "line" to make sure we don't have TaxService in response
            $sample = substr(trim($content), 0, 20);
            $taxServiceName = self::cleanPhpClassNameToIntuitEntityName(get_class($entity));
            if (false === strpos($sample, $taxServiceName)) {
                //last attempt to verify content before
                if (0 === strpos($sample, '{"TaxCode":')) {
                    return "{\"$taxServiceName\":$content}";
                }
            }
        }

        return $content;
    }

    /**
     * Returns an downloaded entity under the specified realm. The realm must be set in the context.
     *
     * @param object $entity Entity to Find
     * @return string IPPIntuitEntity Returns an entity of specified Id.
     * @deprecated The download for QuickBooksOnline is only supporting download PDF for Invoice and SalesReceipt. Other download function are not defined now.
     */
    public function Download($entity)
    {
        return $this->FindById($entity);
    }

    /**
     * Verifies string as email agains RFC 822
     * @param string $email
     * @return type
     */
    public function verifyEmailAddress($email)
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL);
    }

    /**
     * Returns PDF filename based on entity type and id
     * @param IPPIntuitEntity $entity
     * @param string $ext
     * @return string
     */
    public function getExportFileNameForPDF($entity, $ext, $usetimestamp = true)
    {
        //TODO: add timestamp or GUID
        $this->validateEntityId($entity);

        return self::getEntityResourceName($entity) . "_" . $this->getIDString($entity->Id) . ($usetimestamp ? "_" . time() : "") . ".$ext";
    }

    /**
     * Returns the Sync Resthandler associated with the dataserivce
     * @return SyncRestHandler          The SyncRestHandler with the DataService
     */
    protected function getRestHandler()
    {
        if(isset($this->restHandler)){
             return $this->restHandler;
        }else{
             throw new SdkException("The SyncRest handler associated with the DataService is not set.");
        }
    }

    /**
     * Saves exported (e.g. check DownloadPDF) entity into file according to strategy in settings
     * @param ContentWriter $writer
     * @param int $responseCode
     * @param string $fileName
     * @return mixed full path with filename or open handler
     */
    protected function processDownloadedContent(ContentWriter $writer, $responseCode, $fileName = null, $dir)
    {
        $writer->setPrefix($this->getPrefixFromSettings());
        try {
            if(isset($dir) && !empty($dir)){
                $writer->saveFile($dir, $fileName);
            }else if ($this->isTempFile()) {
                $writer->saveTemp();
            } elseif ($this->isFileExport()) {
                $writer->saveFile($this->getFileExportDir(), $fileName);
            } else {
                $writer->saveAsHandler();
            }
            //return object as is
            if ($this->isReturnContentWriter()) {
                return $writer;
            }
            $writer->resetContent();
            $this->logInfo("File was downloaded (http response = $responseCode), bytes written: {$writer->getBytes()}");
        } catch (\Exception $ex) {
            $this->logError("Exception appears during response processing. Http response was $responseCode: " . $ex->getMessage() . "\n" . $ex->getTraceAsString());

            return null;
        }

        return $writer->isHandler() ? $writer->getHandler() : $writer->getTempPath();
    }

    /**
     * Returns true or false if writer object is allowed as return result
     * @return boolean
     */
    private function isReturnContentWriter()
    {
        return $this->serviceContext->IppConfiguration->ContentWriter->returnOject;
    }

    /**
     * Returns prefix for contentWriter
     * @return string
     */
    private function getPrefixFromSettings()
    {
        return $this->serviceContext->IppConfiguration->ContentWriter->prefix;
    }

    /**
     * Returns true if SDK is configured to save temporary files
     * @return boolean
     */
    private function isTempFile()
    {
        return (CoreConstants::FILE_STRATEGY === $this->serviceContext->IppConfiguration->ContentWriter->strategy);
    }

    /**
     * Returns true if SDK is configured to export files to another location
     * @return boolean
     */
    private function isFileExport()
    {
        return (CoreConstants::EXPORT_STRATEGY === $this->serviceContext->IppConfiguration->ContentWriter->strategy);
    }

    /**
     * Returns path to directory where SDK should save files
     * @return string
     */
    private function getFileExportDir()
    {
        return $this->serviceContext->IppConfiguration->ContentWriter->exportDir;
    }

    /**
     * Simple verification for entities which can be returned as PDF
     */
    private function isAllowed($entity, $method)
    {
        $className = get_class($entity);
        if (!$className) {
            $this->logError("Intuit entity is expected here instead of $entity");
            throw new IdsException('Unexpected Argument Exception');
        }

        $classArray = explode('\\', $className);
        $trimedClassName = end($classArray);

        return $this->serviceContext->IppConfiguration->OpControlList->isAllowed($trimedClassName, $method);
    }

    private function verifyOperationAccess($entity, $func)
    {
        if (!$this->isAllowed($entity, $func)) {
            $this->logError("Cannot invoke " . $func . " for \"" . get_class($entity) . "\" because of operation contstrains.");
            throw new IdsException('Operation ' . $func . ' is not allowed for entity ' . get_class($entity));
        }

        return true;
    }

    private function validateEntityId($entity)
    {
        if (empty($entity)) {
            $this->logError("Argument Null Exception");
            throw new IdsException('Argument Null Exception');
        }
        if (!isset($entity->Id)) {
            $this->logError("Property ID doesn't exist");
            throw new IdsException('Property ID is not set');
        }

        if (empty($entity->Id)) {
            $this->logError("Property ID is empty");
            throw new IdsException('Property ID is empty');
        }

        return true;
    }

    private function logError($message)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Error, $message);
    }

    private function logInfo($message)
    {
        $this->serviceContext->IppConfiguration->Logger->RequestLog->Log(TraceLevel::Info, $message);
    }

    /**
     * Creates new batch
     *
     * @return Batch returns the batch object
     */
    public function CreateNewBatch()
    {
        $batch = new Batch($this->serviceContext, $this->getRestHandler(), $this->isThrownExceptionOnError());

        return $batch;
    }

    /**
     * Parse input date-time string into unix timestamp
     * @param string $str
     * @return int timestamp or false overwise
     * @throws SdkException
     */
    private function convertToTimestamp($str)
    {
        $result = date_parse($str);
        if (!$result) {
            return false;
        }
        if (empty($result) || !is_array($result)) {
            return false;
        }
        extract($result);
        if (!empty($errors)) {
            throw new SdkException("SDK failed to parse date value \"$str\":"
                . (is_array($errors) ? implode("\n", $errors) : $errors)
            );
        }

        //@TODO: mktime is deprecated since 5.3.0, this package needs 5.6
        return mktime($hour, $minute, $second, $month, $day, $year);
    }

    /**
     * Checks if input string is a valid timestamp or not
     * @param string timestamp contains input timestamp-like string
     * @return bool
     */
    public function isValidTimeStamp($timestamp)
    {
        return ((string)(int)$timestamp === $timestamp) && ($timestamp <= PHP_INT_MAX) && ($timestamp >= ~PHP_INT_MAX);
    }

    /**
     * Verifies input and returns unix timestamp
     * @param mixed $value
     * @return int
     * @throws SdkException
     */
    protected function verifyChangedSince($value)
    {
        if (is_int($value)) {
            return $value;
        }
        // remove whitespaces, tabulation etc
        $trimmed = trim($value);
        if ($this->isValidTimeStamp($trimmed)) {
            return $trimmed;
        }
        //at this point we have numeric string which is not timestamp
        if (is_numeric($value)) {
            throw new SdkException("Input string doesn't look as unix timestamp or date string");
        }
        // trying to parse string into timestamp
        $converted = $this->convertToTimestamp($value);
        if (!$converted) {
            throw new SdkException("Input value should be unix timestamp or valid date string");
        }

        return $converted;
    }

    /**
     * Get the Company Information
     * @return IPPCompanyInfo   CompanyInformation
     */
    public function getCompanyInfo()
    {
        $currentServiceContext = $this->getServiceContext();
        if (!isset($currentServiceContext) || empty($currentServiceContext->realmId)) {
            throw new SdkException("Please Setup Service Context before making get CompanyInfo call.");
        }
        //The CompanyInfo URL
        $uri = implode(CoreConstants::SLASH_CHAR, array('company', $currentServiceContext->realmId, 'companyinfo', $currentServiceContext->realmId));
        $requestParameters = new RequestParameters($uri, 'GET', CoreConstants::CONTENTTYPE_APPLICATIONXML, null);
        $restRequestHandler = $this->getRestHandler();
        list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, null, null, $this->isThrownExceptionOnError());
        $faultHandler = $restRequestHandler->getFaultHandler();
        //$faultHandler now is true or false
        if ($faultHandler) {
            $this->lastError = $faultHandler;
            return null;
        } else {
            $this->lastError = false;
            $parsedResponseBody = $this->getResponseSerializer()->Deserialize($responseBody, true);
            return $parsedResponseBody;
        }
    }

    /**
     * Get the Company Preferences Information
     * @return IPPPreferences  CompanyInformation
     */
    public function getCompanyPreferences()
    {
        $currentServiceContext = $this->getServiceContext();
        if (!isset($currentServiceContext) || empty($currentServiceContext->realmId)) {
           throw new SdkException("Please Setup Service Context before making get Company Preferences call.");
        }
        //The Preferences URL
        $uri = implode(CoreConstants::SLASH_CHAR, array('company', $currentServiceContext->realmId, 'preferences'));
        $requestParameters = new RequestParameters($uri, 'GET', CoreConstants::CONTENTTYPE_APPLICATIONXML, null);
        $restRequestHandler = $this->getRestHandler();
        list($responseCode, $responseBody) = $restRequestHandler->sendRequest($requestParameters, null, null, $this->isThrownExceptionOnError());
        $faultHandler = $restRequestHandler->getFaultHandler();
        //$faultHandler now is true or false
        if ($faultHandler) {
            $this->lastError = $faultHandler;
            return null;
        } else {
            $this->lastError = false;
            $parsedResponseBody = $this->getResponseSerializer()->Deserialize($responseBody, true);
            return $parsedResponseBody;
        }
    }

    /**
     * Get the actual ID string value of either an IPPid object, or an Id string
     * @param Object $id
     * @return String Id
     */
    private function getIDString($id){
        if($id instanceof IPPid || $id instanceof QuickBooksOnline\API\Data\IPPid){
            return (String)$id->value;
        }else{
            return (String)$id;
        }
    }
}