Overview

Namespaces

  • Apptus
    • ESales
      • Connector
        • Report
        • Time
    • Util
      • Cache
  • PHP
  • Overview
  • Namespace
  • Class
  • Tree
  1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 
<?php
namespace Apptus\ESales\Connector;

use Apptus\ESales\Connector\Time\TimeInterval;
use Apptus\Util\String\ListCodec;
use Apptus\Util\XML\XmlListDecoder;

/**
 * Abstract super class for {@see CloudConnector} and {@see OnPremConnector}.
 *
 * Imports can be carried out using the import* methods such as {@see importConfiguration()}. <br>
 * Exports can in a similar manner be done with the export* methods such as {@see exportConfiguration()}.
 *
 * The XML data formats are documented in the section <i>Importing data</i> on <a href="http://zone.apptus.com">Apptus Zone</a> (http://zone.apptus.com).<br>
 * The format for configuration and panels isn't documented publicly. They are generated by eSales Manager and these exports and
 * imports should be used for backup and migration only. Manually editing these files isn't supported and should not be done.
 *
 * The procedure for imports can be found in the <i>eSales tutorial</i> on <a href="http://zone.apptus.com">Apptus Zone</a> (http://zone.apptus.com).
 *
 * Use the {@see session()} method to get {@see Session} instances. These are used to notify eSales and query panels.
 * View the {@see Session} documentation for details.
 *
 * @see CloudConnector
 * @see OnPremConnector
 */
abstract class Connector {

    /** @internal */
    protected $secureCluster = null;
    /** @internal */
    public $cluster = null;

    /**
     * @internal
     * @param Cluster Cluster used for secure requests (imports, defrag, etc.).
     * @param Cluster Cluster used for normal requests (queries, notifications, etc.).
     */
    protected function __construct($secureCluster, $cluster) {
        if (!$secureCluster instanceof Cluster) {
            $type = gettype($secureCluster) === 'object' ? get_class($secureCluster) : gettype($secureCluster);
            throw new \InvalidArgumentException('Invalid cluster type. Expected Cluster, got ' . $type);
        }
        if (!$cluster instanceof Cluster) {
            $type = gettype($cluster) === 'object' ? get_class($cluster) : gettype($cluster);
            throw new \InvalidArgumentException('Invalid cluster type. Expected Cluster, got ' . $type);
        }
        $this->secureCluster = $secureCluster;
        $this->cluster = $cluster;
    }

    /**
     * Gets a session for the specified session key.
     *
     * Calling this method does not affect the session in the eSales server.
     * A notification or panel query on the object created by this method,
     * is necessary for the properties to stick to the session.
     *
     * When calling this method with null in place of customerKey and/or market,
     * the server will keep the current values of these properties for this session.
     *
     * @param string
     *            The session key to identify the session.
     * @param string
     *            The customer key for the session.
     * @param string
     *            The market for the session.
     * @throws \InvalidArgumentException
     * @return Session
     *            A new Session.
     */
    public final function session($sessionKey, $customerKey = null, $market = null) {
        if ($sessionKey === null || $sessionKey === '') {
            throw new \InvalidArgumentException('Invalid session key: ' . $sessionKey);
        }
        if (!is_string($sessionKey)) {
            $type = gettype($sessionKey) === 'object' ? get_class($sessionKey) : gettype($sessionKey);
            throw new \InvalidArgumentException('Invalid session key type. Expected string, got ' . $type);
        }
        return new Session($sessionKey, $this->secureCluster, $this->cluster, $customerKey, $market);
    }

    /**
     * Return a status report from the eSales cluster as a string with an XML document.
     *
     * @throws RequestFailedException if the request to the cluster fails.
     * @return string
     *            The status report in XML.
     */
    public function status() {
        return $this->secureCluster->query('/esales/cluster/status', array(), null, LoadBalancer::PATIENCE_LONG);
    }


    /**
     * Start a GDPR job on the cluster to remove all data related to the given customer key.
     *
     * Data will be removed both from memory and disk.
     * "Linked" customer keys will also be removed (i.e. customer keys that have been notified in the same session).
     *
     * @param string $customerKey The customer key to remove.
     * @return EventDataJobResult An object containing the job ID of the created job.
     * @throws IOException
     */
    public function createRemoveCustomerDataJob($customerKey) {
        try {
            $response = $this->cluster->postAndGet('/event-data-job/v1/create-remove-customer-job?customer_key=' . $customerKey, array(), null, null);
            return EventDataJobResult::parse($response);
        } catch (MalformedJsonException $e) {
            throw new IOException('Failed to parse response. Message:' . $e->getMessage());
        }
    }


    /**
     * Start a GDPR job on the cluster to export all data related to the given customer key.
     *
     * "Linked" customer keys will also be exported (i.e. customer keys that have been notified in the same session).
     * Status of the job can be checked with {@see checkCustomerDataJobStatus()} and when the status is
     * {@see Status::DONE}, the resulting zip file can be downloaded using
     * {@see downloadExportCustomerDataJobResult()}.
     *
     * @param string $customerKey The customer key to export.
     * @return EventDataJobResult An object containing the job ID of the created job.
     * @throws IOException
     */
    public function createExportCustomerDataJob($customerKey) {
        try {
            $response = $this->secureCluster->postAndGet('/event-data-job/v1/create-export-customer-job?customer_key=' . $customerKey, array(), null, null);
            return EventDataJobResult::parse($response);
        } catch (MalformedJsonException $e) {
            throw new IOException("Failed to parse response. Message: " . $e->getMessage());
        }
    }

    /**
     * Check the status of a GDPR customer data job.
     *
     * @param string $jid The job ID.
     * @return EventDataJobResult An object containing the status of the job.
     * @throws IOException
     * @throws ClusterUnavailableException
     */
    public function checkCustomerDataJobStatus($jid) {
        try {
            $response = $this->secureCluster->query('/event-data-job/v1/job-status?jid=' . $jid, new ArgMap());
            return EventDataJobResult::parse($response);
        } catch (MalformedJsonException $e) {
            throw new IOException("Failed to parse response. Message: " . $e->getMessage());
        }
    }

    /**
     * Download the result of a GDPR export customer data job.
     *
     * @param string $jid The job ID.
     * @return An InputStream with the resulting zip file.
     * @throws ClusterUnavailableException
     */
    public function downloadExportCustomerDataJobResult($jid) {
        return $this->secureCluster->query('/event-data-job/v1/export-customer-job-result?jid=' . $jid, new ArgMap());
    }


    /**
     * Download the result of a GDPR export customer data job.
     *
     * @param string $jid The job ID.
     * @param destination Where to save the resulting zip file.
     * @return int The number of bytes downloaded.
     * @throws IOException
     * @throws ClusterUnavailableException
     */
    public function downloadExportCustomerDataJobResultToDestination($jid, $destination) {
        return $this->_save($this->downloadExportCustomerDataJobResult($jid), $destination);
    }


    /**
     * @param bool $force If the import should be forced through even if it is considered dangerous. An import is dangerous if it is
     *              a product report that drastically reduces the product count. Setting this to `false` or `null` will cause dangerous
     *              imports to be rejected with an exception, while `true` will suppress the rejection and let the import go through.
     * @internal
     */
    protected final function _importHelper($importData, $name, $type, $force = null) {
        $args = new ArgMap();
        if ($name !== null) {
            $args->put('import_name', $name);
        }
        $args->put('type', $type);
        if ($force !== null) {
            $args->put('force', $force ? 'true' : 'false');
        }

        $compressionMode = $this->secureCluster->compressionMode();

        if ($importData->getExtension() === 'gz' || $importData->getExtension() === 'gzip') {
            $compressionMode = CompressionMode::PRE_COMPRESSED_GZIP;
        }

        $response = $this->secureCluster->postAndGet('/esales/cluster/import', $args, null, $importData->getContent(), $compressionMode);
        self::readLongQueryResponse($response);
    }

    /**
     * @internal
     */
    public static function readLongQueryResponse($response) {
        $lines = preg_split('/(\r?\n)|(\n?\r)/', $response);
        for ($i = 0; $i < count($lines); $i++) {
            $line = trim($lines[$i]);

            if ($line === 'success') {
                return;
            }

            if ($line === 'fail') {
                $failType = isset($lines[$i + 1]) ? $lines[$i + 1] : null;

                if ($failType === 'busy') {
                    $msg = isset($lines[$i + 2]) ? $lines[$i + 2] : null;
                    throw new BusyClusterException($msg);
                }

                if ($failType === 'error_with_http_status') {
                    $message = implode("\n", array_slice($lines, $i + 2));
                    throw new RequestFailedException(self::getHttpErrorMessage($message));
                }

                $message = implode("\n", array_slice($lines, $i + 1));
                throw new RequestFailedException($message);
            }

            if ($line === 'working') {
                continue;
            }

            throw new RequestFailedException('Unexpected response');
        }
    }

    private static function getHttpErrorMessage($message) {
        $codec = new ListCodec(';');
        $decoded = $codec->decodeList($message);

        if (count($decoded) >= 2 && preg_match('/^\d+$/', $decoded[0])) {
            $code = $decoded[0];
            $msg = implode("\n", array_slice($decoded, 1));
            return "Status code: $code, Message: $msg";
        } else {
            return $message;
        }
    }

    /**
     * Imports products from the specified file or string to the eSales cluster.
     *
     * @param resource|string
     *            An opened file or a string with the XML.
     * @param string
     *            A unique identifier for the request. If null, a generated id will be used.
     * @param bool
     *            If the import should be forced through even if it is considered dangerous (e.g. if it drastically reduces the
     *            product count). Setting this to `false` or `null` will cause dangerous imports to be rejected with an exception,
     *            while `true` will suppress the rejection and let the import go through.
     * @throws IOException if the file could not be read.
     * @throws BusyClusterException if the cluster is busy with another task
     * @throws RequestFailedException if the request to the cluster fails.
     */
    public final function importProducts($importFile, $name = null, $force = null) {
        $this->_importHelper($this->_getInputResource($importFile), $name, 'products', $force);
    }

    /**
     * Imports panels from the specified file or string to the eSales cluster.
     *
     * @param resource|string
     *            An opened file or a string with the XML.
     * @param string
     *            A unique identifier for the request. If null, a generated id will be used.
     * @throws IOException if the file could not be read.
     * @throws BusyClusterException if the cluster is busy with another task
     * @throws RequestFailedException if the request to the cluster fails.
     */
    public final function importPanels($importFile, $name = null) {
        $this->_importHelper($this->_getInputResource($importFile), $name, 'panels');
    }

    /**
     * Imports configuration from the specified file or string to the eSales cluster.
     *
     * @param resource|string
     *            An opened file or a string with the XML.
     * @param string
     *            A unique identifier for the request. If null, a generated id will be used.
     * @throws IOException if the file could not be read.
     * @throws BusyClusterException if the cluster is busy with another task
     * @throws RequestFailedException if the request to the cluster fails.
     */
    public final function importConfiguration($importFile, $name = null) {
        $this->_importHelper($this->_getInputResource($importFile), $name, 'configuration');
    }

    /**
     * Imports synonyms from the specified file to the eSales cluster.
     *
     * @param resource|string
     *            An opened file or a string with the XML.
     * @param string
     *            A unique identifier for the request. If null, a generated id will be used.
     * @throws IOException if the file could not be read.
     * @throws BusyClusterException if the cluster is busy with another task
     * @throws RequestFailedException if the request to the cluster fails.
     */
    public final function importSynonyms($importFile, $name = null) {
        $this->_importHelper($this->_getInputResource($importFile), $name, 'synonyms');
    }

    /**
     * Imports ads from the specified file to the eSales cluster.
     *
     * @param resource|string
     *            An opened file or a string with the XML.
     * @param string
     *            A unique identifier for the request. If null, a generated id will be used.
     * @throws IOException if the file could not be read.
     * @throws BusyClusterException if the cluster is busy with another task
     * @throws RequestFailedException if the request to the cluster fails.
     */
    public final function importAds($importFile, $name = null) {
        $this->_importHelper($this->_getInputResource($importFile), $name, 'ads');
    }

    /**
     * @internal
     * @param string|resource
     * @throws IOException
     * @throws \InvalidArgumentException
     * @return string
     */
    protected final function _getInputResource($file) {
        if (is_string($file)) {
            return new ImportData($file, null);
        } elseif (is_resource($file)) {
            $r = '';
            while (!feof($file)) {
                $buffer = fread($file, 4096);
                if ($buffer === false) {
                    throw new IOException('Failed to read from resource');
                }
                $r .= $buffer;
            }

            $resourceUri = stream_get_meta_data($file)['uri'];
            return new ImportData($r, pathinfo($resourceUri, PATHINFO_EXTENSION));
        } else {
            $type = gettype($file) === 'object' ? get_class($file) : gettype($file);
            throw new \InvalidArgumentException('String or resource handle expected, got: ' . $type);
        }
    }

    /**
     * @internal
     */
    protected final function _exportHelper($type, $dest) {
        $xml = $this->secureCluster->getExport($type);
        if ($dest === null) {
            return $xml;
        }
        return $this->_save($xml, $dest);
    }

    /**
     * Exports everything from the eSales cluster as an update file.
     *
     * @param resource|string|null
     *            An opened destination file or a string with a filename.
     * @throws RequestFailedException if the request to the cluster fails.
     * @throws IOException if the file could not be written to.
     * @return int|string The number of bytes written or the XML string itself if no file was given.
     */
    public final function exportProducts($dest = null) {
        return $this->_exportHelper('products', $dest);
    }

    /**
     * Exports panels from the eSales cluster to a definition file.
     *
     * @param resource|string|null
     *            An opened destination file or a string with a filename.
     * @throws RequestFailedException if the request to the cluster fails.
     * @throws IOException if the file could not be written to.
     * @return int|string The number of bytes written or the XML string itself if no file was given.
     */
    public final function exportPanels($dest = null) {
        return $this->_exportHelper('panels', $dest);
    }

    /**
     * Exports configuration from the eSales cluster to an update file.
     *
     * @param resource|string|null
     *            An opened destination file or a string with a filename.
     * @throws RequestFailedException if the request to the cluster fails.
     * @throws IOException if the file could not be written to.
     * @return int|string The number of bytes written or the XML string itself if no file was given.
     */
    public final function exportConfiguration($dest = null) {
        return $this->_exportHelper('configuration', $dest);
    }

    /**
     * Exports synonyms from the eSales cluster as an update file.
     *
     * @param resource|string|null
     *            An opened destination file or a string with a filename.
     * @throws RequestFailedException if the request to the cluster fails.
     * @throws IOException if the file could not be written to.
     * @return int|string The number of bytes written or the XML string itself if no file was given.
     */
    public final function exportSynonyms($dest = null) {
        return $this->_exportHelper('synonyms', $dest);
    }

    /**
     * Exports ads from the eSales cluster as an update file.
     *
     * @param resource|string|null
     *            An opened destination file or a string with a filename.
     * @throws RequestFailedException if the request to the cluster fails.
     * @throws IOException if the file could not be written to.
     * @return int|string The number of bytes written or the XML string itself if no file was given.
     */
    public final function exportAds($dest = null) {
        return $this->_exportHelper('ads', $dest);
    }

    /**
     * Saves the content from a string to a file.
     *
     * @internal
     * @param string
     *            Input string.
     * @param resource|string
     *            Destination file.
     * @throws IOException
     * @return int The size of the file in bytes.
     */
    private function _save($input, $dest) {
        $output = $this->_getOutputResource($dest);

        $ok = fwrite($output, $input);
        if ($ok === false) {
            throw new IOException('Could not write to file.');
        }
        fflush($output);
        //fclose($output); // Tests use temporary files that gets deleted when closed, so don't close here.

        return $ok;
    }

    private function _getOutputResource($file) {
        if (is_string($file)) {
            if (is_file($file) && is_writable($file)) {
                return fopen($file, 'w');
            } else {
                throw new IOException('Could not open file for writing: ' . $file);
            }
        } elseif (is_resource($file)) {
            return $file;
        } else {
            throw new \InvalidArgumentException('Filename or opened file expected, got: ' . $file);
        }
    }

    /**
     * Returns a list of available markets from the eSales cluster. Never returns null.
     *
     * @throws RequestFailedException
     * @return array An indexed array of markets.
     */
    public function availableMarkets() {
        $args = new ArgMap();
        $xml = $this->secureCluster->query('/esales/markets', $args);
        return XmlListDecoder::decode('markets', 'market', $xml);
    }

    /**
     * Creates a reporter for a market during a specified time interval.
     *
     * The reporter can be used to fetch reports from the eSales cluster.
     *
     * @param string
     *            The market for the reports.
     * @param \Apptus\ESales\Connector\Time\TimeInterval
     *            The time interval to fetch reports for.
     * @return Reporter
     *            A Reporter object that can be used to fetch reports.
     */
    public function reporter($market, TimeInterval $interval) {
        return new Reporter($this->secureCluster, $market, $interval);
    }

    /**
     * Creates a reporter for the unknown market (sessions without a market property), during a specified time interval.
     *
     * The reporter can be used to fetch reports from the eSales cluster.
     *
     * @param \Apptus\ESales\Connector\Time\TimeInterval
     *            The time interval for the reports.
     * @return Reporter
     *            A Reporter object that can be used to fetch reports.
     */
    public function reporterForUnknownMarket(TimeInterval $interval) {
        return new Reporter($this->secureCluster, 'Unknown', $interval);
    }

    /**
     * Return the last 100 notifications received by the eSales cluster.
     *
     * @param string
     *            Type of notification to fetch. If null all types of notifications will be included.
     * @throws RequestFailedException if the request to the cluster fails.
     * @return string
     *            A string containing the last 100 notifications
     */
    public function latestNotifications($type = null) {
        $args = null;
        if ($type !== null) {
            $args = new ArgMap();
            $args->put('type', $type);
        }
        return $this->secureCluster->query('/esales/cluster/notifications', $args);
    }

    /**
     * Gets a log from a server. Server index should be an index in the range [0, number_of_servers - 1].
     * The servers are indexed in the same order as they appear in the cluster string. The server
     * indexes can also be retrieved from the status command.
     * Name is the name of a log. Available log names can be retrieved from {@see serverLogNames()}.
     *
     * @param int $index An index in the range [0, number of servers - 1]
     * @param string $name The name of the log to be retrieved.
     * @return string The log as a string
     * @throws RequestFailedException if the request to the cluster fails.
     */
    public function serverLog($index, $name) {
        $args = new ArgMap();
        $args->put('index', $index);
        $args->put('name', $name);

        return $this->secureCluster->query('/esales/cluster/log', $args, null, LoadBalancer::PATIENCE_VERY_LONG);
    }

    /**
     * Gets a list of available log names.
     *
     * @return array A list of available log names.
     * @throws RequestFailedException if the request to the cluster fails.
     */
    public function serverLogNames() {
        $xml = $this->secureCluster->query('/esales/log_names');
        return XmlListDecoder::decode("log_names", "log_name", $xml);
    }

    /**
     * Gets connector version.
     *
     * @return string Connector version.
     */
    public static function getVersion() {
        // This file will be filtered on build and the tags below replaced with the correct numbers.
        return HttpRequestHelper::getConnectorVersion();
    }
}
Apptus ESales Connector PHP API documentation generated by ApiGen