1 | // Copyright (C) 2019 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "qqmltypescreator_p.h" |
5 | #include "qqmltypesclassdescription_p.h" |
6 | |
7 | #include <QtCore/qset.h> |
8 | #include <QtCore/qjsonarray.h> |
9 | #include <QtCore/qsavefile.h> |
10 | #include <QtCore/qfile.h> |
11 | #include <QtCore/qjsondocument.h> |
12 | #include <QtCore/qversionnumber.h> |
13 | |
14 | using namespace Qt::StringLiterals; |
15 | |
16 | QT_BEGIN_NAMESPACE |
17 | |
18 | static QString enquote(const QString &string) |
19 | { |
20 | QString s = string; |
21 | return QString::fromLatin1(ba: "\"%1\"" ).arg(a: s.replace(c: QLatin1Char('\\'), after: QLatin1String("\\\\" )) |
22 | .replace(c: QLatin1Char('"'),after: QLatin1String("\\\"" ))); |
23 | } |
24 | |
25 | static QString convertPrivateClassToUsableForm(QString s) |
26 | { |
27 | // typical privateClass entry in MOC looks like: ClassName::d_func(), where |
28 | // ClassName is a non-private class name. we don't need "::d_func()" piece |
29 | // so that could be removed, but we need "Private" so that ClassName becomes |
30 | // ClassNamePrivate (at present, simply consider this correct) |
31 | s.replace(before: u"::d_func()"_s , after: u"Private"_s ); |
32 | return s; |
33 | } |
34 | |
35 | void QmlTypesCreator::writeClassProperties(const QmlTypesClassDescription &collector) |
36 | { |
37 | if (!collector.file.isEmpty()) |
38 | m_qml.writeScriptBinding(name: QLatin1String("file" ), rhs: enquote(string: collector.file)); |
39 | m_qml.writeScriptBinding(name: QLatin1String("name" ), rhs: enquote(string: collector.className)); |
40 | |
41 | if (!collector.accessSemantics.isEmpty()) |
42 | m_qml.writeScriptBinding(name: QLatin1String("accessSemantics" ), rhs: enquote(string: collector.accessSemantics)); |
43 | |
44 | if (!collector.defaultProp.isEmpty()) |
45 | m_qml.writeScriptBinding(name: QLatin1String("defaultProperty" ), rhs: enquote(string: collector.defaultProp)); |
46 | |
47 | if (!collector.parentProp.isEmpty()) |
48 | m_qml.writeScriptBinding(name: QLatin1String("parentProperty" ), rhs: enquote(string: collector.parentProp)); |
49 | |
50 | if (!collector.superClass.isEmpty()) |
51 | m_qml.writeScriptBinding(name: QLatin1String("prototype" ), rhs: enquote(string: collector.superClass)); |
52 | |
53 | if (!collector.sequenceValueType.isEmpty()) { |
54 | const QString name = collector.sequenceValueType.endsWith(c: '*'_L1) |
55 | ? collector.sequenceValueType.chopped(n: 1) |
56 | : collector.sequenceValueType; |
57 | m_qml.writeScriptBinding(name: QLatin1String("valueType" ), rhs: enquote(string: name)); |
58 | } |
59 | |
60 | if (!collector.extensionType.isEmpty()) |
61 | m_qml.writeScriptBinding(name: QLatin1String("extension" ), rhs: enquote(string: collector.extensionType)); |
62 | |
63 | if (collector.extensionIsNamespace) |
64 | m_qml.writeScriptBinding(name: QLatin1String("extensionIsNamespace" ), rhs: QLatin1String("true" )); |
65 | |
66 | if (!collector.implementsInterfaces.isEmpty()) { |
67 | QStringList interfaces; |
68 | for (const QString &interface : collector.implementsInterfaces) |
69 | interfaces << enquote(string: interface); |
70 | |
71 | m_qml.writeArrayBinding(name: QLatin1String("interfaces" ), elements: interfaces); |
72 | } |
73 | |
74 | if (!collector.deferredNames.isEmpty()) { |
75 | QStringList deferredNames; |
76 | for (const QString &name : collector.deferredNames) |
77 | deferredNames << enquote(string: name); |
78 | |
79 | m_qml.writeArrayBinding(name: QLatin1String("deferredNames" ), elements: deferredNames); |
80 | } |
81 | |
82 | if (!collector.immediateNames.isEmpty()) { |
83 | QStringList immediateNames; |
84 | for (const QString &name : collector.immediateNames) |
85 | immediateNames << enquote(string: name); |
86 | |
87 | m_qml.writeArrayBinding(name: QLatin1String("immediateNames" ), elements: immediateNames); |
88 | } |
89 | |
90 | if (collector.elementName.isEmpty()) // e.g. if QML_ANONYMOUS |
91 | return; |
92 | |
93 | if (!collector.sequenceValueType.isEmpty()) { |
94 | qWarning() << "Ignoring name of sequential container:" << collector.elementName; |
95 | qWarning() << "Sequential containers are anonymous. Use QML_ANONYMOUS to register them." ; |
96 | return; |
97 | } |
98 | |
99 | QStringList exports; |
100 | QStringList metaObjects; |
101 | |
102 | for (auto it = collector.revisions.begin(), end = collector.revisions.end(); it != end; ++it) { |
103 | const QTypeRevision revision = *it; |
104 | if (revision < collector.addedInRevision) |
105 | continue; |
106 | if (collector.removedInRevision.isValid() && !(revision < collector.removedInRevision)) |
107 | break; |
108 | if (revision.hasMajorVersion() && revision.majorVersion() > m_version.majorVersion()) |
109 | break; |
110 | |
111 | exports.append(t: enquote(string: QString::fromLatin1(ba: "%1/%2 %3.%4" ) |
112 | .arg(args&: m_module, args: collector.elementName) |
113 | .arg(a: revision.hasMajorVersion() ? revision.majorVersion() |
114 | : m_version.majorVersion()) |
115 | .arg(a: revision.minorVersion()))); |
116 | metaObjects.append(t: QString::number(revision.toEncodedVersion<quint16>())); |
117 | } |
118 | |
119 | m_qml.writeArrayBinding(name: QLatin1String("exports" ), elements: exports); |
120 | |
121 | if (!collector.isCreatable || collector.isSingleton) |
122 | m_qml.writeScriptBinding(name: QLatin1String("isCreatable" ), rhs: QLatin1String("false" )); |
123 | |
124 | if (collector.isSingleton) |
125 | m_qml.writeScriptBinding(name: QLatin1String("isSingleton" ), rhs: QLatin1String("true" )); |
126 | |
127 | if (collector.hasCustomParser) |
128 | m_qml.writeScriptBinding(name: QLatin1String("hasCustomParser" ), rhs: QLatin1String("true" )); |
129 | |
130 | m_qml.writeArrayBinding(name: QLatin1String("exportMetaObjectRevisions" ), elements: metaObjects); |
131 | |
132 | if (!collector.attachedType.isEmpty()) |
133 | m_qml.writeScriptBinding(name: QLatin1String("attachedType" ), rhs: enquote(string: collector.attachedType)); |
134 | } |
135 | |
136 | void QmlTypesCreator::writeType(const QJsonObject &property, const QString &key) |
137 | { |
138 | auto it = property.find(key); |
139 | if (it == property.end()) |
140 | return; |
141 | |
142 | QString type = (*it).toString(); |
143 | if (type.isEmpty() || type == QLatin1String("void" )) |
144 | return; |
145 | |
146 | const QLatin1String typeKey("type" ); |
147 | |
148 | bool isList = false; |
149 | bool isPointer = false; |
150 | // This is a best effort approach (like isPointer) and will not return correct results in the |
151 | // presence of typedefs. |
152 | bool isConstant = false; |
153 | |
154 | auto handleList = [&](QLatin1String list) { |
155 | if (!type.startsWith(s: list) || !type.endsWith(c: QLatin1Char('>'))) |
156 | return false; |
157 | |
158 | const int listSize = list.size(); |
159 | const QString elementType = type.mid(position: listSize, n: type.size() - listSize - 1).trimmed(); |
160 | |
161 | // QQmlListProperty internally constructs the pointer. Passing an explicit '*' will |
162 | // produce double pointers. QList is only for value types. We can't handle QLists |
163 | // of pointers (unless specially registered, but then they're not isList). |
164 | if (elementType.endsWith(c: QLatin1Char('*'))) |
165 | return false; |
166 | |
167 | isList = true; |
168 | type = elementType; |
169 | return true; |
170 | }; |
171 | |
172 | if (!handleList(QLatin1String("QQmlListProperty<" )) |
173 | && !handleList(QLatin1String("QList<" ))) { |
174 | if (type.endsWith(c: QLatin1Char('*'))) { |
175 | isPointer = true; |
176 | type = type.left(n: type.size() - 1); |
177 | } |
178 | if (type.startsWith(s: u"const " )) { |
179 | isConstant = true; |
180 | type = type.sliced(pos: strlen(s: "const " )); |
181 | } |
182 | } |
183 | |
184 | if (type == QLatin1String("qreal" )) { |
185 | #ifdef QT_COORD_TYPE_STRING |
186 | type = QLatin1String(QT_COORD_TYPE_STRING); |
187 | #else |
188 | type = QLatin1String("double" ); |
189 | #endif |
190 | } else if (type == QLatin1String("qint16" )) { |
191 | type = QLatin1String("short" ); |
192 | } else if (type == QLatin1String("quint16" )) { |
193 | type = QLatin1String("ushort" ); |
194 | } else if (type == QLatin1String("qint32" )) { |
195 | type = QLatin1String("int" ); |
196 | } else if (type == QLatin1String("quint32" )) { |
197 | type = QLatin1String("uint" ); |
198 | } else if (type == QLatin1String("qint64" )) { |
199 | type = QLatin1String("qlonglong" ); |
200 | } else if (type == QLatin1String("quint64" )) { |
201 | type = QLatin1String("qulonglong" ); |
202 | } else if (type == QLatin1String("QList<QObject*>" )) { |
203 | type = QLatin1String("QObjectList" ); |
204 | } |
205 | |
206 | m_qml.writeScriptBinding(name: typeKey, rhs: enquote(string: type)); |
207 | const QLatin1String trueString("true" ); |
208 | if (isList) |
209 | m_qml.writeScriptBinding(name: QLatin1String("isList" ), rhs: trueString); |
210 | if (isPointer) |
211 | m_qml.writeScriptBinding(name: QLatin1String("isPointer" ), rhs: trueString); |
212 | if (isConstant) |
213 | m_qml.writeScriptBinding(name: QLatin1String("isConstant" ), rhs: trueString); |
214 | } |
215 | |
216 | void QmlTypesCreator::writeProperties(const QJsonArray &properties) |
217 | { |
218 | for (const QJsonValue property : properties) { |
219 | const QJsonObject obj = property.toObject(); |
220 | const QString name = obj[QLatin1String("name" )].toString(); |
221 | m_qml.writeStartObject(component: QLatin1String("Property" )); |
222 | m_qml.writeScriptBinding(name: QLatin1String("name" ), rhs: enquote(string: name)); |
223 | const auto it = obj.find(key: QLatin1String("revision" )); |
224 | if (it != obj.end()) |
225 | m_qml.writeScriptBinding(name: QLatin1String("revision" ), rhs: QString::number(it.value().toInt())); |
226 | |
227 | writeType(property: obj, key: QLatin1String("type" )); |
228 | |
229 | const auto bindable = obj.constFind(key: QLatin1String("bindable" )); |
230 | if (bindable != obj.constEnd()) |
231 | m_qml.writeScriptBinding(name: QLatin1String("bindable" ), rhs: enquote(string: bindable->toString())); |
232 | const auto read = obj.constFind(key: QLatin1String("read" )); |
233 | if (read != obj.constEnd()) |
234 | m_qml.writeScriptBinding(name: QLatin1String("read" ), rhs: enquote(string: read->toString())); |
235 | const auto write = obj.constFind(key: QLatin1String("write" )); |
236 | if (write != obj.constEnd()) |
237 | m_qml.writeScriptBinding(name: QLatin1String("write" ), rhs: enquote(string: write->toString())); |
238 | const auto reset = obj.constFind(key: QLatin1String("reset" )); |
239 | if (reset != obj.constEnd()) |
240 | m_qml.writeScriptBinding(name: QLatin1String("reset" ), rhs: enquote(string: reset->toString())); |
241 | const auto notify = obj.constFind(key: QLatin1String("notify" )); |
242 | if (notify != obj.constEnd()) |
243 | m_qml.writeScriptBinding(name: QLatin1String("notify" ), rhs: enquote(string: notify->toString())); |
244 | const auto index = obj.constFind(key: QLatin1String("index" )); |
245 | if (index != obj.constEnd()) { |
246 | m_qml.writeScriptBinding(name: QLatin1String("index" ), |
247 | rhs: QString::number(index.value().toInt())); |
248 | } |
249 | const auto privateClass = obj.constFind(key: QLatin1String("privateClass" )); |
250 | if (privateClass != obj.constEnd()) { |
251 | m_qml.writeScriptBinding( |
252 | name: QLatin1String("privateClass" ), |
253 | rhs: enquote(string: convertPrivateClassToUsableForm(s: privateClass->toString()))); |
254 | } |
255 | |
256 | if (!obj.contains(key: QLatin1String("write" )) && !obj.contains(key: QLatin1String("member" ))) |
257 | m_qml.writeScriptBinding(name: QLatin1String("isReadonly" ), rhs: QLatin1String("true" )); |
258 | |
259 | const auto final = obj.constFind(key: QLatin1String("final" )); |
260 | if (final != obj.constEnd() && final->toBool()) |
261 | m_qml.writeScriptBinding(name: QLatin1String("isFinal" ), rhs: QLatin1String("true" )); |
262 | |
263 | const auto constant = obj.constFind(key: QLatin1String("constant" )); |
264 | if (constant != obj.constEnd() && constant->toBool()) |
265 | m_qml.writeScriptBinding(name: QLatin1String("isConstant" ), rhs: QLatin1String("true" )); |
266 | |
267 | const auto required = obj.constFind(key: QLatin1String("required" )); |
268 | if (required != obj.constEnd() && required->toBool()) |
269 | m_qml.writeScriptBinding(name: QLatin1String("isRequired" ), rhs: QLatin1String("true" )); |
270 | |
271 | m_qml.writeEndObject(); |
272 | } |
273 | } |
274 | |
275 | void QmlTypesCreator::writeMethods(const QJsonArray &methods, const QString &type) |
276 | { |
277 | const auto writeFlag = [this](const QLatin1String &name, const QJsonObject &obj) { |
278 | const auto flag = obj.find(key: name); |
279 | if (flag != obj.constEnd() && flag->toBool()) |
280 | m_qml.writeBooleanBinding(name, value: true); |
281 | }; |
282 | |
283 | for (const QJsonValue method : methods) { |
284 | const QJsonObject obj = method.toObject(); |
285 | const QString name = obj[QLatin1String("name" )].toString(); |
286 | if (name.isEmpty()) |
287 | continue; |
288 | const QJsonArray arguments = method[QLatin1String("arguments" )].toArray(); |
289 | const auto revision = obj.find(key: QLatin1String("revision" )); |
290 | m_qml.writeStartObject(component: type); |
291 | m_qml.writeScriptBinding(name: QLatin1String("name" ), rhs: enquote(string: name)); |
292 | if (revision != obj.end()) |
293 | m_qml.writeScriptBinding(name: QLatin1String("revision" ), rhs: QString::number(revision.value().toInt())); |
294 | writeType(property: obj, key: QLatin1String("returnType" )); |
295 | |
296 | writeFlag(QLatin1String("isCloned" ), obj); |
297 | writeFlag(QLatin1String("isConstructor" ), obj); |
298 | writeFlag(QLatin1String("isJavaScriptFunction" ), obj); |
299 | |
300 | for (qsizetype i = 0, end = arguments.size(); i != end; ++i) { |
301 | const QJsonObject obj = arguments[i].toObject(); |
302 | if (i == 0 && end == 1 && |
303 | obj[QLatin1String("type" )].toString() == QLatin1String("QQmlV4Function*" )) { |
304 | m_qml.writeScriptBinding(name: QLatin1String("isJavaScriptFunction" ), |
305 | rhs: QLatin1String("true" )); |
306 | break; |
307 | } |
308 | m_qml.writeStartObject(component: QLatin1String("Parameter" )); |
309 | const QString name = obj[QLatin1String("name" )].toString(); |
310 | if (!name.isEmpty()) |
311 | m_qml.writeScriptBinding(name: QLatin1String("name" ), rhs: enquote(string: name)); |
312 | writeType(property: obj, key: QLatin1String("type" )); |
313 | m_qml.writeEndObject(); |
314 | } |
315 | m_qml.writeEndObject(); |
316 | } |
317 | } |
318 | |
319 | void QmlTypesCreator::writeEnums(const QJsonArray &enums) |
320 | { |
321 | for (const QJsonValue item : enums) { |
322 | const QJsonObject obj = item.toObject(); |
323 | const QJsonArray values = obj.value(key: QLatin1String("values" )).toArray(); |
324 | QStringList valueList; |
325 | |
326 | for (const QJsonValue value : values) |
327 | valueList.append(t: enquote(string: value.toString())); |
328 | |
329 | m_qml.writeStartObject(component: QLatin1String("Enum" )); |
330 | m_qml.writeScriptBinding(name: QLatin1String("name" ), |
331 | rhs: enquote(string: obj.value(key: QLatin1String("name" )).toString())); |
332 | auto alias = obj.find(key: QLatin1String("alias" )); |
333 | if (alias != obj.end()) |
334 | m_qml.writeScriptBinding(name: alias.key(), rhs: enquote(string: alias->toString())); |
335 | auto isFlag = obj.find(key: QLatin1String("isFlag" )); |
336 | if (isFlag != obj.end() && isFlag->toBool()) |
337 | m_qml.writeBooleanBinding(name: isFlag.key(), value: true); |
338 | writeType(property: obj, key: QLatin1String("type" )); |
339 | m_qml.writeArrayBinding(name: QLatin1String("values" ), elements: valueList); |
340 | m_qml.writeEndObject(); |
341 | } |
342 | } |
343 | |
344 | static bool isAllowedInMajorVersion(const QJsonValue &member, QTypeRevision maxMajorVersion) |
345 | { |
346 | const auto memberObject = member.toObject(); |
347 | const auto it = memberObject.find(key: QLatin1String("revision" )); |
348 | if (it == memberObject.end()) |
349 | return true; |
350 | |
351 | const QTypeRevision memberRevision = QTypeRevision::fromEncodedVersion(value: it->toInt()); |
352 | return !memberRevision.hasMajorVersion() |
353 | || memberRevision.majorVersion() <= maxMajorVersion.majorVersion(); |
354 | } |
355 | |
356 | template<typename Postprocess> |
357 | QJsonArray members( |
358 | const QJsonObject *classDef, const QString &key, QTypeRevision maxMajorVersion, |
359 | Postprocess &&process) |
360 | { |
361 | QJsonArray classDefMembers; |
362 | |
363 | const QJsonArray candidates = classDef->value(key).toArray(); |
364 | for (QJsonValue member : candidates) { |
365 | if (isAllowedInMajorVersion(member, maxMajorVersion)) |
366 | classDefMembers.append(value: process(std::move(member))); |
367 | } |
368 | |
369 | return classDefMembers; |
370 | } |
371 | |
372 | static QJsonArray members( |
373 | const QJsonObject *classDef, const QString &key, QTypeRevision maxMajorVersion) |
374 | { |
375 | return members(classDef, key, maxMajorVersion, process: [](QJsonValue &&member) { return member; }); |
376 | } |
377 | |
378 | static QJsonArray constructors( |
379 | const QJsonObject *classDef, const QString &key, QTypeRevision maxMajorVersion) |
380 | { |
381 | return members(classDef, key, maxMajorVersion, process: [](QJsonValue &&member) { |
382 | QJsonObject ctor = member.toObject(); |
383 | ctor[QLatin1String("isConstructor" )] = true; |
384 | return ctor; |
385 | }); |
386 | } |
387 | |
388 | |
389 | void QmlTypesCreator::writeComponents() |
390 | { |
391 | const QLatin1String signalsKey("signals" ); |
392 | const QLatin1String enumsKey("enums" ); |
393 | const QLatin1String propertiesKey("properties" ); |
394 | const QLatin1String slotsKey("slots" ); |
395 | const QLatin1String methodsKey("methods" ); |
396 | const QLatin1String constructorsKey("constructors" ); |
397 | |
398 | const QLatin1String signalElement("Signal" ); |
399 | const QLatin1String componentElement("Component" ); |
400 | const QLatin1String methodElement("Method" ); |
401 | |
402 | for (const QJsonObject &component : m_ownTypes) { |
403 | QmlTypesClassDescription collector; |
404 | collector.collect(classDef: &component, types: m_ownTypes, foreign: m_foreignTypes, |
405 | mode: QmlTypesClassDescription::TopLevel, defaultRevision: m_version); |
406 | |
407 | if (collector.omitFromQmlTypes) |
408 | continue; |
409 | |
410 | m_qml.writeStartObject(component: componentElement); |
411 | |
412 | writeClassProperties(collector); |
413 | |
414 | if (const QJsonObject *classDef = collector.resolvedClass) { |
415 | writeEnums(enums: members(classDef, key: enumsKey, maxMajorVersion: m_version)); |
416 | |
417 | writeProperties(properties: members(classDef, key: propertiesKey, maxMajorVersion: m_version)); |
418 | |
419 | writeMethods(methods: members(classDef, key: signalsKey, maxMajorVersion: m_version), type: signalElement); |
420 | writeMethods(methods: members(classDef, key: slotsKey, maxMajorVersion: m_version), type: methodElement); |
421 | writeMethods(methods: members(classDef, key: methodsKey, maxMajorVersion: m_version), type: methodElement); |
422 | writeMethods(methods: constructors(classDef, key: constructorsKey, maxMajorVersion: m_version), type: methodElement); |
423 | } |
424 | m_qml.writeEndObject(); |
425 | |
426 | if (collector.resolvedClass != &component |
427 | && std::binary_search( |
428 | first: m_referencedTypes.begin(), last: m_referencedTypes.end(), |
429 | val: component.value(QStringLiteral("qualifiedClassName" )).toString())) { |
430 | |
431 | // This type is referenced from elsewhere and has a QML_FOREIGN of its own. We need to |
432 | // also generate a description of the local type then. All the QML_* macros are |
433 | // ignored, and the result is an anonymous type. |
434 | |
435 | m_qml.writeStartObject(component: componentElement); |
436 | |
437 | QmlTypesClassDescription collector; |
438 | collector.collectLocalAnonymous(classDef: &component, types: m_ownTypes, foreign: m_foreignTypes, defaultRevision: m_version); |
439 | |
440 | writeClassProperties(collector); |
441 | writeEnums(enums: members(classDef: &component, key: enumsKey, maxMajorVersion: m_version)); |
442 | |
443 | writeProperties(properties: members(classDef: &component, key: propertiesKey, maxMajorVersion: m_version)); |
444 | |
445 | writeMethods(methods: members(classDef: &component, key: signalsKey, maxMajorVersion: m_version), type: signalElement); |
446 | writeMethods(methods: members(classDef: &component, key: slotsKey, maxMajorVersion: m_version), type: methodElement); |
447 | writeMethods(methods: members(classDef: &component, key: methodsKey, maxMajorVersion: m_version), type: methodElement); |
448 | writeMethods(methods: constructors(classDef: &component, key: constructorsKey, maxMajorVersion: m_version), type: methodElement); |
449 | |
450 | m_qml.writeEndObject(); |
451 | } |
452 | } |
453 | } |
454 | |
455 | bool QmlTypesCreator::generate(const QString &outFileName) |
456 | { |
457 | m_qml.writeStartDocument(); |
458 | m_qml.writeLibraryImport(uri: QLatin1String("QtQuick.tooling" ), majorVersion: 1, minorVersion: 2); |
459 | m_qml.write(data: QString::fromLatin1( |
460 | ba: "\n// This file describes the plugin-supplied types contained in the library." |
461 | "\n// It is used for QML tooling purposes only." |
462 | "\n//" |
463 | "\n// This file was auto-generated by qmltyperegistrar.\n\n" )); |
464 | m_qml.writeStartObject(component: QLatin1String("Module" )); |
465 | |
466 | writeComponents(); |
467 | |
468 | m_qml.writeEndObject(); |
469 | |
470 | QSaveFile file(outFileName); |
471 | if (!file.open(flags: QIODevice::WriteOnly)) |
472 | return false; |
473 | |
474 | if (file.write(data: m_output) != m_output.size()) |
475 | return false; |
476 | |
477 | return file.commit(); |
478 | } |
479 | |
480 | QT_END_NAMESPACE |
481 | |
482 | |