PjbProxyClient::getOption()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

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