Passed
Push — master ( 550b9e...103607 )
by Sébastien
09:49 queued 07:35
created

PjbProxyClient::clearLastException()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Soluble Japha / PhpJavaBridge.
7
 *
8
 * @author Vanvelthem Sébastien
9
 * @license MIT
10
 */
11
12
namespace Soluble\Japha\Bridge\Driver\Pjb62;
13
14
use Monolog\Logger;
15
use Soluble\Japha\Bridge\Exception;
16
use Soluble\Japha\Interfaces;
17
use Soluble\Japha\Bridge\Driver\ClientInterface;
18
use ArrayObject;
19
use Soluble\Japha\Bridge\Driver\Pjb62\Exception\IllegalArgumentException;
20
use Psr\Log\LoggerInterface;
21
use Psr\Log\NullLogger;
22
23
class PjbProxyClient implements ClientInterface
24
{
25
    /**
26
     * @var PjbProxyClient|null
27
     */
28
    protected static $instance;
29
30
    protected static $unregistering = false;
31
32
    /**
33
     * @var array
34
     */
35
    protected $defaultOptions = [
36
        'java_disable_autoload' => true,
37
        'java_log_level' => null,
38
        'java_send_size' => 8192,
39
        'java_recv_size' => 8192,
40
        // By default do not use persistent connection
41
        'use_persistent_connection' => false,
42
        // java_prefer_values=true is the default working mode
43
        // of the soluble-japha client... It may be less efficient,
44
        // because it casts java String, Boolean, Integer... objects
45
        // automatically into (string, bool, integer...), and thus
46
        // not require additional writing like ($ba->values($myInt)) in
47
        // order to use a remote object. But prevent to work on the proxy instead,
48
        // so the value is always transferred for those types. If you put
49
        // at false you'll have to rework on the code.
50
        'java_prefer_values' => true,
51
        // use SimpleParser (pure PHP code) even if NativeParser (based on xml_* php functions) may be used
52
        // should only be used to workaround bugs or limitations regarding the xml extension
53
        'force_simple_xml_parser' => false,
54
    ];
55
56
    /**
57
     * @var Client|null
58
     */
59
    protected static $client;
60
61
    /**
62
     * Internal cache for already loaded Java classes.
63
     *
64
     * @var array
65
     */
66
    protected $classMapCache = [];
67
68
    /**
69
     * @var string
70
     */
71
    protected $compatibilityOption;
72
73
    /**
74
     * @var ArrayObject
75
     */
76
    protected $options;
77
78
    /**
79
     * @var LoggerInterface
80
     */
81
    protected $logger;
82
83
    /**
84
     * @var string|null
85
     */
86
    protected static $instanceOptionsKey;
87
88
    /**
89
     * Private contructor.
90
     *
91
     * $options requires :
92
     *  'servlet_address' => 'http://127.0.0.1:8080/javabridge-bundle/java/servlet.phpjavabridge'
93
     *
94
     *  Optionally :
95
     *  'java_log_level' => null
96
     *  'java_send_size' => 8192,
97
     *  'java_recv_size' => 8192
98
     *
99
     *
100
     * @throws Exception\InvalidArgumentException
101
     * @throws Exception\ConnectionException
102
     *
103
     * @see PjbProxyClient::getInstance()
104
     *
105
     * @param array           $options
106
     * @param LoggerInterface $logger
107
     */
108 27
    protected function __construct(array $options, LoggerInterface $logger)
109
    {
110 27
        $this->options = new ArrayObject(array_merge($this->defaultOptions, $options));
111 27
        self::$instanceOptionsKey = serialize((array) $this->options);
112
113 27
        $this->logger = $logger;
114 27
        $this->loadClient();
115 23
    }
116
117
    /**
118
     * Return a unique instance of the phpjavabridge client
119
     * $options is an associative array and requires :.
120
     *
121
     *  'servlet_address' => 'http://127.0.0.1:8080/javabridge-bundle/java/servlet.phpjavabridge'
122
     *
123
     *  $options can be :
124
     *  "java_send_size" => 8192,
125
     *  "java_recv_size" => 8192,
126
     *  "use_persistent_connection" => false
127
     *  "java_log_level' => null,
128
     *  "java_prefer_values" => true (see note)
129
     *
130
     * <code>
131
     *    $options = [
132
     *      "servlet_address" => 'http://127.0.0.1:8080/javabridge-bundle/servlet.phpjavabridge'
133
     *      "java_send_size" => 8192,
134
     *      "java_recv_size" => 8192,
135
     *      "use_persistent_connection" => false,
136
     *      "internal_encoding" => 'UTF-8'
137
     *    ];
138
     *    $pjb = PjbProxyClient::getInstance($options, $logger);
139
     * </code>
140
     *
141
     * Note: java_prefer_values=true is the default working mode
142
     * of the soluble-japha client...
143
     *
144
     * Disadvantage: Not good for performance !!!
145
     *  > From mailinglist: Please note that the option JAVA_PREFER_VALUES kills performance as it
146
     *  > checks for an exception after each call (I.e. each java call generates a full network round-trip).
147
     *  Note that in simple_benchmarks.php no difference have been measured (localhost), need more
148
     *  taylor made tests to see.
149
     *
150
     * Advantage: More readable / writable
151
     *  > it casts java String, Boolean, Integer... objects
152
     *  > automatically into (string, bool, integer...), and thus
153
     *  > not require additional writing like ($ba->values($myInt)) in
154
     *  > order to get the value. (proxy)
155
     *
156
     * If you put at false you'll have to rework on the code.
157
     * Check what's best for yourself
158
     *
159
     * @throws Exception\InvalidArgumentException
160
     * @throws Exception\ConnectionException
161
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
162
     *
163
     * @param array|null           $options
164
     * @param LoggerInterface|null $logger  any psr3 logger
165
     *
166
     * @return PjbProxyClient
167
     */
168 148
    public static function getInstance(?array $options = null, ?LoggerInterface $logger = null): self
169
    {
170 148
        if (self::$instance === null) {
171 28
            if ($options === null) {
172 3
                throw new Exception\InvalidUsageException(
173
                    'Cannot instanciate PjbProxyClient without "$options" the first time, '.
174 3
                    'or the instance have been unregistered since'
175
                );
176
            }
177 27
            if ($logger === null) {
178 10
                $logger = new NullLogger();
179
            }
180 27
            self::$instance = new self($options, $logger);
181
        }
182
183 145
        return self::$instance;
184
    }
185
186
    /**
187
     * @return bool
188
     */
189 3
    public static function isInitialized(): bool
190
    {
191 3
        return self::$instance !== null;
192
    }
193
194
    /**
195
     * Load pjb client with options.
196
     *
197
     * @throws Exception\InvalidArgumentException
198
     * @throws Exception\ConnectionException
199
     */
200 27
    protected function loadClient(): void
201
    {
202 27
        if (self::$client === null) {
203 27
            $options = $this->options;
204
205 27
            if (!isset($options['servlet_address'])) {
206 1
                throw new Exception\InvalidArgumentException(__METHOD__.' Missing required parameter servlet_address');
207
            }
208
209 26
            $connection = static::parseServletUrl($options['servlet_address']);
210
211 25
            $params = new ArrayObject([
212 25
                Client::PARAM_JAVA_HOSTS => $connection['servlet_host'],
213 25
                Client::PARAM_JAVA_SERVLET => $connection['servlet_uri'],
214 25
                Client::PARAM_JAVA_AUTH_USER => $connection['auth_user'],
215 25
                Client::PARAM_JAVA_AUTH_PASSWORD => $connection['auth_password'],
216 25
                Client::PARAM_JAVA_DISABLE_AUTOLOAD => $options['java_disable_autoload'],
217 25
                Client::PARAM_JAVA_PREFER_VALUES => $options['java_prefer_values'],
218 25
                Client::PARAM_JAVA_SEND_SIZE => $options['java_send_size'],
219 25
                Client::PARAM_JAVA_RECV_SIZE => $options['java_recv_size'],
220 25
                Client::PARAM_JAVA_LOG_LEVEL => $options['java_log_level'],
221 25
                Client::PARAM_XML_PARSER_FORCE_SIMPLE_PARSER => $options['force_simple_xml_parser'],
222 25
                Client::PARAM_USE_PERSISTENT_CONNECTION => $options['use_persistent_connection']
223
            ]);
224
225 25
            self::$client = new Client($params, $this->logger);
226
227
            // Added in order to work with custom exceptions
228 23
            self::getClient()->throwExceptionProxyFactory = new Proxy\DefaultThrowExceptionProxyFactory(self::$client, $this->logger);
229
230 23
            $this->bootstrap();
231
        }
232 23
    }
233
234
    /**
235
     * Return Pjb62 internal client.
236
     *
237
     * @return Client
238
     *
239
     * @throws Exception\BrokenConnectionException
240
     */
241 133
    public static function getClient(): Client
242
    {
243 133
        if (self::$client === null) {
244 5
            throw new Exception\BrokenConnectionException('Client is not registered');
245
        }
246
247 133
        return self::$client;
248
    }
249
250
    /**
251
     * Return a Java class.
252
     *
253
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
254
     *
255
     * @param string $name Name of the java class
256
     *
257
     * @return JavaClass
258
     */
259 55
    public function getJavaClass($name): Interfaces\JavaClass
260
    {
261 55
        if (!array_key_exists($name, $this->classMapCache)) {
262 24
            $this->classMapCache[$name] = new JavaClass($name);
263
        }
264
265 50
        return $this->classMapCache[$name];
266
    }
267
268
    /**
269
     * Invoke a method dynamically.
270
     *
271
     * Example:
272
     * <code>
273
     * $bigint1 = new Java('java.math.BigInteger', 10);
274
     * $bigint2 = new Java('java.math.BigInteger', 20);
275
     * $bigint3 = PjbProxyClient::invokeMethod($bigint, "add", [$bigint2])
276
     * </code>
277
     *
278
     * <br> Any declared exception can be caught by PHP code. <br>
279
     * Exceptions derived from java.lang.RuntimeException or Error should
280
     * not be caught unless declared in the methods throws clause -- OutOfMemoryErrors cannot be caught at all,
281
     * even if declared.
282
     *
283
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
284
     *
285
     * @param Interfaces\JavaType|null $object a java object or type
286
     * @param string                   $method A method string
287
     * @param array                    $args   Arguments to send to method
288
     *
289
     * @return mixed
290
     */
291 5
    public function invokeMethod(?Interfaces\JavaType $object = null, string $method, array $args = [])
292
    {
293 5
        $id = ($object === null) ? 0 : $object->__getJavaInternalObjectId();
294
295 5
        return self::getClient()->invokeMethod($id, $method, $args);
296
    }
297
298
    /**
299
     * Inspect the java object | type.
300
     *
301
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
302
     *
303
     * @param Interfaces\JavaType $object
304
     *
305
     * @return string
306
     *
307
     * @throws IllegalArgumentException
308
     */
309 12
    public function inspect(Interfaces\JavaType $object): string
310
    {
311 12
        return self::getClient()->invokeMethod(0, 'inspect', [$object]);
312
    }
313
314
    /**
315
     * Test whether an object is an instance of java class or interface.
316
     *
317
     * @throws Exception\InvalidArgumentException
318
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
319
     *
320
     * @param Interfaces\JavaObject                                             $object
321
     * @param JavaType|string|Interfaces\JavaClass|Interfaces\JavaObject|string $class
322
     *
323
     * @return bool
324
     */
325 8
    public function isInstanceOf(Interfaces\JavaObject $object, $class): bool
326
    {
327 8
        if (is_string($class)) {
328
            // Attempt to initiate a class
329 6
            $name = $class;
330
            // Will eventually throws ClassNotFoundException
331 6
            $class = $this->getJavaClass($name);
332 3
        } elseif (!$class instanceof Interfaces\JavaObject) {
333 2
            throw new Exception\InvalidArgumentException(__METHOD__.'Class $class parameter must be of Interfaces\JavaClass, Interfaces\JavaObject or string');
334
        }
335
336 4
        return self::getClient()->invokeMethod(0, 'instanceOf', [$object, $class]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::getClient()...array($object, $class)) returns the type Soluble\Japha\Bridge\Driver\Pjb62\JavaType which is incompatible with the type-hinted return boolean.
Loading history...
337
    }
338
339
    /**
340
     * Evaluate a Java object.
341
     *
342
     * Evaluate a object and fetch its content, if possible. Use java_values() to convert a Java object into an equivalent PHP value.
343
     *
344
     * A java array, Map or Collection object is returned
345
     * as a php array.
346
     * An array, Map or Collection proxy is returned as a java array, Map or
347
     * Collection object, and a null proxy is returned as null.
348
     * All values of java types for which a primitive php type exists are
349
     * returned as php values.
350
     * Everything else is returned unevaluated.
351
     * Please make sure that the values do not not exceed
352
     * php's memory limit. Example:
353
     *
354
     *
355
     * <code>
356
     * $str = new java("java.lang.String", "hello");
357
     * echo java_values($str);
358
     * => hello
359
     * $chr = $str->toCharArray();
360
     * echo $chr;
361
     * => [o(array_of-C):"[C@1b10d42"]
362
     * $ar = java_values($chr);
363
     * print $ar;
364
     * => Array
365
     * print $ar[0];
366
     * => [o(Character):"h"]
367
     * print java_values($ar[0]);
368
     * => h
369
     * </code>
370
     *
371
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
372
     *
373
     * @param Interfaces\JavaObject $object
374
     *
375
     * @return mixed
376
     */
377 12
    public function getValues(Interfaces\JavaObject $object)
378
    {
379 12
        return self::getClient()->invokeMethod(0, 'getValues', [$object]);
380
    }
381
382
    /**
383
     * Return latest exception.
384
     *
385
     * @deprecated
386
     *
387
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
388
     *
389
     * @return \Soluble\Japha\Bridge\Driver\Pjb62\Exception\JavaException
390
     */
391
    public function getLastException()
392
    {
393
        return self::getClient()->invokeMethod(0, 'getLastException', []);
394
    }
395
396
    /**
397
     * Clear last exception.
398
     *
399
     * @deprecated
400
     *
401
     * @throws \Soluble\Japha\Bridge\Driver\Pjb62\Exception\BrokenConnectionException
402
     */
403
    public function clearLastException(): void
404
    {
405
        self::getClient()->invokeMethod(0, 'clearLastException', []);
406
    }
407
408
    /**
409
     * @param Client $client
410
     *
411
     * @return string
412
     */
413 23
    public function getCompatibilityOption(Client $client = null): string
414
    {
415 23
        if ($this->compatibilityOption === null) {
416 23
            if ($client === null) {
417 1
                $client = $client = self::getClient();
0 ignored issues
show
Unused Code introduced by
The assignment to $client is dead and can be removed.
Loading history...
418
            }
419
420 23
            $java_prefer_values = (int) $this->getOption('java_prefer_values');
421 23
            $java_log_level = $this->getOption('java_log_level');
422 23
            $compatibility = ($client->RUNTIME['PARSER'] === 'NATIVE') ? (0103 - $java_prefer_values) : (0100 + $java_prefer_values);
423 23
            if (is_int($java_log_level)) {
424 1
                $compatibility |= 128 | (7 & $java_log_level) << 2;
425
            }
426 23
            $this->compatibilityOption = \chr($compatibility);
427
        }
428
429 23
        return $this->compatibilityOption;
430
    }
431
432
    /**
433
     * Utility class to parse servlet_address,
434
     * i.e 'http://localhost:8080/javabridge-bundle/java/servlet.phpjavabridge'.
435
     *
436
     * @throws Exception\InvalidArgumentException
437
     *
438
     * @param string $servlet_address
439
     *
440
     * @return array associative array with 'servlet_host' and 'servlet_uri'
441
     */
442 29
    public static function parseServletUrl(string $servlet_address): array
443
    {
444 29
        $url = parse_url($servlet_address);
445
446 29
        if ($url === false || !isset($url['host'])) {
447 1
            throw new Exception\InvalidArgumentException(__METHOD__." Cannot parse url '$servlet_address'");
448
        }
449
450 28
        $scheme = '';
451 28
        if (isset($url['scheme'])) {
452 28
            $scheme = $url['scheme'] === 'https' ? 'ssl://' : $scheme;
453
        }
454 28
        $host = $url['host'];
455 28
        $port = $url['port'];
456 28
        $path = $url['path'] ?? '';
457
458
        $infos = [
459 28
            'servlet_host' => "{$scheme}{$host}:{$port}",
460 28
            'servlet_uri' => $path,
461 28
            'auth_user' => $url['user'] ?? null,
462 28
            'auth_password' => $url['pass'] ?? null,
463
        ];
464
465 28
        return $infos;
466
    }
467
468
    /**
469
     * For compatibility usage all constants have been kept.
470
     */
471 23
    protected function bootstrap($options = []): void
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

471
    protected function bootstrap(/** @scrutinizer ignore-unused */ $options = []): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
472
    {
473 23
        register_shutdown_function(['Soluble\Japha\Bridge\Driver\Pjb62\PjbProxyClient', 'unregisterInstance']);
474 23
    }
475
476
    /**
477
     * Return options.
478
     *
479
     * @return ArrayObject
480
     */
481 2
    public function getOptions(): ArrayObject
482
    {
483 2
        return $this->options;
484
    }
485
486
    /**
487
     * Return specific option.
488
     *
489
     * @return mixed
490
     */
491 24
    public function getOption(string $name)
492
    {
493 24
        if (!$this->options->offsetExists($name)) {
494 1
            throw new Exception\InvalidArgumentException("Option '$name' does not exists'");
495
        }
496
497 24
        return $this->options[$name];
498
    }
499
500 17
    public function getLogger(): LoggerInterface
501
    {
502 17
        return $this->logger;
503
    }
504
505
    /**
506
     * @throws Exception\BrokenConnectionException|Exception\AuthenticationException
507
     */
508 17
    public static function unregisterAndThrowBrokenConnectionException(string $message = null, int $code = null): void
509
    {
510 17
        if (self::$instance !== null) {
511 17
            $message = $message ?? 'undefined messsage';
512
513
            switch ($code) {
514 17
                case 401:
515
                    $exception = new Exception\AuthenticationException(sprintf(
516
                        'Java bridge authentication failure: code: %s',
517
                        $code
518
                    ));
519
                    break;
520
                default:
521 17
                    $exception = new Exception\BrokenConnectionException(sprintf(
522 17
                        'Java bridge broken connection: "%s" (code: %s)',
523 17
                        $message,
524 17
                        $code
525
                    ));
526
            }
527
            try {
528 17
                self::$instance->getLogger()->critical(sprintf(
529 17
                    '[soluble-japha] BrokenConnectionException to "%s": "%s" (code: "%s")',
530 17
                    self::$instance->getOption('servlet_address'),
531 17
                    $message,
532 17
                    $code ?? '?'
533
                ));
534
            } catch (\Throwable $e) {
535
                // discard logger errors
536
            }
537
538 17
            self::unregisterInstance();
539 17
            throw $exception;
540
        }
541
    }
542
543
    /**
544
     * Clean up PjbProxyClient instance.
545
     */
546 22
    public static function unregisterInstance(): void
547
    {
548 22
        if (!self::$unregistering && self::$client !== null) {
549 22
            self::$unregistering = true;
550
551 22
            if ((self::$client->preparedToSendBuffer ?: '') !== '') {
552
                self::$client->sendBuffer .= self::$client->preparedToSendBuffer;
553
            }
554
555
            try {
556 22
                self::$client->sendBuffer .= self::$client->protocol->getKeepAlive();
557 22
                self::$client->protocol->flush();
558 1
            } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
559
            }
560
561
            // TODO MUST TEST, IT WAS REMOVED FROM FUNCTION
562
            // BECAUSE IT SIMPLY LOOKS LIKE THE LINES BEFORE
563
            // ADDED AN IF TO CHECK THE CHANNEL In CASE OF
564
            //
565 22
            if (isset(self::$client->protocol->handler->channel) &&
566 22
                false === strpos(get_class(self::getClient()->protocol->handler->channel), '/EmptyChannel/')) {
567
                try {
568 22
                    self::$client->protocol->keepAlive();
569 17
                } catch (\Throwable $e) {
570
                    // silently discard exceptions when unregistering
571
                }
572
            }
573
574 22
            self::$client = null;
575 22
            self::$instance = null;
576 22
            self::$instanceOptionsKey = null;
577 22
            self::$unregistering = false;
578
        }
579 22
    }
580
}
581