UNPKG

@decaf-ts/for-nano

Version:

decaf-ts persistence adapter for CouchDB via nano

852 lines (847 loc) 97 kB
import { OperationKeys, InternalError, ConflictError, onCreate } from '@decaf-ts/db-decorators'; import 'reflect-metadata'; import { CouchDBKeys, CouchDBAdapter, generateIndexes } from '@decaf-ts/for-couchdb'; import Nano from 'nano'; import { Decoration, propMetadata } from '@decaf-ts/decorator-validation'; import { Dispatch, Repository, PersistenceKeys, UnsupportedError } from '@decaf-ts/core'; /** * @description Identifier for the Nano database flavor * @summary Constant string that identifies the database type as "nano" for use in adapter selection and configuration * @const NanoFlavour * @memberOf module:for-nano */ const NanoFlavour = "nano"; /** * @description Dispatcher for Nano database change events * @summary Handles the subscription to and processing of database change events from a Nano database, * notifying observers when documents are created, updated, or deleted * @template DocumentScope - The Nano document scope type * @param {number} [timeout=5000] - Timeout in milliseconds for change feed requests * @class NanoDispatch * @example * ```typescript * // Create a dispatcher for a Nano database * const db = server.db.use('my_database'); * const adapter = new NanoAdapter(db); * const dispatch = new NanoDispatch(); * * // The dispatcher will automatically subscribe to changes * // and notify observers when documents change * ``` * @mermaid * classDiagram * class Dispatch { * +initialize() * +updateObservers() * } * class NanoDispatch { * -observerLastUpdate?: string * -attemptCounter: number * -timeout: number * +constructor(timeout) * #changeHandler() * #initialize() * } * Dispatch <|-- NanoDispatch */ class NanoDispatch extends Dispatch { constructor(timeout = 5000) { super(); this.timeout = timeout; this.attemptCounter = 0; } /** * @description Processes database change events * @summary Handles the response from the Nano changes feed, processes the changes, * and notifies observers about document changes * @param {RequestError | null} error - Error object if the request failed * @param response - The changes response from Nano * @param {any} [headers] - Response headers (unused) * @return {Promise<void>} A promise that resolves when all changes have been processed * @mermaid * sequenceDiagram * participant D as NanoDispatch * participant L as Logger * participant O as Observers * Note over D: Receive changes from Nano * alt Error in response * D->>L: Log error * D-->>D: Return early * end * alt Response is string * D->>D: Parse JSON from string * end * D->>D: Process changes * D->>D: Group changes by table and operation * loop For each table * loop For each operation * D->>O: updateObservers(table, operation, ids) * D->>D: Update observerLastUpdate * D->>L: Log successful dispatch * end * end */ async changeHandler(error, response, // eslint-disable-next-line @typescript-eslint/no-unused-vars headers) { const log = this.log.for(this.changeHandler); if (error) return log.error(`Error in change request: ${error}`); try { response = (typeof response === "string" ? response .split("\n") .filter((r) => !!r) .map((r) => JSON.parse(r)) : response); } catch (e) { return log.error(`Error parsing couchdb change feed: ${e}`); } const count = response.length; if (count > 0) { log.debug(`Received ${count} changes. processing...`); const changes = response .map((rec, i) => { if (i === count - 1) { if (this.observerLastUpdate === rec.last_seq) log.error(`Invalid last update check: ${this.observerLastUpdate} !== ${rec.last_seq}`); return; } const r = rec; const [table, id] = r.id.split(CouchDBKeys.SEPARATOR); return { table: table, id: id, operation: r.deleted ? OperationKeys.DELETE : r.changes[r.changes.length - 1].rev.split("-")[0] === "1" ? OperationKeys.CREATE : OperationKeys.UPDATE, step: r.changes[r.changes.length - 1].rev, }; }) .reduce((accum, r) => { if (!r) return accum; const { table, id, operation, step } = r; if (!accum[table]) accum[table] = {}; if (!accum[table][operation]) accum[table][operation] = { ids: new Set(), step: step }; accum[table][operation].ids.add(id); accum[table][operation].step = step; return accum; }, {}); for (const table of Object.keys(changes)) { for (const op of Object.keys(changes[table])) { try { await this.updateObservers(table, op, [ ...changes[table][op].ids.values(), ]); this.observerLastUpdate = changes[table][op].step; log.verbose(`Observer refresh dispatched by ${op} for ${table}`); log.debug(`pks: ${Array.from(changes[table][op].ids.values())}`); } catch (e) { log.error(`Failed to dispatch observer refresh for ${table}, op ${op}: ${e}`); } } } } } /** * @description Initializes the dispatcher and subscribes to database changes * @summary Sets up the continuous changes feed subscription to the Nano database * and handles reconnection attempts if the connection fails * @return {Promise<void>} A promise that resolves when the subscription is established * @mermaid * sequenceDiagram * participant D as NanoDispatch * participant S as subscribeToCouch * participant DB as Nano Database * participant L as Logger * D->>S: Call subscribeToCouch * S->>S: Check adapter and native * alt No adapter or native * S-->>S: throw InternalError * end * S->>DB: changes(options, changeHandler) * alt Success * DB-->>S: Subscription established * S-->>D: Promise resolves * D->>L: Log successful subscription * else Error * DB-->>S: Error * S->>S: Increment attemptCounter * alt attemptCounter > 3 * S->>L: Log error * S-->>D: Promise rejects * else attemptCounter <= 3 * S->>L: Log retry * S->>S: Wait timeout * S->>S: Recursive call to subscribeToCouch * end * end */ async initialize() { const log = this.log.for(this.initialize); const subLog = log.for(subscribeToCouch); async function subscribeToCouch() { if (!this.adapter || !this.native) throw new InternalError(`No adapter/native observed for dispatch`); try { this.native.changes({ feed: "continuous", include_docs: false, since: this.observerLastUpdate || "now", timeout: this.timeout, }, this.changeHandler.bind(this)); } catch (e) { if (++this.attemptCounter > 3) return subLog.error(`Failed to subscribe to couchdb changes: ${e}`); subLog.info(`Failed to subscribe to couchdb changes: ${e}. Retrying in 5 seconds...`); await new Promise((resolve) => setTimeout(resolve, this.timeout)); return subscribeToCouch.call(this); } } subscribeToCouch .call(this) .then(() => { this.log.info(`Subscribed to couchdb changes`); }) .catch((e) => { throw new InternalError(`Failed to subscribe to couchdb changes: ${e}`); }); } } /** * @description Sets the creator or updater field in a model based on the user in the context * @summary Callback function used in decorators to automatically set the created_by or updated_by fields * with the username from the context when a document is created or updated * @template M - Type extending Model * @template R - Type extending NanoRepository<M> * @template V - Type extending RelationsMetadata * @param {R} this - The repository instance * @param {Context<NanoFlags>} context - The operation context containing user information * @param {V} data - The relation metadata * @param key - The property key to set with the username * @param {M} model - The model instance being created or updated * @return {Promise<void>} A promise that resolves when the operation is complete * @function createdByOnNanoCreateUpdate * @memberOf module:for-nano * @mermaid * sequenceDiagram * participant F as createdByOnNanoCreateUpdate * participant C as Context * participant M as Model * F->>C: get("user") * C-->>F: user object * F->>M: set key to user.name * Note over F: If no user in context * F-->>F: throw UnsupportedError */ async function createdByOnNanoCreateUpdate(context, data, key, model) { try { const user = context.get("user"); model[key] = user.name; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new UnsupportedError("No User found in context. Please provide a user in the context"); } } /** * @description Adapter for interacting with Nano databases * @summary Provides a standardized interface for performing CRUD operations on Nano databases, * extending the CouchDB adapter with Nano-specific functionality. This adapter handles document * creation, reading, updating, and deletion, as well as bulk operations and index management. * @template DocumentScope - The Nano document scope type * @template NanoFlags - Configuration flags for Nano operations * @template Context - Context type for operations * @param {DocumentScope<any>} scope - The Nano document scope to use for database operations * @param {string} [alias] - Optional alias for the adapter * @class NanoAdapter * @example * ```typescript * // Connect to a Nano database * const server = NanoAdapter.connect('admin', 'password', 'localhost:5984'); * const db = server.db.use('my_database'); * * // Create an adapter instance * const adapter = new NanoAdapter(db); * * // Use the adapter for database operations * const document = await adapter.read('users', '123'); * ``` * @mermaid * classDiagram * class CouchDBAdapter { * +flags() * +Dispatch() * +index() * +create() * +read() * +update() * +delete() * } * class NanoAdapter { * +flags() * +Dispatch() * +index() * +create() * +createAll() * +read() * +readAll() * +update() * +updateAll() * +delete() * +deleteAll() * +raw() * +static connect() * +static createDatabase() * +static deleteDatabase() * +static createUser() * +static deleteUser() * +static decoration() * } * CouchDBAdapter <|-- NanoAdapter */ class NanoAdapter extends CouchDBAdapter { constructor(scope, alias) { super(scope, NanoFlavour, alias); } /** * @description Generates flags for database operations * @summary Creates a set of flags for a specific operation, including user information * @template M - Type extending Model * @param {OperationKeys} operation - The operation being performed (create, read, update, delete) * @param {Constructor<M>} model - The model constructor * @param {Partial<NanoFlags>} flags - Partial flags to be merged * @return {Promise<NanoFlags>} Complete flags for the operation */ async flags(operation, model, flags) { return Object.assign(await super.flags(operation, model, flags), { user: { name: this.native.config.url.split("@")[0].split(":")[0], }, }); } /** * @description Creates a new NanoDispatch instance * @summary Returns a dispatcher for handling Nano-specific operations * @return {NanoDispatch} A new NanoDispatch instance */ Dispatch() { return new NanoDispatch(); } /** * @description Creates database indexes for models * @summary Generates and creates indexes in the Nano database based on the provided models * @template M - Type extending Model * @param models - Model constructors to create indexes for * @return {Promise<void>} A promise that resolves when all indexes are created * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant G as generateIndexes * participant DB as Nano Database * A->>G: generateIndexes(models) * G-->>A: indexes * loop For each index * A->>DB: createIndex(index) * DB-->>A: response * Note over A: Check if index already exists * alt Index exists * A-->>A: throw ConflictError * end * end */ async index(...models) { const indexes = generateIndexes(models); for (const index of indexes) { const res = await this.native.createIndex(index); const { result, id, name } = res; if (result === "existing") throw new ConflictError(`Index for table ${name} with id ${id}`); } } /** * @description Creates a new document in the database * @summary Inserts a new document into the Nano database with the provided data * @param {string} tableName - The name of the table/collection * @param {string | number} id - The document identifier * @param {Record<string, any>} model - The document data to insert * @return {Promise<Record<string, any>>} A promise that resolves to the created document with metadata * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant DB as Nano Database * A->>DB: insert(model) * alt Success * DB-->>A: response with ok=true * A->>A: assignMetadata(model, response.rev) * A-->>A: return document with metadata * else Error * DB-->>A: error * A-->>A: throw parseError(e) * else Not OK * DB-->>A: response with ok=false * A-->>A: throw InternalError * end */ async create(tableName, id, model) { let response; try { response = await this.native.insert(model); } catch (e) { throw this.parseError(e); } if (!response.ok) throw new InternalError(`Failed to insert doc id: ${id} in table ${tableName}`); return this.assignMetadata(model, response.rev); } /** * @description Creates multiple documents in the database * @summary Inserts multiple documents into the Nano database in a single bulk operation * @param {string} tableName - The name of the table/collection * @param {string[] | number[]} ids - Array of document identifiers * @param models - Array of document data to insert * @return A promise that resolves to an array of created documents with metadata * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant DB as Nano Database * A->>DB: bulk({docs: models}) * alt Success * DB-->>A: response array * A->>A: Check if all responses have no errors * alt All OK * A->>A: assignMultipleMetadata(models, revs) * A-->>A: return documents with metadata * else Some errors * A->>A: Collect error messages * A-->>A: throw InternalError with collected messages * end * else Error * DB-->>A: error * A-->>A: throw parseError(e) * end */ async createAll(tableName, ids, models) { let response; try { response = await this.native.bulk({ docs: models }); } catch (e) { throw this.parseError(e); } if (!response.every((r) => !r.error)) { const errors = response.reduce((accum, el, i) => { if (el.error) accum.push(`el ${i}: ${el.error}${el.reason ? ` - ${el.reason}` : ""}`); return accum; }, []); throw new InternalError(errors.join("\n")); } return this.assignMultipleMetadata(models, response.map((r) => r.rev)); } /** * @description Retrieves a document from the database * @summary Fetches a single document from the Nano database by its ID * @param {string} tableName - The name of the table/collection * @param {string | number} id - The document identifier * @return {Promise<Record<string, any>>} A promise that resolves to the retrieved document with metadata * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant DB as Nano Database * A->>A: generateId(tableName, id) * A->>DB: get(_id) * alt Success * DB-->>A: record * A->>A: assignMetadata(record, record._rev) * A-->>A: return document with metadata * else Error * DB-->>A: error * A-->>A: throw parseError(e) * end */ async read(tableName, id) { const _id = this.generateId(tableName, id); let record; try { record = await this.native.get(_id); } catch (e) { throw this.parseError(e); } return this.assignMetadata(record, record._rev); } /** * @description Retrieves multiple documents from the database * @summary Fetches multiple documents from the Nano database by their IDs in a single operation * @param {string} tableName - The name of the table/collection * @param {Array<string | number | bigint>} ids - Array of document identifiers * @return A promise that resolves to an array of retrieved documents with metadata * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant DB as Nano Database * A->>A: Map ids to generateId(tableName, id) * A->>DB: fetch({keys: mappedIds}, {}) * DB-->>A: results * A->>A: Process each result row * loop For each row * alt Row has error * A-->>A: throw InternalError * else Row has document * A->>A: assignMetadata(doc, doc._rev) * else No document * A-->>A: throw InternalError * end * end * A-->>A: return documents with metadata */ async readAll(tableName, ids) { const results = await this.native.fetch({ keys: ids.map((id) => this.generateId(tableName, id)) }, {}); return results.rows.map((r) => { if (r.error) throw new InternalError(r.error); if (r.doc) { const res = Object.assign({}, r.doc); return this.assignMetadata(res, r.doc[CouchDBKeys.REV]); } throw new InternalError("Should be impossible"); }); } /** * @description Updates a document in the database * @summary Updates an existing document in the Nano database with the provided data * @param {string} tableName - The name of the table/collection * @param {string | number} id - The document identifier * @param {Record<string, any>} model - The updated document data * @return {Promise<Record<string, any>>} A promise that resolves to the updated document with metadata * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant DB as Nano Database * A->>DB: insert(model) * alt Success * DB-->>A: response with ok=true * A->>A: assignMetadata(model, response.rev) * A-->>A: return document with metadata * else Error * DB-->>A: error * A-->>A: throw parseError(e) * else Not OK * DB-->>A: response with ok=false * A-->>A: throw InternalError * end */ async update(tableName, id, model) { let response; try { response = await this.native.insert(model); } catch (e) { throw this.parseError(e); } if (!response.ok) throw new InternalError(`Failed to update doc id: ${id} in table ${tableName}`); return this.assignMetadata(model, response.rev); } async updateAll(tableName, ids, models) { let response; try { response = await this.native.bulk({ docs: models }); } catch (e) { throw this.parseError(e); } if (!response.every((r) => !r.error)) { const errors = response.reduce((accum, el, i) => { if (el.error) accum.push(`el ${i}: ${el.error}${el.reason ? ` - ${el.reason}` : ""}`); return accum; }, []); throw new InternalError(errors.join("\n")); } return this.assignMultipleMetadata(models, response.map((r) => r.rev)); } async delete(tableName, id) { const _id = this.generateId(tableName, id); let record; try { record = await this.native.get(_id); await this.native.destroy(_id, record._rev); } catch (e) { throw this.parseError(e); } return this.assignMetadata(record, record._rev); } async deleteAll(tableName, ids) { const results = await this.native.fetch({ keys: ids.map((id) => this.generateId(tableName, id)) }, {}); const deletion = await this.native.bulk({ docs: results.rows.map((r) => { r[CouchDBKeys.DELETED] = true; return r; }), }); deletion.forEach((d) => { if (d.error) console.error(d.error); }); return results.rows.map((r) => { if (r.error) throw new InternalError(r.error); if (r.doc) { const res = Object.assign({}, r.doc); return this.assignMetadata(res, r.doc[CouchDBKeys.REV]); } throw new InternalError("Should be impossible"); }); } async raw(rawInput, docsOnly = true) { try { const response = await this.native.find(rawInput); if (response.warning) console.warn(response.warning); if (docsOnly) return response.docs; return response; } catch (e) { throw this.parseError(e); } } static connect(user, pass, host = "localhost:5984", protocol = "http") { return Nano(`${protocol}://${user}:${pass}@${host}`); } /** * @description Creates a new database on the Nano server * @summary Creates a new database with the specified name on the connected Nano server * @param {ServerScope} con - The Nano server connection * @param {string} name - The name of the database to create * @return {Promise<void>} A promise that resolves when the database is created * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant DB as Nano Server * A->>DB: db.create(name) * alt Success * DB-->>A: result with ok=true * else Error * DB-->>A: error * A-->>A: throw parseError(e) * else Not OK * DB-->>A: result with ok=false * A-->>A: throw parseError(error, reason) * end */ static async createDatabase(con, name) { let result; try { result = await con.db.create(name); } catch (e) { throw CouchDBAdapter.parseError(e); } const { ok, error, reason } = result; if (!ok) throw CouchDBAdapter.parseError(error, reason); } /** * @description Deletes a database from the Nano server * @summary Removes an existing database with the specified name from the connected Nano server * @param {ServerScope} con - The Nano server connection * @param {string} name - The name of the database to delete * @return {Promise<void>} A promise that resolves when the database is deleted * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant DB as Nano Server * A->>DB: db.destroy(name) * alt Success * DB-->>A: result with ok=true * else Error * DB-->>A: error * A-->>A: throw parseError(e) * else Not OK * DB-->>A: result with ok=false * A-->>A: throw InternalError * end */ static async deleteDatabase(con, name) { let result; try { result = await con.db.destroy(name); } catch (e) { throw CouchDBAdapter.parseError(e); } const { ok } = result; if (!ok) throw new InternalError(`Failed to delete database with name ${name}`); } /** * @description Creates a new user and grants access to a database * @summary Creates a new user in the Nano server and configures security to grant the user access to a specific database * @param {ServerScope} con - The Nano server connection * @param {string} dbName - The name of the database to grant access to * @param {string} user - The username to create * @param {string} pass - The password for the new user * @param {string[]} [roles=["reader", "writer"]] - The roles to assign to the user * @return {Promise<void>} A promise that resolves when the user is created and granted access * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant U as _users Database * participant S as Security API * A->>A: Create user object * A->>U: insert(user) * alt Success * U-->>A: response with ok=true * A->>S: PUT _security with user permissions * alt Security Success * S-->>A: security response with ok=true * else Security Failure * S-->>A: security response with ok=false * A-->>A: throw InternalError * end * else Error * U-->>A: error * A-->>A: throw parseError(e) * else Not OK * U-->>A: response with ok=false * A-->>A: throw InternalError * end */ static async createUser(con, dbName, user, pass, roles = ["reader", "writer"]) { const users = con.db.use("_users"); const usr = { _id: "org.couchdb.user:" + user, name: user, password: pass, roles: roles, type: "user", }; try { const created = await users.insert(usr); const { ok } = created; if (!ok) throw new InternalError(`Failed to create user ${user}`); const security = await con.request({ db: dbName, method: "put", path: "_security", // headers: { // // }, body: { admins: { names: [user], roles: [], }, members: { names: [user], roles: roles, }, }, }); if (!security.ok) throw new InternalError(`Failed to authorize user ${user} to db ${dbName}`); } catch (e) { throw CouchDBAdapter.parseError(e); } } /** * @description Deletes a user from the Nano server * @summary Removes an existing user from the Nano server * @param {ServerScope} con - The Nano server connection * @param {string} dbName - The name of the database (used for logging purposes) * @param {string} user - The username to delete * @return {Promise<void>} A promise that resolves when the user is deleted * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant U as _users Database * A->>A: Generate user ID * A->>U: get(id) * U-->>A: user document * A->>U: destroy(id, user._rev) * alt Success * U-->>A: success response * else Error * U-->>A: error * A-->>A: throw parseError(e) * end */ static async deleteUser(con, dbName, user) { const users = con.db.use("_users"); const id = "org.couchdb.user:" + user; try { const usr = await users.get(id); await users.destroy(id, usr._rev); } catch (e) { throw CouchDBAdapter.parseError(e); } } /** * @description Sets up decorations for Nano-specific model properties * @summary Configures decorators for created_by and updated_by fields in models to be automatically * populated with the user from the context when documents are created or updated * @return {void} * @mermaid * sequenceDiagram * participant A as NanoAdapter * participant D as Decoration * participant R as Repository * A->>R: key(PersistenceKeys.CREATED_BY) * R-->>A: createdByKey * A->>D: flavouredAs("nano") * A->>D: for(createdByKey) * A->>D: define(onCreate(createdByOnNanoCreateUpdate), propMetadata) * A->>D: apply() * A->>R: key(PersistenceKeys.UPDATED_BY) * R-->>A: updatedByKey * A->>D: flavouredAs("nano") * A->>D: for(updatedByKey) * A->>D: define(onCreate(createdByOnNanoCreateUpdate), propMetadata) * A->>D: apply() */ static decoration() { const createdByKey = Repository.key(PersistenceKeys.CREATED_BY); const updatedByKey = Repository.key(PersistenceKeys.UPDATED_BY); Decoration.flavouredAs("nano") .for(createdByKey) .define(onCreate(createdByOnNanoCreateUpdate), propMetadata(createdByKey, {})) .apply(); Decoration.flavouredAs("nano") .for(updatedByKey) .define(onCreate(createdByOnNanoCreateUpdate), propMetadata(updatedByKey, {})) .apply(); } } // Forces override for Nano Decoration NanoAdapter.decoration(); /** * @description A TypeScript module for interacting with Nano databases * @summary This module provides a set of utilities, classes, and types for working with Nano databases. It includes repository patterns, adapters, and type definitions to simplify database operations. * @module for-nano */ /** * @description Package version identifier * @summary Stores the current package version string for the for-nano module * @const VERSION * @memberOf module:for-nano */ const VERSION = "0.1.6"; export { NanoAdapter, NanoFlavour, VERSION, createdByOnNanoCreateUpdate }; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9yLW5hbm8uZXNtLmNqcyIsInNvdXJjZXMiOlsiLi4vc3JjL2NvbnN0YW50cy50cyIsIi4uL3NyYy9OYW5vRGlzcGF0Y2gudHMiLCIuLi9zcmMvYWRhcHRlci50cyIsIi4uL3NyYy9pbmRleC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEBkZXNjcmlwdGlvbiBJZGVudGlmaWVyIGZvciB0aGUgTmFubyBkYXRhYmFzZSBmbGF2b3JcbiAqIEBzdW1tYXJ5IENvbnN0YW50IHN0cmluZyB0aGF0IGlkZW50aWZpZXMgdGhlIGRhdGFiYXNlIHR5cGUgYXMgXCJuYW5vXCIgZm9yIHVzZSBpbiBhZGFwdGVyIHNlbGVjdGlvbiBhbmQgY29uZmlndXJhdGlvblxuICogQGNvbnN0IE5hbm9GbGF2b3VyXG4gKiBAbWVtYmVyT2YgbW9kdWxlOmZvci1uYW5vXG4gKi9cbmV4cG9ydCBjb25zdCBOYW5vRmxhdm91ciA9IFwibmFub1wiO1xuIiwiaW1wb3J0IHsgRGlzcGF0Y2ggfSBmcm9tIFwiQGRlY2FmLXRzL2NvcmVcIjtcbmltcG9ydCB7XG4gIERhdGFiYXNlQ2hhbmdlc1Jlc3BvbnNlLFxuICBEYXRhYmFzZUNoYW5nZXNSZXN1bHRJdGVtLFxuICBEb2N1bWVudFNjb3BlLFxuICBSZXF1ZXN0RXJyb3IsXG59IGZyb20gXCJuYW5vXCI7XG5pbXBvcnQgeyBJbnRlcm5hbEVycm9yLCBPcGVyYXRpb25LZXlzIH0gZnJvbSBcIkBkZWNhZi10cy9kYi1kZWNvcmF0b3JzXCI7XG5pbXBvcnQgeyBDb3VjaERCS2V5cyB9IGZyb20gXCJAZGVjYWYtdHMvZm9yLWNvdWNoZGJcIjtcblxuLyoqXG4gKiBAZGVzY3JpcHRpb24gRGlzcGF0Y2hlciBmb3IgTmFubyBkYXRhYmFzZSBjaGFuZ2UgZXZlbnRzXG4gKiBAc3VtbWFyeSBIYW5kbGVzIHRoZSBzdWJzY3JpcHRpb24gdG8gYW5kIHByb2Nlc3Npbmcgb2YgZGF0YWJhc2UgY2hhbmdlIGV2ZW50cyBmcm9tIGEgTmFubyBkYXRhYmFzZSxcbiAqIG5vdGlmeWluZyBvYnNlcnZlcnMgd2hlbiBkb2N1bWVudHMgYXJlIGNyZWF0ZWQsIHVwZGF0ZWQsIG9yIGRlbGV0ZWRcbiAqIEB0ZW1wbGF0ZSBEb2N1bWVudFNjb3BlIC0gVGhlIE5hbm8gZG9jdW1lbnQgc2NvcGUgdHlwZVxuICogQHBhcmFtIHtudW1iZXJ9IFt0aW1lb3V0PTUwMDBdIC0gVGltZW91dCBpbiBtaWxsaXNlY29uZHMgZm9yIGNoYW5nZSBmZWVkIHJlcXVlc3RzXG4gKiBAY2xhc3MgTmFub0Rpc3BhdGNoXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogLy8gQ3JlYXRlIGEgZGlzcGF0Y2hlciBmb3IgYSBOYW5vIGRhdGFiYXNlXG4gKiBjb25zdCBkYiA9IHNlcnZlci5kYi51c2UoJ215X2RhdGFiYXNlJyk7XG4gKiBjb25zdCBhZGFwdGVyID0gbmV3IE5hbm9BZGFwdGVyKGRiKTtcbiAqIGNvbnN0IGRpc3BhdGNoID0gbmV3IE5hbm9EaXNwYXRjaCgpO1xuICpcbiAqIC8vIFRoZSBkaXNwYXRjaGVyIHdpbGwgYXV0b21hdGljYWxseSBzdWJzY3JpYmUgdG8gY2hhbmdlc1xuICogLy8gYW5kIG5vdGlmeSBvYnNlcnZlcnMgd2hlbiBkb2N1bWVudHMgY2hhbmdlXG4gKiBgYGBcbiAqIEBtZXJtYWlkXG4gKiBjbGFzc0RpYWdyYW1cbiAqICAgY2xhc3MgRGlzcGF0Y2gge1xuICogICAgICtpbml0aWFsaXplKClcbiAqICAgICArdXBkYXRlT2JzZXJ2ZXJzKClcbiAqICAgfVxuICogICBjbGFzcyBOYW5vRGlzcGF0Y2gge1xuICogICAgIC1vYnNlcnZlckxhc3RVcGRhdGU/OiBzdHJpbmdcbiAqICAgICAtYXR0ZW1wdENvdW50ZXI6IG51bWJlclxuICogICAgIC10aW1lb3V0OiBudW1iZXJcbiAqICAgICArY29uc3RydWN0b3IodGltZW91dClcbiAqICAgICAjY2hhbmdlSGFuZGxlcigpXG4gKiAgICAgI2luaXRpYWxpemUoKVxuICogICB9XG4gKiAgIERpc3BhdGNoIDx8LS0gTmFub0Rpc3BhdGNoXG4gKi9cbmV4cG9ydCBjbGFzcyBOYW5vRGlzcGF0Y2ggZXh0ZW5kcyBEaXNwYXRjaDxEb2N1bWVudFNjb3BlPGFueT4+IHtcbiAgcHJpdmF0ZSBvYnNlcnZlckxhc3RVcGRhdGU/OiBzdHJpbmc7XG4gIHByaXZhdGUgYXR0ZW1wdENvdW50ZXI6IG51bWJlciA9IDA7XG4gIGNvbnN0cnVjdG9yKHByaXZhdGUgdGltZW91dCA9IDUwMDApIHtcbiAgICBzdXBlcigpO1xuICB9XG5cbiAgLyoqXG4gICAqIEBkZXNjcmlwdGlvbiBQcm9jZXNzZXMgZGF0YWJhc2UgY2hhbmdlIGV2ZW50c1xuICAgKiBAc3VtbWFyeSBIYW5kbGVzIHRoZSByZXNwb25zZSBmcm9tIHRoZSBOYW5vIGNoYW5nZXMgZmVlZCwgcHJvY2Vzc2VzIHRoZSBjaGFuZ2VzLFxuICAgKiBhbmQgbm90aWZpZXMgb2JzZXJ2ZXJzIGFib3V0IGRvY3VtZW50IGNoYW5nZXNcbiAgICogQHBhcmFtIHtSZXF1ZXN0RXJyb3IgfCBudWxsfSBlcnJvciAtIEVycm9yIG9iamVjdCBpZiB0aGUgcmVxdWVzdCBmYWlsZWRcbiAgICogQHBhcmFtIHJlc3BvbnNlIC0gVGhlIGNoYW5nZXMgcmVzcG9uc2UgZnJvbSBOYW5vXG4gICAqIEBwYXJhbSB7YW55fSBbaGVhZGVyc10gLSBSZXNwb25zZSBoZWFkZXJzICh1bnVzZWQpXG4gICAqIEByZXR1cm4ge1Byb21pc2U8dm9pZD59IEEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdoZW4gYWxsIGNoYW5nZXMgaGF2ZSBiZWVuIHByb2Nlc3NlZFxuICAgKiBAbWVybWFpZFxuICAgKiBzZXF1ZW5jZURpYWdyYW1cbiAgICogICBwYXJ0aWNpcGFudCBEIGFzIE5hbm9EaXNwYXRjaFxuICAgKiAgIHBhcnRpY2lwYW50IEwgYXMgTG9nZ2VyXG4gICAqICAgcGFydGljaXBhbnQgTyBhcyBPYnNlcnZlcnNcbiAgICogICBOb3RlIG92ZXIgRDogUmVjZWl2ZSBjaGFuZ2VzIGZyb20gTmFub1xuICAgKiAgIGFsdCBFcnJvciBpbiByZXNwb25zZVxuICAgKiAgICAgRC0+Pkw6IExvZyBlcnJvclxuICAgKiAgICAgRC0tPj5EOiBSZXR1cm4gZWFybHlcbiAgICogICBlbmRcbiAgICogICBhbHQgUmVzcG9uc2UgaXMgc3RyaW5nXG4gICAqICAgICBELT4+RDogUGFyc2UgSlNPTiBmcm9tIHN0cmluZ1xuICAgKiAgIGVuZFxuICAgKiAgIEQtPj5EOiBQcm9jZXNzIGNoYW5nZXNcbiAgICogICBELT4+RDogR3JvdXAgY2hhbmdlcyBieSB0YWJsZSBhbmQgb3BlcmF0aW9uXG4gICAqICAgbG9vcCBGb3IgZWFjaCB0YWJsZVxuICAgKiAgICAgbG9vcCBGb3IgZWFjaCBvcGVyYXRpb25cbiAgICogICAgICAgRC0+Pk86IHVwZGF0ZU9ic2VydmVycyh0YWJsZSwgb3BlcmF0aW9uLCBpZHMpXG4gICAqICAgICAgIEQtPj5EOiBVcGRhdGUgb2JzZXJ2ZXJMYXN0VXBkYXRlXG4gICAqICAgICAgIEQtPj5MOiBMb2cgc3VjY2Vzc2Z1bCBkaXNwYXRjaFxuICAgKiAgICAgZW5kXG4gICAqICAgZW5kXG4gICAqL1xuICBwcm90ZWN0ZWQgYXN5bmMgY2hhbmdlSGFuZGxlcihcbiAgICBlcnJvcjogUmVxdWVzdEVycm9yIHwgbnVsbCxcbiAgICByZXNwb25zZTogKERhdGFiYXNlQ2hhbmdlc1Jlc3BvbnNlIHwgRGF0YWJhc2VDaGFuZ2VzUmVzdWx0SXRlbSlbXSB8IHN0cmluZyxcbiAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgQHR5cGVzY3JpcHQtZXNsaW50L25vLXVudXNlZC12YXJzXG4gICAgaGVhZGVycz86IGFueVxuICApIHtcbiAgICBjb25zdCBsb2cgPSB0aGlzLmxvZy5mb3IodGhpcy5jaGFuZ2VIYW5kbGVyKTtcbiAgICBpZiAoZXJyb3IpIHJldHVybiBsb2cuZXJyb3IoYEVycm9yIGluIGNoYW5nZSByZXF1ZXN0OiAke2Vycm9yfWApO1xuICAgIHRyeSB7XG4gICAgICByZXNwb25zZSA9IChcbiAgICAgICAgdHlwZW9mIHJlc3BvbnNlID09PSBcInN0cmluZ1wiXG4gICAgICAgICAgPyByZXNwb25zZVxuICAgICAgICAgICAgICAuc3BsaXQoXCJcXG5cIilcbiAgICAgICAgICAgICAgLmZpbHRlcigocikgPT4gISFyKVxuICAgICAgICAgICAgICAubWFwKChyKSA9PiBKU09OLnBhcnNlKHIpKVxuICAgICAgICAgIDogcmVzcG9uc2VcbiAgICAgICkgYXMgRGF0YWJhc2VDaGFuZ2VzUmVzcG9uc2VbXTtcbiAgICB9IGNhdGNoIChlOiB1bmtub3duKSB7XG4gICAgICByZXR1cm4gbG9nLmVycm9yKGBFcnJvciBwYXJzaW5nIGNvdWNoZGIgY2hhbmdlIGZlZWQ6ICR7ZX1gKTtcbiAgICB9XG4gICAgY29uc3QgY291bnQgPSByZXNwb25zZS5sZW5ndGg7XG4gICAgaWYgKGNvdW50ID4gMCkge1xuICAgICAgbG9nLmRlYnVnKGBSZWNlaXZlZCAke2NvdW50fSBjaGFuZ2VzLiBwcm9jZXNzaW5nLi4uYCk7XG4gICAgICBjb25zdCBjaGFuZ2VzID0gcmVzcG9uc2VcbiAgICAgICAgLm1hcCgocmVjLCBpKSA9PiB7XG4gICAgICAgICAgaWYgKGkgPT09IGNvdW50IC0gMSkge1xuICAgICAgICAgICAgaWYgKFxuICAgICAgICAgICAgICB0aGlzLm9ic2VydmVyTGFzdFVwZGF0ZSA9PT1cbiAgICAgICAgICAgICAgKHJlYyBhcyBEYXRhYmFzZUNoYW5nZXNSZXNwb25zZSkubGFzdF9zZXFcbiAgICAgICAgICAgIClcbiAgICAgICAgICAgICAgbG9nLmVycm9yKFxuICAgICAgICAgICAgICAgIGBJbnZhbGlkIGxhc3QgdXBkYXRlIGNoZWNrOiAke3RoaXMub2JzZXJ2ZXJMYXN0VXBkYXRlfSAhPT0gJHsocmVjIGFzIERhdGFiYXNlQ2hhbmdlc1Jlc3BvbnNlKS5sYXN0X3NlcX1gXG4gICAgICAgICAgICAgICk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgfVxuICAgICAgICAgIGNvbnN0IHIgPSByZWMgYXMgRGF0YWJhc2VDaGFuZ2VzUmVzdWx0SXRlbTtcbiAgICAgICAgICBjb25zdCBbdGFibGUsIGlkXSA9IHIuaWQuc3BsaXQoQ291Y2hEQktleXMuU0VQQVJBVE9SKTtcbiAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgdGFibGU6IHRhYmxlLFxuICAgICAgICAgICAgaWQ6IGlkLFxuICAgICAgICAgICAgb3BlcmF0aW9uOiByLmRlbGV0ZWRcbiAgICAgICAgICAgICAgPyBPcGVyYXRpb25LZXlzLkRFTEVURVxuICAgICAgICAgICAgICA6IHIuY2hhbmdlc1tyLmNoYW5nZXMubGVuZ3RoIC0gMV0ucmV2LnNwbGl0KFwiLVwiKVswXSA9PT0gXCIxXCJcbiAgICAgICAgICAgICAgICA/IE9wZXJhdGlvbktleXMuQ1JFQVRFXG4gICAgICAgICAgICAgICAgOiBPcGVyYXRpb25LZXlzLlVQREFURSxcbiAgICAgICAgICAgIHN0ZXA6IHIuY2hhbmdlc1tyLmNoYW5nZXMubGVuZ3RoIC0gMV0ucmV2LFxuICAgICAgICAgIH07XG4gICAgICAgIH0pXG4gICAgICAgIC5yZWR1Y2UoXG4gICAgICAgICAgKFxuICAgICAgICAgICAgYWNjdW06IFJlY29yZDxcbiAgICAgICAgICAgICAgc3RyaW5nLFxuICAgICAgICAgICAgICBSZWNvcmQ8XG4gICAgICAgICAgICAgICAgc3RyaW5nLFxuICAgICAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICAgIGlkczogU2V0PGFueT47XG4gICAgICAgICAgICAgICAgICBzdGVwOiBzdHJpbmc7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICA+XG4gICAgICAgICAgICA+LFxuICAgICAgICAgICAgclxuICAgICAgICAgICkgPT4ge1xuICAgICAgICAgICAgaWYgKCFyKSByZXR1cm4gYWNjdW07XG4gICAgICAgICAgICBjb25zdCB7IHRhYmxlLCBpZCwgb3BlcmF0aW9uLCBzdGVwIH0gPSByIGFzIHtcbiAgICAgICAgICAgICAgdGFibGU6IHN0cmluZztcbiAgICAgICAgICAgICAgaWQ6IHN0cmluZztcbiAgICAgICAgICAgICAgb3BlcmF0aW9uOiBPcGVyYXRpb25LZXlzO1xuICAgICAgICAgICAgICBzdGVwOiBzdHJpbmc7XG4gICAgICAgICAgICB9O1xuICAgICAgICAgICAgaWYgKCFhY2N1bVt0YWJsZV0pIGFjY3VtW3RhYmxlXSA9IHt9O1xuICAgICAgICAgICAgaWYgKCFhY2N1bVt0YWJsZV1bb3BlcmF0aW9uXSlcbiAgICAgICAgICAgICAgYWNjdW1bdGFibGVdW29wZXJhdGlvbl0gPSB7IGlkczogbmV3IFNldCgpLCBzdGVwOiBzdGVwIH07XG4gICAgICAgICAgICBhY2N1bVt0YWJsZV1bb3BlcmF0aW9uXS5pZHMuYWRkKGlkKTtcbiAgICAgICAgICAgIGFjY3VtW3RhYmxlXVtvcGVyYXRpb25dLnN0ZXAgPSBzdGVwO1xuICAgICAgICAgICAgcmV0dXJuIGFjY3VtO1xuICAgICAgICAgIH0sXG4gICAgICAgICAge31cbiAgICAgICAgKTtcblxuICAgICAgZm9yIChjb25zdCB0YWJsZSBvZiBPYmplY3Qua2V5cyhjaGFuZ2VzKSkge1xuICAgICAgICBmb3IgKGNvbnN0IG9wIG9mIE9iamVjdC5rZXlzKGNoYW5nZXNbdGFibGVdKSkge1xuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICBhd2FpdCB0aGlzLnVwZGF0ZU9ic2VydmVycyh0YWJsZSwgb3AsIFtcbiAgICAgICAgICAgICAgLi4uY2hhbmdlc1t0YWJsZV1bb3BdLmlkcy52YWx1ZXMoKSxcbiAgICAgICAgICAgIF0pO1xuICAgICAgICAgICAgdGhpcy5vYnNlcnZlckxhc3RVcGRhdGUgPSBjaGFuZ2VzW3RhYmxlXVtvcF0uc3RlcDtcbiAgICAgICAgICAgIGxvZy52ZXJib3NlKGBPYnNlcnZlciByZWZyZXNoIGRpc3BhdGNoZWQgYnkgJHtvcH0gZm9yICR7dGFibGV9YCk7XG4gICAgICAgICAgICBsb2cuZGVidWcoYHBrczogJHtBcnJheS5mcm9tKGNoYW5nZXNbdGFibGVdW29wXS5pZHMudmFsdWVzKCkpfWApO1xuICAgICAgICAgIH0gY2F0Y2ggKGU6IHVua25vd24pIHtcbiAgICAgICAgICAgIGxvZy5lcnJvcihcbiAgICAgICAgICAgICAgYEZhaWxlZCB0byBkaXNwYXRjaCBvYnNlcnZlciByZWZyZXNoIGZvciAke3RhYmxlfSwgb3AgJHtvcH06ICR7ZX1gXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBAZGVzY3JpcHRpb24gSW5pdGlhbGl6ZXMgdGhlIGRpc3BhdGNoZXIgYW5kIHN1YnNjcmliZXMgdG8gZGF0YWJhc2UgY2hhbmdlc1xuICAgKiBAc3VtbWFyeSBTZXRzIHVwIHRoZSBjb250aW51b3VzIGNoYW5nZXMgZmVlZCBzdWJzY3JpcHRpb24gdG8gdGhlIE5hbm8gZGF0YWJhc2VcbiAgICogYW5kIGhhbmRsZXMgcmVjb25uZWN0aW9uIGF0dGVtcHRzIGlmIHRoZSBjb25uZWN0aW9uIGZhaWxzXG4gICAqIEByZXR1cm4ge1Byb21pc2U8dm9pZD59IEEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdoZW4gdGhlIHN1YnNjcmlwdGlvbiBpcyBlc3RhYmxpc2hlZFxuICAgKiBAbWVybWFpZFxuICAgKiBzZXF1ZW5jZURpYWdyYW1cbiAgICogICBwYXJ0aWNpcGFudCBEIGFzIE5hbm9EaXNwYXRjaFxuICAgKiAgIHBhcnRpY2lwYW50IFMgYXMgc3Vic2NyaWJlVG9Db3VjaFxuICAgKiAgIHBhcnRpY2lwYW50IERCIGFzIE5hbm8gRGF0YWJhc2VcbiAgICogICBwYXJ0aWNpcGFudCBMIGFzIExvZ2dlclxuICAgKiAgIEQtPj5TOiBDYWxsIHN1YnNjcmliZVRvQ291Y2hcbiAgICogICBTLT4+UzogQ2hlY2sgYWRhcHRlciBhbmQgbmF0aXZlXG4gICAqICAgYWx0IE5vIGFkYXB0ZXIgb3IgbmF0aXZlXG4gICAqICAgICBTLS0+PlM6IHRocm93IEludGVybmFsRXJyb3JcbiAgICogICBlbmRcbiAgICogICBTLT4+REI6IGNoYW5nZXMob3B0aW9ucywgY2hhbmdlSGFuZGxlcilcbiAgICogICBhbHQgU3VjY2Vzc1xuICAgKiAgICAgREItLT4+UzogU3Vic2NyaXB0aW9uIGVzdGFibGlzaGVkXG4gICAqICAgICBTLS0+PkQ6IFByb21pc2UgcmVzb2x2ZXNcbiAgICogICAgIEQtPj5MOiBMb2cgc3VjY2Vzc2Z1bCBzdWJzY3JpcHRpb25cbiAgICogICBlbHNlIEVycm9yXG4gICAqICAgICBEQi0tPj5TOiBFcnJvclxuICAgKiAgICAgUy0+PlM6IEluY3JlbWVudCBhdHRlbXB0Q291bnRlclxuICAgKiAgICAgYWx0IGF0dGVtcHRDb3VudGVyID4gM1xuICAgKiAgICAgICBTLT4+TDogTG9nIGVycm9yXG4gICAqICAgICAgIFMtLT4+RDogUHJvbWlzZSByZWplY3RzXG4gICAqICAgICBlbHNlIGF0dGVtcHRDb3VudGVyIDw9IDNcbiAgICogICAgICAgUy0+Pkw6IExvZyByZXRyeVxuICAgKiAgICAgICBTLT4+UzogV2FpdCB0aW1lb3V0XG4gICAqICAgICAgIFMtPj5TOiBSZWN1cnNpdmUgY2FsbCB0byBzdWJzY3JpYmVUb0NvdWNoXG4gICAqICAgICBlbmRcbiAgICogICBlbmRcbiAgICovXG4gIHByb3RlY3RlZCBvdmVycmlkZSBhc3luYyBpbml0aWFsaXplKCk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IGxvZyA9IHRoaXMubG9nLmZvcih0aGlzLmluaXRpYWxpemUpO1xuICAgIGNvbnN0IHN1YkxvZyA9IGxvZy5mb3Ioc3Vic2NyaWJlVG9Db3VjaCk7XG4gICAgYXN5bmMgZnVuY3Rpb24gc3Vic2NyaWJlVG9Db3VjaCh0aGlzOiBOYW5vRGlzcGF0Y2gpOiBQcm9taXNlPHZvaWQ+IHtcbiAgICAgIGlmICghdGhpcy5hZGFwdGVyIHx8ICF0aGlzLm5hdGl2ZSlcbiAgICAgICAgdGhyb3cgbmV3IEludGVybmFsRXJyb3IoYE5vIGFkYXB0ZXIvbmF0aXZlIG9ic2VydmVkIGZvciBkaXNwYXRjaGApO1xuXG4gICAgICB0cnkge1xuICAgICAgICB0aGlzLm5hdGl2ZS5jaGFuZ2VzKFxuICAgICAgICAgIHtcbiAgICAgICAgICAgIGZlZWQ6IFwiY29udGludW91c1wiLFxuICAgICAgICAgICAgaW5jbHVkZV9kb2NzOiBmYWxzZSxcbiAgICAgICAgICAgIHNpbmNlOiB0aGlzLm9ic2VydmVyTGFzdFVwZGF0ZSB8fCBcIm5vd1wiLFxuICAgICAgICAgICAgdGltZW91dDogdGhpcy50aW1lb3V0LFxuICAgICAgICAgIH0sXG4gICAgICAgICAgdGhpcy5jaGFuZ2VIYW5kbGVyLmJpbmQodGhpcykgYXMgYW55XG4gICAgICAgICk7XG4gICAgICB9IGNhdGNoIChlOiB1bmtub3duKSB7XG4gICAgICAgIGlmICgrK3RoaXMuYXR0ZW1wdENvdW50ZXIgPiAzKVxuICAgICAgICAgIHJldHVybiBzdWJMb2cuZXJyb3IoYEZhaWxlZCB0byBzdWJzY3JpYmUgdG8gY291Y2hkYiBjaGFuZ2VzOiAke2V9YCk7XG4gICAgICAgIHN1YkxvZy5pbmZvKFxuICAgICAgICAgIGBGYWlsZWQgdG8gc3Vic2NyaWJlIHRvIGNvdWNoZGIgY2hhbmdlczogJHtlfS4gUmV0cnlpbmcgaW4gNSBzZWNvbmRzLi4uYFxuICAgICAgICApO1xuICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gc2V0VGltZW91dChyZXNvbHZlLCB0aGlzLnRpbWVvdXQpKTtcbiAgICAgICAgcmV0dXJuIHN1YnNjcmliZVRvQ291Y2guY2FsbCh0aGlzKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBzdWJzY3JpYmVUb0NvdWNoXG4gICAgICAuY2FsbCh0aGlzKVxuICAgICAgLnRoZW4oKCkgPT4ge1xuICAgICAgICB0aGlzLmxvZy5pbmZvKGBTdWJzY3JpYmVkIHRvIGNvdWNoZGIgY2hhbmdlc2ApO1xuICAgICAgfSlcbiAgICAgIC5jYXRjaCgoZTogdW5rbm93bikgPT4ge1xuICAgICAgICB0aHJvdyBuZXcgSW50ZXJuYWxFcnJvcihgRmFpbGVkIHRvIHN1YnNjcmliZSB0byBjb3VjaGRiIGNoYW5nZXM6ICR7ZX1gKTtcbiAgICAgIH0pO1xuICB9XG59XG4iLCJpbXBvcnQge1xuICBDb25mbGljdEVycm9yLFxuICBDb250ZXh0LFxuICBJbnRlcm5hbEVycm9yLFxuICBvbkNyZWF0ZSxcbiAgT3BlcmF0aW9uS2V5cyxcbn0gZnJvbSBcIkBkZWNhZi10cy9kYi1kZWNvcmF0b3JzXCI7XG5pbXBvcnQgXCJyZWZsZWN0LW1ldGFkYXRhXCI7XG5pbXBvcnQge1xuICBDb3VjaERCQWRhcHRlcixcbiAgQ291Y2hEQktleXMsXG4gIENyZWF0ZUluZGV4UmVxdWVzdCxcbiAgZ2VuZXJhdGVJbmRleGVzLFxuICBNYW5nb1F1ZXJ5LFxuICBNYW5nb1Jlc3BvbnNlLFxufSBmcm9tIFwiQGRlY2FmLXRzL2Zvci1jb3VjaGRiXCI7XG5pbXBvcnQgTmFubyBmcm9tIFwibmFub1wiO1xuaW1wb3J0IHtcbiAgRG9jdW1lbnRCdWxrUmVzcG9uc2UsXG4gIERvY3VtZW50R2V0UmVzcG9uc2UsXG4gIERvY3VtZW50SW5zZXJ0UmVzcG9uc2UsXG4gIERvY3VtZW50U2NvcGUsXG4gIE1heWJlRG9jdW1lbnQsXG4gIFNlcnZlclNjb3BlLFxufSBmcm9tIFwibmFub1wiO1xuaW1wb3J0IHtcbiAgQ29uc3RydWN0b3IsXG4gIERlY29yYXRpb24sXG4gIE1vZGVsLFxuICBwcm9wTWV0YWRhdGEsXG59IGZyb20gXCJAZGVjYWYtdHMvZGVjb3JhdG9yLXZhbGlkYXRpb25cIjtcbmltcG9ydCB7IE5hbm9GbGFncyB9IGZyb20gXCIuL3R5cGVzXCI7XG5pbXBvcnQge1xuICBQZXJzaXN0ZW5jZUtleXMsXG4gIFJlbGF0aW9uc01ldGFkYXRhLFxuICBSZXBvc2l0b3J5LFxuICBVbnN1cHBvcnRlZEVycm9yLFxufSBmcm9tIFwiQGRlY2FmLXRzL2NvcmVcIjtcbmltcG9ydCB7IE5hbm9GbGF2b3VyIH0gZnJvbSBcIi4vY29uc3RhbnRzXCI7XG5pbXBvcnQgeyBOYW5vUmVwb3NpdG9yeSB9IGZyb20gXCIuL05hbm9SZXBvc2l0b3J5XCI7XG5pbXBvcnQgeyBOYW5vRGlzcGF0Y2ggfSBmcm9tIFwiLi9OYW5vRGlzcGF0Y2hcIjtcblxuLyoqXG4gKiBAZGVzY3JpcHRpb24gU2V0cyB0aGUgY3JlYXRvciBvciB1cGRhdGVyIGZpZWxkIGluIGEgbW9kZWwgYmFzZWQgb24gdGhlIHVzZXIgaW4gdGhlIGNvbnRleHRcbiAqIEBzdW1tYXJ5IENhbGxiYWNrIGZ1bmN0aW9uIHVzZWQgaW4gZGVjb3JhdG9ycyB0byBhdXRvbWF0aWNhbGx5IHNldCB0aGUgY3JlYXRlZF9ieSBvciB1cGRhdGVkX2J5IGZpZWxkc1xuICogd2l0aCB0aGUgdXNlcm5hbWUgZnJvbSB0aGUgY29udGV4dCB3aGVuIGEgZG9jdW1lbnQgaXMgY3JlYXRlZCBvciB1cGRhdGVkXG4gKiBAdGVtcGxhdGUgTSAtIFR5cGUgZXh0ZW5kaW5nIE1vZGVsXG4gKiBAdGVtcGxhdGUgUiAtIFR5cGUgZXh0ZW5kaW5nIE5hbm9SZXBvc2l0b3J5PE0+XG4gKiBAdGVtcGxhdGUgViAtIFR5cGUgZXh0ZW5kaW5nIFJlbGF0aW9uc01ldGFkYXRhXG4gKiBAcGFyYW0ge1J9IHRoaXMgLSBUaGUgcmVwb3NpdG9yeSBpbnN0YW5jZVxuICogQHBhcmFtIHtDb250ZXh0PE5hbm9GbGFncz59IGNvbnRleHQgLSBUaGUgb3BlcmF0aW9uIGNvbnRleHQgY29udGFpbmluZyB1c2VyIGluZm9ybWF0aW9uXG4gKiBAcGFyYW0ge1Z9IGRhdGEgLSBUaGUgcmVsYXRpb24gbWV0YWRhdGFcbiAqIEBwYXJhbSBrZXkgLSBUaGUgcHJvcGVydHkga2V5IHRvIHNldCB3aXRoIHRoZSB1c2VybmFtZVxuICogQHBhcmFtIHtNfSBtb2RlbCAtIFRoZSBtb2RlbCBpbnN0YW5jZSBiZWluZyBjcmVhdGVkIG9yIHVwZGF0ZWRcbiAqIEByZXR1cm4ge1Byb21pc2U8dm9pZD59IEEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdoZW4gdGhlIG9wZXJhdGlvbiBpcyBjb21wbGV0ZVxuICogQGZ1bmN0aW9uIGNyZWF0ZWRCeU9uTmFub0NyZWF0ZVVwZGF0ZVxuICogQG1lbWJlck9mIG1vZHVsZTpmb3ItbmFub1xuICogQG1lcm1haWRcbiAqIHNlcXVlbmNlRGlhZ3JhbVxuICogICBwYXJ0aWNpcGFudCBGIGFzIGNyZWF0ZWRCeU9uTmFub0NyZWF0ZVVwZGF0ZVxuICogICBwYXJ0aWNpcGFudCBDIGFzIENvbnRleHRcbiAqICAgcGFydGljaXBhbnQgTSBhcyBNb2RlbFxuICogICBGLT4+QzogZ2V0KFwidXNlclwiKVxuICogICBDLS0+PkY6IHVzZXIgb2JqZWN0XG4gKiAgIEYtPj5NOiBzZXQga2V5IHRvIHVzZXIubmFtZVxuICogICBOb3RlIG92ZXIgRjogSWYgbm8gdXNlciBpbiBjb250ZXh0XG4gKiAgIEYtLT4+RjogdGhyb3cgVW5zdXBwb3J0ZWRFcnJvclxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY3JlYXRlZEJ5T25OYW5vQ3JlYXRlVXBkYXRlPFxuICBNIGV4dGVuZHMgTW9kZWwsXG4gIFIgZXh0ZW5kcyBOYW5vUmVwb3NpdG9yeTxNPixcbiAgViBleHRlbmRzIFJlbGF0aW9uc01ldGFkYXRhLFxuPihcbiAgdGhpczogUixcbiAgY29udGV4dDogQ29udGV4dDxOYW5vRmxhZ3M+LFxuICBkYXRhOiBWLFxuICBrZXk6IGtleW9mIE0sXG4gIG1vZGVsOiBNXG4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgdHJ5IHtcbiAgICBjb25zdCB1c2VyID0gY29udGV4dC5nZXQoXCJ1c2VyXCIpO1xuICAgIG1vZGVsW2tleV0gPSB1c2VyLm5hbWUgYXMgTVt0eXBlb2Yga2V5XTtcbiAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgQHR5cGVzY3JpcHQtZXNsaW50L25vLXVudXNlZC12YXJzXG4gIH0gY2F0Y2ggKGU6IHVua25vd24pIHtcbiAgICB0aHJvdyBuZXcgVW5zdXBwb3J0ZWRFcnJvcihcbiAgICAgIFwiTm8gVXNlciBmb3VuZCBpbiBjb250ZXh0LiBQbGVhc2UgcHJvdmlkZSBhIHVzZXIgaW4gdGhlIGNvbnRleHRcIlxuICAgICk7XG4gIH1cbn1cblxuLyoqXG4gKiBAZGVzY3JpcHRpb24gQWRhcHRlciBmb3IgaW50ZXJhY3Rpbmcgd2l0aCBOYW5vIGRhdGFiYXNlc1xuICogQHN1bW1hcnkgUHJvdmlkZXMgYSBzdGFuZGFyZGl6ZWQgaW50ZXJmYWNlIGZvciBwZXJmb3JtaW5nIENSVUQgb3BlcmF0aW9ucyBvbiBOYW5vIGRhdGFiYXNlcyxcbiAqIGV4dGVuZGluZyB0aGUgQ291Y2hEQiBhZGFwdGVyIHdpdGggTmFuby1zcGVjaWZpYyBmdW5jdGlvbmFsaXR5LiBUaGlzIGFkYXB0ZXIgaGFuZGxlcyBkb2N1bWVudFxuICogY3JlYXRpb24sIHJlYWRpbmcsIHVwZGF0aW5nLCBhbmQgZGVsZXRpb24sIGFzIHdlbGwgYXMgYnVsayBvcGVyYXRpb25zIGFuZCBpbmRleCBtYW5hZ2VtZW50LlxuICogQHRlbXBsYXRlIERvY3VtZW50U2NvcGUgLSBUaGUgTmFubyBkb2N1bWVudCBzY29wZSB0eXBlXG4gKiBAdGVtcGxhdGUgTmFub0ZsYWdzIC0gQ29uZmlndXJhdGlvbiBmbGFncyBmb3IgTmFubyBvcGVyYXRpb25zXG4gKiBAdGVtcGxhdGUgQ29udGV4dCAtIENvbnRleHQgdHlwZSBmb3Igb3BlcmF0aW9uc1xuICogQHBhcmFtIHtEb2N1bWVudFNjb3BlPGFueT59IHNjb3BlIC0gVGhlIE5hbm8gZG9jdW1lbnQgc2NvcGUgdG8gdXNlIGZvciBkYXRhYmFzZSBvcGVyYXRpb25zXG4gKiBAcGFyYW0ge3N0cmluZ30gW2FsaWFzXSAtIE9wdGlvbmFsIGFsaWFzIGZvciB0aGUgYWRhcHRlclxuICogQGNsYXNzIE5hbm9BZGFwdGVyXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogLy8gQ29ubmVjdCB0byBhIE5hbm8gZGF0YWJhc2VcbiAqIGNvbnN0IHNlcnZlciA9IE5hbm9BZGFwdGVyLmNvbm5lY3QoJ2FkbWluJywgJ3Bhc3N3b3JkJywgJ2xvY2FsaG9zdDo1OTg0Jyk7XG4gKiBjb25zdCBkYiA9IHNlcnZlci5kYi51c2UoJ215X2RhdGFiYXNlJyk7XG4gKlxuICogLy8gQ3JlYXRlIGFuIGFkYXB0ZXIgaW5zdGFuY2VcbiAqIGNvbnN0IGFkYXB0ZXIgPSBuZXcgTmFub0FkYXB0ZXIoZGIpO1xuICpcbiAqIC8vIFVzZSB0aGUgYWRhcHRlciBmb3IgZGF0YWJhc2Ugb3BlcmF0aW9uc1xuICogY29uc3QgZG9jdW1lbnQgPSBhd2FpdCBhZGFwdGVyLnJlYWQoJ3VzZXJzJywgJzEyMycpO1xuICogYGBgXG4gKiBAbWVybWFpZFxuICogY2xhc3NEaWFncmFtXG4gKiAgIGNsYXNzIENvdWNoREJBZGFwdGVyIHtcbiAqICAgICArZmxhZ3MoKVxuICogICAgICtEaXNwYXRjaCgpXG4gKiAgICAgK2luZGV4KClcbiAqICAgICArY3JlYXRlKClcbiAqICAgICArcmVhZCgpXG4gKiAgICAgK3VwZGF0ZSgpXG4gKiAgICAgK2RlbGV0ZSgpXG4gKiAgIH1cbiAqICAgY2xhc3MgTmFub0FkYXB0ZXIge1xuICogICAgICtmbGFncygpXG4gKiAgICAgK0Rpc3BhdGNoKClcbiAqICAgICAraW5kZXgoKVxuICogICAgICtjcmVhdGUoKVxuICogICAgICtjcmVhdGVBbGwoKVxuICogICAgICtyZWFkKClcbiAqICAgICArcmVhZEFsbCgpXG4gKiAgICAgK3VwZGF0ZSgpXG4gKiAgICAgK3VwZGF0ZUFsbCgpXG4gKiAgICAgK2RlbGV0ZSgpXG4gKiAgICAgK2RlbGV0ZUFsbCgpXG4gKiAgICAgK3JhdygpXG4gKiAgICAgK3N0YXRpYyBjb25uZWN0KClcbiAqICAgICArc3RhdGljIGNyZWF0ZURhdGFiYXNlKClcbiAqICAgICArc3RhdGljIGRlbGV0ZURhdGFiYXNlKClcbiAqICAgICArc3RhdGljIGNyZWF0ZVVzZXIoKVxuICogICAgICtzdGF0aWM