Completed
Push — main ( 6b3c3f...a8a095 )
by
unknown
17s queued 15s
created

CollectionHandler::addDocument()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 1
Metric Value
cc 6
eloc 26
c 5
b 1
f 1
nc 5
nop 3
dl 0
loc 35
rs 8.8817
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita Brevia plugin
6
 *
7
 * Copyright 2023 Atlas Srl
8
 */
9
namespace Brevia\BEdita\Index;
10
11
use BEdita\Core\Filesystem\FilesystemRegistry;
12
use BEdita\Core\Model\Entity\AsyncJob;
13
use BEdita\Core\Model\Entity\ObjectEntity;
14
use Brevia\BEdita\Client\BreviaClient;
15
use Cake\Http\Client\FormData;
16
use Cake\Log\LogTrait;
17
use Cake\ORM\Locator\LocatorAwareTrait;
18
use Cake\Utility\Hash;
19
use Laminas\Diactoros\UploadedFile;
20
21
/**
22
 * Handle Brevia collection via API.
23
 */
24
class CollectionHandler
25
{
26
    use LocatorAwareTrait;
27
    use LogTrait;
28
29
    /**
30
     * Brevia API client
31
     *
32
     * @var \Brevia\BEdita\Client\BreviaClient
33
     */
34
    protected BreviaClient $client;
35
36
    /**
37
     * List of properties to exclude when saving Brevia collection metadata
38
     *
39
     * @var array
40
     */
41
    public const COLLECTION_FIELDS_EXCLUDED = [
42
        'uname',
43
        'type',
44
        'created',
45
        'modified',
46
        'lang',
47
        'locked',
48
        'published',
49
        'created_by',
50
        'modified_by',
51
        'collection_uuid',
52
        'collection_updated',
53
        '_meta',
54
        '_joinData',
55
    ];
56
57
    /**
58
     * List of properties to check when updating a Brevia collection index
59
     *
60
     * @var array
61
     */
62
    public const DOCUMENT_PROPERTIES = [
63
        'title',
64
        'description',
65
        'body',
66
        'url',
67
    ];
68
69
    /**
70
     * Handler constructor
71
     */
72
    public function __construct()
73
    {
74
        $this->client = new BreviaClient();
75
    }
76
77
    /**
78
     * Create Brevia collection
79
     *
80
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
81
     * @return void
82
     */
83
    public function createCollection(ObjectEntity $collection): void
84
    {
85
        $msg = sprintf('Creating collection "%s"', $collection->get('title'));
86
        $this->log($msg, 'info');
87
88
        $response = $this->client->post('/collections', $this->breviaCollection($collection));
89
        $collection->set('collection_uuid', Hash::get($response->getJson(), 'uuid'));
90
        $collection->set('collection_updated', date('c'));
91
        $collection->getTable()->saveOrFail($collection, ['_skipAfterSave' => true]);
92
    }
93
94
    /**
95
     * Update Brevia collection
96
     *
97
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
98
     * @return void
99
     */
100
    public function updateCollection(ObjectEntity $collection): void
101
    {
102
        $msg = sprintf('Updating collection "%s"', $collection->get('title'));
103
        $this->log($msg, 'info');
104
        $path = sprintf('/collections/%s', $collection->get('collection_uuid'));
105
        $this->client->patch($path, $this->breviaCollection($collection));
106
        $collection->set('collection_updated', date('c'));
107
        $collection->getTable()->saveOrFail($collection, ['_skipAfterSave' => true]);
108
    }
109
110
    /**
111
     * Fetch Brevia collection fields
112
     *
113
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
114
     * @return array
115
     */
116
    protected function breviaCollection(ObjectEntity $collection): array
117
    {
118
        $fields = array_diff_key(
119
            $collection->toArray(),
120
            array_flip(static::COLLECTION_FIELDS_EXCLUDED)
121
        ) + ['deleted' => $collection->get('deleted')];
122
123
        return [
124
            'name' => $collection->get('uname'),
125
            'cmetadata' => array_filter($fields),
126
        ];
127
    }
128
129
    /**
130
     * Remove Brevia collection
131
     *
132
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
133
     * @return void
134
     */
135
    public function removeCollection(ObjectEntity $collection): void
136
    {
137
        $msg = sprintf('Removing collection "%s"', $collection->get('title'));
138
        $this->log($msg, 'info');
139
        $path = sprintf('/collections/%s', $collection->get('collection_uuid'));
140
        $this->client->delete($path);
141
        $collection->set('collection_uuid', null);
142
    }
143
144
    /**
145
     * Add document to collection index
146
     *
147
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
148
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
149
     * @param bool $useJob Use async job to upload files
150
     * @return void
151
     */
152
    public function addDocument(ObjectEntity $collection, ObjectEntity $entity, bool $useJob = true): void
153
    {
154
        if ($entity->get('status') !== 'on' || $entity->get('deleted')) {
155
            $msg = sprintf('Skipping doc "%s" - ', $entity->get('title')) .
156
                sprintf('status "%s" - deleted %b', $entity->get('status'), (bool)$entity->get('deleted'));
157
            $this->log($msg, 'info');
158
159
            return;
160
        }
161
        if ($entity->get('type') === 'files') {
162
            if ($useJob) {
163
                $this->uploadDocumentJob($collection, $entity);
164
            } else {
165
                $this->uploadDocument($collection, $entity);
166
            }
167
168
            return;
169
        }
170
        $body = [
171
            'collection_id' => $collection->get('collection_uuid'),
172
            'document_id' => (string)$entity->get('id'),
173
            'metadata' => $this->documentMetadata($collection, $entity),
174
        ];
175
        if ($entity->get('type') === 'links') {
176
            $body['link'] = (string)$entity->get('url');
177
            $body['options'] = Hash::get((array)$entity->get('extra'), 'brevia.options');
178
            $body['metadata']['url'] = $body['link'];
179
            $this->client->post('/index/link', array_filter($body));
180
        } else {
181
            $body['content'] = sprintf("%s\n%s", (string)$entity->get('title'), strip_tags((string)$entity->get('body')));
182
            $this->client->post('/index', $body);
183
        }
184
        $entity->set('index_updated', date('c'));
185
        $entity->set('index_status', 'done');
186
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
187
    }
188
189
    /**
190
     * Define metadata looking at current metadata and `extra.brevia.metadata`
191
     */
192
    protected function documentMetadata(ObjectEntity $collection, ObjectEntity $entity, array $addMeta = []): array
193
    {
194
        $defaultMetadata = ['type' => $entity->get('type')];
195
        $metadata = Hash::get((array)$entity->get('extra'), 'brevia.metadata', $defaultMetadata);
196
197
        $path = sprintf('/index/%s/%s', $collection->get('collection_uuid'), $entity->get('id'));
198
        $response = $this->client->get($path);
199
200
        return (array)Hash::get((array)$response->getJson(), '0.cmetadata', $metadata) + $addMeta;
201
    }
202
203
    /**
204
     * Upload file to index
205
     *
206
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
207
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
208
     * @return void
209
     */
210
    public function uploadDocument(ObjectEntity $collection, ObjectEntity $entity): void
211
    {
212
        $form = new FormData();
213
        if (empty($entity->get('streams'))) {
214
            $entity->getTable()->loadInto($entity, ['Streams']);
215
        }
216
        /** @var \BEdita\Core\Model\Entity\Stream|null $stream */
217
        $stream = Hash::get($entity, 'streams.0');
218
        if (empty($stream)) {
219
            return;
220
        }
221
        $resource = FilesystemRegistry::getMountManager()->readStream($stream->uri);
222
        $file = new UploadedFile(
223
            $resource,
224
            $stream->file_size,
225
            UPLOAD_ERR_OK,
226
            $stream->file_name,
227
            $stream->mime_type,
228
        );
229
        $form->addFile('file', $file);
230
        // read options in `extra.brevia` if available
231
        $options = Hash::get((array)$entity->get('extra'), 'brevia.options');
232
        $fileMetadata = ['file' => $stream->file_name];
233
        $form->addMany(array_filter([
234
            'collection_id' => $collection->get('collection_uuid'),
235
            'document_id' => (string)$entity->get('id'),
236
            'metadata' => json_encode($this->documentMetadata($collection, $entity, $fileMetadata)),
237
            'options' => $options ? json_encode($options) : null,
238
        ]));
239
        $this->client->postMultipart(
240
            '/index/upload',
241
            $form
242
        );
243
        $entity->set('index_updated', date('c'));
244
        $entity->set('index_status', 'done');
245
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
246
    }
247
248
    /**
249
     * Create async job to upload file to index
250
     *
251
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
252
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity File entity
253
     * @return void
254
     */
255
    protected function uploadDocumentJob(ObjectEntity $collection, ObjectEntity $entity): void
256
    {
257
        $asyncJob = new AsyncJob([
258
            'service' => 'Brevia/BEdita.IndexFile',
259
            'max_attempts' => 3,
260
            'priority' => 5,
261
            'payload' => [
262
                'collection_id' => $collection->id,
263
                'file_id' => $entity->id,
264
            ],
265
        ]);
266
        $this->fetchTable('AsyncJobs')->saveOrFail($asyncJob);
267
        $entity->set('index_status', 'processing');
268
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
269
    }
270
271
    /**
272
     * Update collection index for a document
273
     *
274
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
275
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
276
     * @param bool $forceAdd Force add document action
277
     * @return void
278
     */
279
    public function updateDocument(ObjectEntity $collection, ObjectEntity $entity, bool $forceAdd = false): void
280
    {
281
        if (
282
            $entity->isNew() ||
283
            ($entity->isDirty('deleted') && !$entity->get('deleted')) ||
284
            ($entity->isDirty('status') && $entity->get('status') === 'on') ||
285
            $forceAdd
286
        ) {
287
            $this->log($this->logMessage('Add', $collection, $entity), 'info');
288
            $this->addDocument($collection, $entity);
289
290
            return;
291
        }
292
        if (
293
            ($entity->isDirty('deleted') && $entity->get('deleted')) ||
294
            ($entity->isDirty('status') && in_array($entity->get('status'), ['draft', 'off']))
295
        ) {
296
            $this->removeDocument($collection, $entity);
297
298
            return;
299
        }
300
        // see if some object properties have changed (no effect on `files` objects)
301
        if ($entity->get('type') === 'files') {
302
            return;
303
        }
304
305
        foreach (static::DOCUMENT_PROPERTIES as $field) {
306
            if ($entity->isDirty($field)) {
307
                $this->log($this->logMessage('Update', $collection, $entity), 'info');
308
                $this->addDocument($collection, $entity);
309
310
                return;
311
            }
312
        }
313
    }
314
315
    /**
316
     * Log message on index action
317
     *
318
     * @param string $action Action to log
319
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
320
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
321
     * @return string
322
     */
323
    protected function logMessage(string $action, ObjectEntity $collection, ObjectEntity $entity): string
324
    {
325
        return sprintf('%s document "%s"', $action, $entity->get('title')) .
326
            sprintf(' [collection "%s"]', $collection->get('title'));
327
    }
328
329
    /**
330
     * Remove document from collection index
331
     *
332
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
333
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
334
     * @return void
335
     */
336
    public function removeDocument(ObjectEntity $collection, ObjectEntity $entity): void
337
    {
338
        $this->log($this->logMessage('Remove', $collection, $entity), 'info');
339
        $path = sprintf('/index/%s/%s', $collection->get('collection_uuid'), $entity->get('id'));
340
        $this->client->delete($path);
341
        $entity->set('index_status', null);
342
        $entity->set('index_updated', null);
343
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
344
    }
345
}
346