ngx-scroll-position-restoration
Version:
Scroll position restoration in Angular.
350 lines • 53 kB
JavaScript
import { isPlatformServer } from '@angular/common';
import { Inject, Injectable, NgZone, PLATFORM_ID } from '@angular/core';
import { NavigationStart, Router, NavigationEnd } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import * as DomUtils from './dom-utils';
import { NGX_SCROLL_POSITION_RESTORATION_CONFIG_INJECTION_TOKEN } from './ngx-scroll-position-restoration-config-injection-token';
export class NgxScrollPositionRestorationService {
constructor(router, zone, platformId, config) {
this.router = router;
this.zone = zone;
this.platformId = platformId;
this.config = config;
this.applyStateToDomTimer = 0;
this.currentPageState = {};
this.lastNavigationStartAt = 0;
this.navigationIDs = [];
this.pageStates = {};
this.scrolledElements = new Set();
this.maximumNumberOfCachedPageStates = 20;
this.serviceDestroyed$ = new Subject();
}
/**
* Initialize NgxScrollPositionRestorationService.
*/
initialize() {
if (isPlatformServer(this.platformId)) {
return;
}
this.setupScrollBinding();
// this.setupRouterBinding();
// I bind to the router events and perform to primary actions:
// --
// NAVIGATION START: When the user is about to navigate away from the current view,
// I inspect the current DOM state and commit any scrolled-element offsets to the
// in-memory cache of the page state (scroll events were recorded during the lifetime
// of the current router state).
// --
// NAVIGATION END: When the user completes a navigation to a new view, I check to see
// if the new view is really the restoration of a previously cached page state; and,
// if so, I try to reinstate the old scrolled-element offsets in the rendered DOM.
this.router.events.pipe(takeUntil(this.serviceDestroyed$)).subscribe((event) => {
// Filter navigation event streams to the appropriate event handlers.
if (event instanceof NavigationStart) {
this.handleNavigationStart(event);
}
else if (event instanceof NavigationEnd) {
this.handleNavigationEnd();
}
});
// Since we're going to be implementing a custom scroll retention algorithm,
// let's disable the one that is provided by the browser. This will keep our
// polyfill the source of truth.
this.disableBrowserDefaultScrollRestoration();
}
ngOnDestroy() {
this.serviceDestroyed$.next();
this.serviceDestroyed$.complete();
}
clearSavedWindowScrollTopInLastNavigation() {
const lastNavigationId = this.navigationIDs[this.navigationIDs.length - 1];
if (lastNavigationId) {
if (this.config.debug && this.pageStates[lastNavigationId][DomUtils.WINDOW_SELECTOR]) {
console.log('Navigation in a "secondary" router-outlet - Remove window scroll position from recorded scroll positions.');
}
delete (this.pageStates[lastNavigationId][DomUtils.WINDOW_SELECTOR]);
}
}
/**
* I attempt to apply the given page-state to the rendered DOM. I will continue to poll the document until all states have been reinstated; or, until the poll duration has been exceeded; or, until a subsequent navigation takes place.
*/
applyPageStateToDom(pageState) {
if (this.config.debug) {
this.debugPageState(pageState, 'Attempting to reapply scroll positions after a popstate navigation (backward or forward).');
}
if (this.objectIsEmpty(pageState)) {
return;
}
// Let's create a copy of the page state so that we can safely delete keys from
// it as we successfully apply them to the rendered DOM.
const pendingPageState = Object.assign({}, pageState);
// Setup the scroll retention timer outside of the Angular Zone so that it
// doesn't trigger any additional change-detection digests.
this.zone.runOutsideAngular(() => {
const startedAt = Date.now();
this.applyStateToDomTimer = setInterval(() => {
for (const selector in pendingPageState) {
const target = DomUtils.select(selector);
// If the target element doesn't exist in the DOM yet, it
// could be an indication of asynchronous loading and
// rendering. Move onto the next selector while we still
// have time.
if (!target) {
continue;
}
// If the element in question has been scrolled (by the user)
// while we're attempting to reinstate the previous scroll
// offsets, then ignore this state - the user's action should
// take precedence.
if (this.scrolledElements.has(target)) {
delete (pendingPageState[selector]);
// Otherwise, let's try to restore the scroll for the target.
}
else {
const scrollTop = pendingPageState[selector];
const resultantScrollTop = DomUtils.scrollTo(target, scrollTop);
// If the attempt to restore the element to its previous
// offset resulted in a match, then stop tracking this
// element. Otherwise, we'll continue to try and scroll
// it in the subsequent tick.
// --
// NOTE: We continue to try and update it because the
// target element may exist in the DOM but also be
// loading asynchronous data that is required for the
// previous scroll offset.
if (resultantScrollTop === scrollTop) {
delete (pendingPageState[selector]);
}
}
}
// If there are no more elements to scroll or, we've exceeded our
// poll duration, then stop watching the DOM.
if (this.objectIsEmpty(pendingPageState)
|| ((Date.now() - startedAt) >= this.config.pollDuration)) {
clearTimeout(this.applyStateToDomTimer);
if (this.config.debug) {
if (this.objectIsEmpty(pendingPageState)) {
console.log('%c Successfully reapplied all recorded scroll positions to the DOM.', 'color: #2ecc71');
}
else {
console.warn(`Could not reapply following recorded scroll positions to the DOM after a poll duration of: ${this.config.pollDuration} milliseconds:`);
this.debugPageState(pendingPageState);
}
}
}
}, this.config.pollCadence);
});
}
/**
* I get the page state from the given set of nodes. This extracts the CSS selectors and offsets from the recorded elements.
*/
getPageStateFromNodes(nodes) {
const pageState = {};
nodes.forEach(target => {
// Generate a CSS selector from the given target.
// --
// TODO: Right now, this algorithm creates the selector by walking up the
// DOM tree and using the simulated encapsulation attributes. But, it
// would be cool to have a configuration option that tells this algorithm
// to look for a specific id-prefix or attribute or something. This would
// require the developer to provide those; but it would be optimal.
const selector = DomUtils.getSelector(target);
// If the given Target is no longer part of the active DOM, the selector
// will be null.
if (selector) {
pageState[selector] = DomUtils.getScrollTop(target);
}
});
return pageState;
}
/**
* I determine if the given object is empty (ie, has no keys).
*/
objectIsEmpty(object) {
for (const key in object) {
return false;
}
return true;
}
// The goal of the NavigationStart event is to take changes that have been made
// to the current DOM and store them in the render-state tree so they can be
// reinstated at a future date.
handleNavigationStart(event) {
this.lastNavigationStartAt = Date.now();
// Get the navigation ID and the restored navigation ID for use in the
// NavigationEnd event handler.
this.navigationID = event.id;
/**
* Maybe in future update @todo: use ngx-navigation-trigger here, like:
* (event.restoredState && this.whenShouldScrollPositionBeRestored.has(this.navigationTrigger))
*/
this.restoredNavigationID = event.restoredState ? event.restoredState.navigationId : null;
// If the user is navigating away from the current view, kill any timers that
// may be trying to reinstate a page-state.
clearTimeout(this.applyStateToDomTimer);
// Before we navigate away from the current page state, let's commit any
// scroll-elements to the current page state.
Object.assign(this.currentPageState, this.getPageStateFromNodes(this.scrolledElements));
this.scrolledElements.clear();
if (this.config.debug) {
this.debugPageState(this.currentPageState, 'Recorded scroll positions.');
}
}
;
// The primary goal of the NavigationEnd event is to reinstate a cached page
// state in the event that the navigation is restoring a previously rendered page
// as the result of a popstate event (ex, the user hit the Back or Forward
// buttons).
handleNavigationEnd() {
const previousPageState = this.currentPageState;
// Now that we know the navigation was successful, let's start and store a
// new page state to track future scrolling.
this.currentPageState = this.pageStates[this.navigationID] = {};
// While we are going to track elements that will be scrolled during the
// current page rendering, it is possible that there are elements that were
// scrolled during a prior page rendering that still exist on the page, but
// were not scrolled recently (such as a secondary router-outlet). As such,
// let's look at the previous page state and 'pull forward' any state that
// still pertains to the current page.
if (!this.restoredNavigationID) {
for (const selector in previousPageState) {
const target = DomUtils.select(selector);
// Only pull the selector forward if it corresponds to an element
// that still exists in the rendered page.
if (!target) {
continue;
}
// Only pull the selector forward if the target is still at the same
// offset after the navigation has taken place. In other words, if
// the offset has somehow changed in between the NavigationStart and
// NavigationEnd events, then ignore it. To be honest, this really
// only applies to the WINDOW, which can change in offset due to the
// change in what the Router is actively rendering in the DOM.
if (DomUtils.getScrollTop(target) !== previousPageState[selector]) {
continue;
}
this.currentPageState[selector] = previousPageState[selector];
if (this.config.debug) {
console.group('Pulling scroll position from previous page state in current page state.');
console.log({
selector,
scrollPosition: this.currentPageState[selector]
});
console.groupEnd();
}
}
// If we're restoring a previous page state AND we have that previous page
// state cached in-memory, let's copy the previous state and then restore the
// offsets in the DOM.
}
else if (this.restoredNavigationID && this.pageStates[this.restoredNavigationID]) {
// NOTE: We're copying the offsets from the restored state into the
// current state instead of just swapping the references because these
// navigations are different in the Router history. Since each navigation
// - imperative or popstate - gets a unique ID, we never truly 'go back'
// in history; the Router only 'goes forward', with the notion that we're
// recreating a previous state sometimes.
this.applyPageStateToDom(Object.assign(this.currentPageState, this.pageStates[this.restoredNavigationID]));
}
// Keep track of the navigation event so we can limit the size of our
// in-memory page state cache.
this.navigationIDs.push(this.navigationID);
// Trim the oldest page states as we go so that the in-memory cache doesn't
// grow, unbounded.
while (this.navigationIDs.length > this.maximumNumberOfCachedPageStates) {
delete (this.pageStates[this.navigationIDs.shift()]);
}
}
;
/**
* I bind to the scroll event and keep track of any elements that are scrolled in the rendered document.
*/
setupScrollBinding() {
/**
* Maybe @todo: You should try to find a way to get scrollable (scrolled) elements only during NavigationStart.
* Advantages:
* - Better performance: no need to listen to the scroll event the whole time.
* - Some elements might be added to the `scrolledElements` are not part of the DOM any more.
* Disavantages:
* - during NavigationStart scrollable elements that are maybe present after the intialization of page (before any user-interactions that can remove them) might be not part DOM any more.
*
*/
// Add scroll-binding outside of the Angular Zone so it doesn't trigger any
// additional change-detection digests.
this.zone.runOutsideAngular(() => {
// When navigating, the browser emits some scroll events as the DOM
// (Document Object Model) changes shape in a way that forces the various
// scroll offsets to change. Since these scroll events are not indicative
// of a user's actual scrolling intent, we're going to ignore them. This
// needs to be done on both sides of the navigation event (for reasons
// that are not fully obvious or logical -- basically, the window's
// scroll changes at a time that is not easy to tap into). Ignoring these
// scroll events is important because the polyfilly stops trying to
// reinstate a scroll-offset if it sees that the given element has
// already been scrolled during the current rendering.
const scrollBufferWindow = 100;
let target;
window.addEventListener('scroll', event => {
// If the scroll event happens immediately following a
// navigation event, then ignore it - it is likely a scroll that
// was forced by the browser's native behavior.
if ((Date.now() - this.lastNavigationStartAt) < scrollBufferWindow) {
return;
}
// The target will return NULL for elements that have irrelevant
// scroll behaviors (like textarea inputs). As such, we have to
// check to see if the domUtils returned anything.
target = DomUtils.getTargetFromScrollEvent(event);
if (target) {
this.scrolledElements.add(target);
}
},
// We have to use the CAPTURING phase. Scroll events DO NOT BUBBLE.
// As such, if we want to listen for all scroll events in the
// document, we have to use the capturing phase (as the event travels
// down through the DOM tree).
true);
});
}
debugPageState(pageState, message) {
if (this.objectIsEmpty(pageState)) {
return;
}
console.group(message || '');
for (const [selector, scrollPosition] of Object.entries(pageState)) {
console.log({
selector,
scrollPosition
});
}
console.groupEnd();
}
/**
* Disable browser default scroll restoration.
*
* Documentation:
* - https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration
*/
disableBrowserDefaultScrollRestoration() {
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
}
}
NgxScrollPositionRestorationService.decorators = [
{ type: Injectable }
];
NgxScrollPositionRestorationService.ctorParameters = () => [
{ type: Router },
{ type: NgZone },
{ type: String, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] },
{ type: undefined, decorators: [{ type: Inject, args: [NGX_SCROLL_POSITION_RESTORATION_CONFIG_INJECTION_TOKEN,] }] }
];
/**
* Source:
* - https://www.bennadel.com/blog/3534-restoring-and-resetting-the-scroll-position-using-the-navigationstart-event-in-angular-7-0-4.htm
* - http://bennadel.github.io/JavaScript-Demos/demos/router-retain-scroll-polyfill-angular7/
* - https://github.com/bennadel/JavaScript-Demos/tree/master/demos/router-retain-scroll-polyfill-angular7
*/
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmd4LXNjcm9sbC1wb3NpdGlvbi1yZXN0b3JhdGlvbi5zZXJ2aWNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LXNjcm9sbC1wb3NpdGlvbi1yZXN0b3JhdGlvbi9zcmMvbGliL25neC1zY3JvbGwtcG9zaXRpb24tcmVzdG9yYXRpb24uc2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQWEsV0FBVyxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBQ25GLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxFQUFrQyxhQUFhLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6RyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBQy9CLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUMzQyxPQUFPLEtBQUssUUFBUSxNQUFNLGFBQWEsQ0FBQztBQUV4QyxPQUFPLEVBQUUsc0RBQXNELEVBQUUsTUFBTSwwREFBMEQsQ0FBQztBQUdsSSxNQUFNLE9BQU8sbUNBQW1DO0lBaUI5QyxZQUNVLE1BQWMsRUFDZCxJQUFZLEVBQ1MsVUFBa0IsRUFDeUIsTUFBMEM7UUFIMUcsV0FBTSxHQUFOLE1BQU0sQ0FBUTtRQUNkLFNBQUksR0FBSixJQUFJLENBQVE7UUFDUyxlQUFVLEdBQVYsVUFBVSxDQUFRO1FBQ3lCLFdBQU0sR0FBTixNQUFNLENBQW9DO1FBbkI1Ryx5QkFBb0IsR0FBVyxDQUFDLENBQUM7UUFDakMscUJBQWdCLEdBQWMsRUFBRSxDQUFDO1FBQ2pDLDBCQUFxQixHQUFXLENBQUMsQ0FBQztRQUNsQyxrQkFBYSxHQUFhLEVBQUUsQ0FBQztRQUM3QixlQUFVLEdBQWUsRUFBRSxDQUFDO1FBQzVCLHFCQUFnQixHQUFnQixJ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QWtEO2dCQUNsRCxNQUFNLEdBQUcsUUFBUSxDQUFDLHdCQUF3QixDQUFDLEtBQUssQ0FBQyxDQUFDO2dCQUNsRCxJQUFJLE1BQU0sRUFBRTtvQkFDVixJQUFJLENBQUMsZ0JBQWdCLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO2lCQUNuQztZQUNILENBQUM7WUFDRCxtRUFBbUU7WUFDbkUsOERBQThEO1lBQzlELHFFQUFxRTtZQUNyRSw4QkFBOEI7WUFDOUIsSUFBSSxDQUNMLENBQUM7UUFDSixDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFTyxjQUFjLENBQUMsU0FBb0IsRUFBRSxPQUFnQjtRQUMzRCxJQUFJLElBQUksQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDLEVBQUU7WUFDakMsT0FBTztTQUNSO1FBQ0QsT0FBTyxDQUFDLEtBQUssQ0FBQyxPQUFPLElBQUksRUFBRSxDQUFDLENBQUM7UUFDN0IsS0FBSyxNQUFNLENBQUMsUUFBUSxFQUFFLGNBQWMsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLEVBQUU7WUFDbEUsT0FBTyxDQUFDLEdBQUcsQ0FBQztnQkFDVixRQUFRO2dCQUNSLGNBQWM7YUFDZixDQUFDLENBQUM7U0FDSjtRQUNELE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQztJQUNyQixDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSyxzQ0FBc0M7UUFDNUMsSUFBSSxtQkFBbUIsSUFBSSxPQUFPLEVBQUU7WUFDbEMsT0FBTyxDQUFDLGlCQUFpQixHQUFHLFFBQVEsQ0FBQztTQUN0QztJQUNILENBQUM7OztZQTNZRixVQUFVOzs7WUFQZSxNQUFNO1lBREgsTUFBTTt5Q0E2QjlCLE1BQU0sU0FBQyxXQUFXOzRDQUNsQixNQUFNLFNBQUMsc0RBQXNEOztBQXFZbEU7Ozs7O0dBS0ciLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBpc1BsYXRmb3JtU2VydmVyIH0gZnJvbSAnQGFuZ3VsYXIvY29tbW9uJztcbmltcG9ydCB7IEluamVjdCwgSW5qZWN0YWJsZSwgTmdab25lLCBPbkRlc3Ryb3ksIFBMQVRGT1JNX0lEIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQgeyBOYXZpZ2F0aW9uU3RhcnQsIFJvdXRlciwgRXZlbnQgYXMgUm91dGVyTmF2aWdhdGlvbkV2ZW50LCBOYXZpZ2F0aW9uRW5kIH0gZnJvbSAnQGFuZ3VsYXIvcm91dGVyJztcbmltcG9ydCB7IFN1YmplY3QgfSBmcm9tICdyeGpzJztcbmltcG9ydCB7IHRha2VVbnRpbCB9IGZyb20gJ3J4anMvb3BlcmF0b3JzJztcbmltcG9ydCAqIGFzIERvbVV0aWxzIGZyb20gJy4vZG9tLXV0aWxzJztcbmltcG9ydCB7IE5neFNjcm9sbFBvc2l0aW9uUmVzdG9yYXRpb25Db25maWcgfSBmcm9tICcuL25neC1zY3JvbGwtcG9zaXRpb24tcmVzdG9yYXRpb24tY29uZmlnJztcbmltcG9ydCB7IE5HWF9TQ1JPTExfUE9TSVRJT05fUkVTVE9SQVRJT05fQ09ORklHX0lOSkVDVElPTl9UT0tFTiB9IGZyb20gJy4vbmd4LXNjcm9sbC1wb3NpdGlvbi1yZXN0b3JhdGlvbi1jb25maWctaW5qZWN0aW9uLXRva2VuJztcblxuQEluamVjdGFibGUoKVxuZXhwb3J0IGNsYXNzIE5neFNjcm9sbFBvc2l0aW9uUmVzdG9yYXRpb25TZXJ2aWNlIGltcGxlbWVudHMgT25EZXN0cm95IHtcblxuICBwcml2YXRlIGFwcGx5U3RhdGVUb0RvbVRpbWVyOiBudW1iZXIgPSAwO1xuICBwcml2YXRlIGN1cnJlbnRQYWdlU3RhdGU6IFBhZ2VTdGF0ZSA9IHt9O1xuICBwcml2YXRlIGxhc3ROYXZpZ2F0aW9uU3RhcnRBdDogbnVtYmVyID0gMDtcbiAgcHJpdmF0ZSBuYXZpZ2F0aW9uSURzOiBudW1iZXJbXSA9IFtdO1xuICBwcml2YXRlIHBhZ2VTdGF0ZXM6IFBhZ2VTdGF0ZXMgPSB7fTtcbiAgcHJpdmF0ZSBzY3JvbGxlZEVsZW1lbnRzOiBTZXQ8VGFyZ2V0PiA9IG5ldyBTZXQoKTtcblxuICAvLyBXZSBuZWVkIHRvIGtlZXAgdHJhY2sgb2YgdGhlc2UgdmFsdWVzIGFjcm9zcyB0aGUgU3RhcnQgLyBFbmQgZXZlbnRzLlxuICBwcml2YXRlIG5hdmlnYXRpb25JRCE6IG51bWJlcjtcbiAgcHJpdmF0ZSByZXN0b3JlZE5hdmlnYXRpb25JRCE6IG51bWJlciB8IG51bGw7XG5cbiAgcHJpdmF0ZSBtYXhpbXVtTnVtYmVyT2ZDYWNoZWRQYWdlU3RhdGVzOiBudW1iZXIgPSAyMDtcblxuICBwcml2YXRlIHNlcnZpY2VEZXN0cm95ZWQkID0gbmV3IFN1YmplY3Q8dm9pZD4oKTtcblxuICBjb25zdHJ1Y3RvcihcbiAgICBwcml2YXRlIHJvdXRlcjogUm91dGVyLFxuICAgIHByaXZhdGUgem9uZTogTmdab25lLFxuICAgIEBJbmplY3QoUExBVEZPUk1fSUQpIHByaXZhdGUgcGxhdGZvcm1JZDogc3RyaW5nLFxuICAgIEBJbmplY3QoTkdYX1NDUk9MTF9QT1NJVElPTl9SRVNUT1JBVElPTl9DT05GSUdfSU5KRUNUSU9OX1RPS0VOKSBwcml2YXRlIGNvbmZpZzogTmd4U2Nyb2xsUG9zaXRpb25SZXN0b3JhdGlvbkNvbmZpZ1xuICApIHsgfVxuXG4gIC8qKlxuICAgKiBJbml0aWFsaXplIE5neFNjcm9sbFBvc2l0aW9uUmVzdG9yYXRpb25TZXJ2aWNlLlxuICAgKi9cbiAgaW5pdGlhbGl6ZSgpOiB2b2lkIHtcbiAgICBpZiAoaXNQbGF0Zm9ybVNlcnZlcih0aGlzLnBsYXRmb3JtSWQpKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgdGhpcy5zZXR1cFNjcm9sbEJpbmRpbmcoKTtcblxuICAgIC8vIHRoaXMuc2V0dXBSb3V0ZXJCaW5kaW5nKCk7XG4gICAgLy8gSSBiaW5kIHRvIHRoZSByb3V0ZXIgZXZlbnRzIGFuZCBwZXJmb3JtIHRvIHByaW1hcnkgYWN0aW9uczpcbiAgICAvLyAtLVxuICAgIC8vIE5BVklHQVRJT04gU1RBUlQ6IFdoZW4gdGhlIHVzZXIgaXMgYWJvdXQgdG8gbmF2aWdhdGUgYXdheSBmcm9tIHRoZSBjdXJyZW50IHZpZXcsXG4gICAgLy8gSSBpbnNwZWN0IHRoZSBjdXJyZW50IERPTSBzdGF0ZSBhbmQgY29tbWl0IGFueSBzY3JvbGxlZC1lbGVtZW50IG9mZnNldHMgdG8gdGhlXG4gICAgLy8gaW4tbWVtb3J5IGNhY2hlIG9mIHRoZSBwYWdlIHN0YXRlIChzY3JvbGwgZXZlbnRzIHdlcmUgcmVjb3JkZWQgZHVyaW5nIHRoZSBsaWZldGltZVxuICAgIC8vIG9mIHRoZSBjdXJyZW50IHJvdXRlciBzdGF0ZSkuXG4gICAgLy8gLS1cbiAgICAvLyBOQVZJR0FUSU9OIEVORDogV2hlbiB0aGUgdXNlciBjb21wbGV0ZXMgYSBuYXZpZ2F0aW9uIHRvIGEgbmV3IHZpZXcsIEkgY2hlY2sgdG8gc2VlXG4gICAgLy8gaWYgdGhlIG5ldyB2aWV3IGlzIHJlYWxseSB0aGUgcmVzdG9yYXRpb24gb2YgYSBwcmV2aW91c2x5IGNhY2hlZCBwYWdlIHN0YXRlOyBhbmQsXG4gICAgLy8gaWYgc28sIEkgdHJ5IHRvIHJlaW5zdGF0ZSB0aGUgb2xkIHNjcm9sbGVkLWVsZW1lbnQgb2Zmc2V0cyBpbiB0aGUgcmVuZGVyZWQgRE9NLlxuICAgIHRoaXMucm91dGVyLmV2ZW50cy5waXBlKFxuICAgICAgdGFrZVVudGlsKHRoaXMuc2VydmljZURlc3Ryb3llZCQpXG4gICAgKS5zdWJzY3JpYmUoXG4gICAgICAoZXZlbnQ6IFJvdXRlck5hdmlnYXRpb25FdmVudCkgPT4ge1xuICAgICAgICAvLyBGaWx0ZXIgbmF2aWdhdGlvbiBldmVudCBzdHJlYW1zIHRvIHRoZSBhcHByb3ByaWF0ZSBldmVudCBoYW5kbGVycy5cbiAgICAgICAgaWYgKGV2ZW50IGluc3RhbmNlb2YgTmF2aWdhdGlvblN0YXJ0KSB7XG4gICAgICAgICAgdGhpcy5oYW5kbGVOYXZpZ2F0aW9uU3RhcnQoZXZlbnQpO1xuICAgICAgICB9IGVsc2UgaWYgKGV2ZW50IGluc3RhbmNlb2YgTmF2aWdhdGlvbkVuZCkge1xuICAgICAgICAgIHRoaXMuaGFuZGxlTmF2aWdhdGlvbkVuZCgpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgKTtcblxuICAgIC8vIFNpbmNlIHdlJ3JlIGdvaW5nIHRvIGJlIGltcGxlbWVudGluZyBhIGN1c3RvbSBzY3JvbGwgcmV0ZW50aW9uIGFsZ29yaXRobSxcbiAgICAvLyBsZXQncyBkaXNhYmxlIHRoZSBvbmUgdGhhdCBpcyBwcm92aWRlZCBieSB0aGUgYnJvd3Nlci4gVGhpcyB3aWxsIGtlZXAgb3VyXG4gICAgLy8gcG9seWZpbGwgdGhlIHNvdXJjZSBvZiB0cnV0aC5cbiAgICB0aGlzLmRpc2FibGVCcm93c2VyRGVmYXVsdFNjcm9sbFJlc3RvcmF0aW9uKCk7XG4gIH1cblxuICBuZ09uRGVzdHJveSgpOiB2b2lkIHtcbiAgICB0aGlzLnNlcnZpY2VEZXN0cm95ZWQkLm5leHQoKTtcbiAgICB0aGlzLnNlcnZpY2VEZXN0cm95ZWQkLmNvbXBsZXRlKCk7XG4gIH1cblxuICBjbGVhclNhdmVkV2luZG93U2Nyb2xsVG9wSW5MYXN0TmF2aWdhdGlvbigpOiB2b2lkIHtcbiAgICBjb25zdCBsYXN0TmF2aWdhdGlvbklkID0gdGhpcy5uYXZpZ2F0aW9uSURzW3RoaXMubmF2aWdhdGlvbklEcy5sZW5ndGggLSAxXTtcbiAgICBpZiAobGFzdE5hdmlnYXRpb25JZCkge1xuICAgICAgaWYgKHRoaXMuY29uZmlnLmRlYnVnICYmIHRoaXMucGFnZVN0YXRlc1tsYXN0TmF2aWdhdGlvbklkXVtEb21VdGlscy5XSU5ET1dfU0VMRUNUT1JdKSB7XG4gICAgICAgIGNvbnNvbGUubG9nKCdOYXZpZ2F0aW9uIGluIGEgXCJzZWNvbmRhcnlcIiByb3V0ZXItb3V0bGV0IC0gUmVtb3ZlIHdpbmRvdyBzY3JvbGwgcG9zaXRpb24gZnJvbSByZWNvcmRlZCBzY3JvbGwgcG9zaXRpb25zLicpO1xuICAgICAgfVxuICAgICAgZGVsZXRlICh0aGlzLnBhZ2VTdGF0ZXNbbGFzdE5hdmlnYXRpb25JZF1bRG9tVXRpbHMuV0lORE9XX1NFTEVDVE9SXSk7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIEkgYXR0ZW1wdCB0byBhcHBseSB0aGUgZ2l2ZW4gcGFnZS1zdGF0ZSB0byB0aGUgcmVuZGVyZWQgRE9NLiBJIHdpbGwgY29udGludWUgdG8gcG9sbCB0aGUgZG9jdW1lbnQgdW50aWwgYWxsIHN0YXRlcyBoYXZlIGJlZW4gcmVpbnN0YXRlZDsgb3IsIHVudGlsIHRoZSBwb2xsIGR1cmF0aW9uIGhhcyBiZWVuIGV4Y2VlZGVkOyBvciwgdW50aWwgYSBzdWJzZXF1ZW50IG5hdmlnYXRpb24gdGFrZXMgcGxhY2UuXG4gICAqL1xuICBwcml2YXRlIGFwcGx5UGFnZVN0YXRlVG9Eb20ocGFnZVN0YXRlOiBQYWdlU3RhdGUpOiB2b2lkIHtcbiAgICBpZiAodGhpcy5jb25maWcuZGVidWcpIHtcbiAgICAgIHRoaXMuZGVidWdQYWdlU3RhdGUocGFnZVN0YXRlLCAnQXR0ZW1wdGluZyB0byByZWFwcGx5IHNjcm9sbCBwb3NpdGlvbnMgYWZ0ZXIgYSBwb3BzdGF0ZSBuYXZpZ2F0aW9uIChiYWNrd2FyZCBvciBmb3J3YXJkKS4nKTtcbiAgICB9XG5cbiAgICBpZiAodGhpcy5vYmplY3RJc0VtcHR5KHBhZ2VTdGF0ZSkpIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyBMZXQncyBjcmVhdGUgYSBjb3B5IG9mIHRoZSBwYWdlIHN0YXRlIHNvIHRoYXQgd2UgY2FuIHNhZmVseSBkZWxldGUga2V5cyBmcm9tXG4gICAgLy8gaXQgYXMgd2Ugc3VjY2Vzc2Z1bGx5IGFwcGx5IHRoZW0gdG8gdGhlIHJlbmRlcmVkIERPTS5cbiAgICBjb25zdCBwZW5kaW5nUGFnZVN0YXRlID0geyAuLi5wYWdlU3RhdGUgfTtcblxuICAgIC8vIFNldHVwIHRoZSBzY3JvbGwgcmV0ZW50aW9uIHRpbWVyIG91dHNpZGUgb2YgdGhlIEFuZ3VsYXIgWm9uZSBzbyB0aGF0IGl0XG4gICAgLy8gZG9lc24ndCB0cmlnZ2VyIGFueSBhZGRpdGlvbmFsIGNoYW5nZS1kZXRlY3Rpb24gZGlnZXN0cy5cbiAgICB0aGlzLnpvbmUucnVuT3V0c2lkZUFuZ3VsYXIoXG4gICAgICAoKTogdm9pZCA9PiB7XG4gICAgICAgIGNvbnN0IHN0YXJ0ZWRBdCA9IERhdGUubm93KCk7XG5cbiAgICAgICAgdGhpcy5hcHBseVN0YXRlVG9Eb21UaW1lciA9IHNldEludGVydmFsKFxuICAgICAgICAgICgpID0+IHtcbiAgICAgICAgICAgIGZvciAoY29uc3Qgc2VsZWN0b3IgaW4gcGVuZGluZ1BhZ2VTdGF0ZSkge1xuICAgICAgICAgICAgICBjb25zdCB0YXJnZXQgPSBEb21VdGlscy5zZWxlY3Qoc2VsZWN0b3IpO1xuXG4gICAgICAgICAgICAgIC8vIElmIHRoZSB0YXJnZXQgZWxlbWVudCBkb2Vzbid0IGV4aXN0IGluIHRoZSBET00geWV0LCBpdFxuICAgICAgICAgICAgICAvLyBjb3VsZCBiZSBhbiBpbmRpY2F0aW9uIG9mIGFzeW5jaHJvbm91cyBsb2FkaW5nIGFuZFxuICAgICAgICAgICAgICAvLyByZW5kZXJpbmcuIE1vdmUgb250byB0aGUgbmV4dCBzZWxlY3RvciB3aGlsZSB3ZSBzdGlsbFxuICAgICAgICAgICAgICAvLyBoYXZlIHRpbWUuXG4gICAgICAgICAgICAgIGlmICghdGFyZ2V0KSB7XG4gICAgICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgICAvLyBJZiB0aGUgZWxlbWVudCBpbiBxdWVzdGlvbiBoYXMgYmVlbiBzY3JvbGxlZCAoYnkgdGhlIHVzZXIpXG4gICAgICAgICAgICAgIC8vIHdoaWxlIHdlJ3JlIGF0dGVtcHRpbmcgdG8gcmVpbnN0YXRlIHRoZSBwcmV2aW91cyBzY3JvbGxcbiAgICAgICAgICAgICAgLy8gb2Zmc2V0cywgdGhlbiBpZ25vcmUgdGhpcyBzdGF0ZSAtIHRoZSB1c2VyJ3MgYWN0aW9uIHNob3VsZFxuICAgICAgICAgICAgICAvLyB0YWtlIHByZWNlZGVuY2UuXG4gICAgICAgICAgICAgIGlmICh0aGlzLnNjcm9sbGVkRWxlbWVudHMuaGFzKHRhcmdldCkpIHtcbiAgICAgICAgICAgICAgICBkZWxldGUgKHBlbmRpbmdQYWdlU3RhdGVbc2VsZWN0b3JdKTtcbiAgICAgICAgICAgICAgICAvLyBPdGhlcndpc2UsIGxldCdzIHRyeSB0byByZXN0b3JlIHRoZSBzY3JvbGwgZm9yIHRoZSB0YXJnZXQuXG4gICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgY29uc3Qgc2Nyb2xsVG9wID0gcGVuZGluZ1BhZ2VTdGF0ZVtzZWxlY3Rvcl07XG4gICAgICAgICAgICAgICAgY29uc3QgcmVzdWx0YW50U2Nyb2xsVG9wID0gRG9tVXRpbHMuc2Nyb2xsVG8odGFyZ2V0LCBzY3JvbGxUb3ApO1xuXG4gICAgICAgICAgICAgICAgLy8gSWYgdGhlIGF0dGVtcHQgdG8gcmVzdG9yZSB0aGUgZWxlbWVudCB0byBpdHMgcHJldmlvdXNcbiAgICAgICAgICAgICAgICAvLyBvZmZzZXQgcmVzdWx0ZWQgaW4gYSBtYXRjaCwgdGhlbiBzdG9wIHRyYWNraW5nIHRoaXNcbiAgICAgICAgICAgICAgICAvLyBlbGVtZW50LiBPdGhlcndpc2UsIHdlJ2xsIGNvbnRpbnVlIHRvIHRyeSBhbmQgc2Nyb2xsXG4gICAgICAgICAgICAgICAgLy8gaXQgaW4gdGhlIHN1YnNlcXVlbnQgdGljay5cbiAgICAgICAgICAgICAgICAvLyAtLVxuICAgICAgICAgICAgICAgIC8vIE5PVEU6IFdlIGNvbnRpbnVlIHRvIHRyeSBhbmQgdXBkYXRlIGl0IGJlY2F1c2UgdGhlXG4gICAgICAgICAgICAgICAgLy8gdGFyZ2V0IGVsZW1lbnQgbWF5IGV4aXN0IGluIHRoZSBET00gYnV0IGFsc28gYmVcbiAgICAgICAgICAgICAgICAvLyBsb2FkaW5nIGFzeW5jaHJvbm91cyBkYXRhIHRoYXQgaXMgcmVxdWlyZWQgZm9yIHRoZVxuICAgICAgICAgICAgICAgIC8vIHByZXZpb3VzIHNjcm9sbCBvZmZzZXQuXG4gICAgICAgICAgICAgICAgaWYgKHJlc3VsdGFudFNjcm9sbFRvcCA9PT0gc2Nyb2xsVG9wKSB7XG4gICAgICAgICAgICAgICAgICBkZWxldGUgKHBlbmRpbmdQYWdlU3RhdGVbc2VsZWN0b3JdKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gSWYgdGhlcmUgYXJlIG5vIG1vcmUgZWxlbWVudHMgdG8gc2Nyb2xsIG9yLCB3ZSd2ZSBleGNlZWRlZCBvdXJcbiAgICAgICAgICAgIC8vIHBvbGwgZHVyYXRpb24sIHRoZW4gc3RvcCB3YXRjaGluZyB0aGUgRE9NLlxuICAgICAgICAgICAgaWYgKHRoaXMub2JqZWN0SXNFbXB0eShwZW5kaW5nUGFnZVN0YXRlKVxuICAgICAgICAgICAgICB8fCAoKERhdGUubm93KCkgLSBzdGFydGVkQXQpID49IHRoaXMuY29uZmlnLnBvbGxEdXJhdGlvbiEpXG4gICAgICAgICAgICApIHtcbiAgICAgICAgICAgICAgY2xlYXJUaW1lb3V0KHRoaXMuYXBwbHlTdGF0ZVRvRG9tVGltZXIpO1xuICAgICAgICAgICAgICBpZiAodGhpcy5jb25maWcuZGVidWcpIHtcbiAgICAgICAgICAgICAgICBpZiAodGhpcy5vYmplY3RJc0VtcHR5KHBlbmRpbmdQYWdlU3RhdGUpKSB7XG4gICAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnJWMgU3VjY2Vzc2Z1bGx5IHJlYXBwbGllZCBhbGwgcmVjb3JkZWQgc2Nyb2xsIHBvc2l0aW9ucyB0byB0aGUgRE9NLicsICdjb2xvcjogIzJlY2M3MScpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICBjb25zb2xlLndhcm4oYENvdWxkIG5vdCByZWFwcGx5IGZvbGxvd2luZyByZWNvcmRlZCBzY3JvbGwgcG9zaXRpb25zIHRvIHRoZSBET00gYWZ0ZXIgYSBwb2xsIGR1cmF0aW9uIG9mOiAke3RoaXMuY29uZmlnLnBvbGxEdXJhdGlvbn0gbWlsbGlzZWNvbmRzOmApO1xuICAgICAgICAgICAgICAgICAgdGhpcy5kZWJ1Z1BhZ2VTdGF0ZShwZW5kaW5nUGFnZVN0YXRlKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIHRoaXMuY29uZmlnLnBvbGxDYWRlbmNlXG4gICAgICAgICk7XG4gICAgICB9XG4gICAgKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBJIGdldCB0aGUgcGFnZSBzdGF0ZSBmcm9tIHRoZSBnaXZlbiBzZXQgb2Ygbm9kZXMuIFRoaXMgZXh0cmFjdHMgdGhlIENTUyBzZWxlY3RvcnMgYW5kIG9mZnNldHMgZnJvbSB0aGUgcmVjb3JkZWQgZWxlbWVudHMuXG4gICAqL1xuICBwcml2YXRlIGdldFBhZ2VTdGF0ZUZyb21Ob2Rlcyhub2RlczogU2V0PFRhcmdldD4pOiBQYWdlU3RhdGUge1xuICAgIGNvbnN0IHBhZ2VTdGF0ZTogUGFnZVN0YXRlID0ge307XG5cbiAgICBub2Rlcy5mb3JFYWNoKFxuICAgICAgdGFyZ2V0ID0+IHtcbiAgICAgICAgLy8gR2VuZXJhdGUgYSBDU1Mgc2VsZWN0b3IgZnJvbSB0aGUgZ2l2ZW4gdGFyZ2V0LlxuICAgICAgICAvLyAtLVxuICAgICAgICAvLyBUT0RPOiBSaWdodCBub3csIHRoaXMgYWxnb3JpdGhtIGNyZWF0ZXMgdGhlIHNlbGVjdG9yIGJ5IHdhbGtpbmcgdXAgdGhlXG4gICAgICAgIC8vIERPTSB0cmVlIGFuZCB1c2luZyB0aGUgc2ltdWxhdGVkIGVuY2Fwc3VsYXRpb24gYXR0cmlidXRlcy4gQnV0LCBpdFxuICAgICAgICAvLyB3b3VsZCBiZSBjb29sIHRvIGhhdmUgYSBjb25maWd1cmF0aW9uIG9wdGlvbiB0aGF0IHRlbGxzIHRoaXMgYWxnb3JpdGhtXG4gICAgICAgIC8vIHRvIGxvb2sgZm9yIGEgc3BlY2lmaWMgaWQtcHJlZml4IG9yIGF0dHJpYnV0ZSBvciBzb21ldGhpbmcuIFRoaXMgd291bGRcbiAgICAgICAgLy8gcmVxdWlyZSB0aGUgZGV2ZWxvcGVyIHRvIHByb3ZpZGUgdGhvc2U7IGJ1dCBpdCB3b3VsZCBiZSBvcHRpbWFsLlxuICAgICAgICBjb25zdCBzZWxlY3RvciA9IERvbVV0aWxzLmdldFNlbGVjdG9yKHRhcmdldCk7XG5cbiAgICAgICAgLy8gSWYgdGhlIGdpdmVuIFRhcmdldCBpcyBubyBsb25nZXIgcGFydCBvZiB0aGUgYWN0aXZlIERPTSwgdGhlIHNlbGVjdG9yXG4gICAgICAgIC8vIHdpbGwgYmUgbnVsbC5cbiAgICAgICAgaWYgKHNlbGVjdG9yKSB7XG4gICAgICAgICAgcGFnZVN0YXRlW3NlbGVjdG9yXSA9IERvbVV0aWxzLmdldFNjcm9sbFRvcCh0YXJnZXQpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgKTtcbiAgICByZXR1cm4gcGFnZVN0YXRlO1xuICB9XG5cbiAgLyoqXG4gICAqIEkgZGV0ZXJtaW5lIGlmIHRoZSBnaXZlbiBvYmplY3QgaXMgZW1wdHkgKGllLCBoYXMgbm8ga2V5cykuXG4gICAqL1xuICBwcml2YXRlIG9iamVjdElzRW1wdHkob2JqZWN0OiBPYmplY3QpOiBib29sZWFuIHtcbiAgICBmb3IgKGNvbnN0IGtleSBpbiBvYmplY3QpIHtcbiAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG4gIH1cblxuICAvLyBUaGUgZ29hbCBvZiB0aGUgTmF2aWdhdGlvblN0YXJ0IGV2ZW50IGlzIHRvIHRha2UgY2hhbmdlcyB0aGF0IGhhdmUgYmVlbiBtYWRlXG4gIC8vIHRvIHRoZSBjdXJyZW50IERPTSBhbmQgc3RvcmUgdGhlbSBpbiB0aGUgcmVuZGVyLXN0YXRlIHRyZWUgc28gdGhleSBjYW4gYmVcbiAgLy8gcmVpbnN0YXRlZCBhdCBhIGZ1dHVyZSBkYXRlLlxuICBwcml2YXRlIGhhbmRsZU5hdmlnYXRpb25TdGFydChldmVudDogTmF2aWdhdGlvblN0YXJ0KTogdm9pZCB7XG4gICAgdGhpcy5sYXN0TmF2aWdhdGlvblN0YXJ0QXQgPSBEYXRlLm5vdygpO1xuXG4gICAgLy8gR2V0IHRoZSBuYXZpZ2F0aW9uIElEIGFuZCB0aGUgcmVzdG9yZWQgbmF2aWdhdGlvbiBJRCBmb3IgdXNlIGluIHRoZVxuICAgIC8vIE5hdmlnYXRpb25FbmQgZXZlbnQgaGFuZGxlci5cbiAgICB0aGlzLm5hdmlnYXRpb25JRCA9IGV2ZW50LmlkO1xuICAgIC8qKlxuICAgICAqIE1heWJlIGluIGZ1dHVyZSB1cGRhdGUgQHRvZG86IHVzZSBuZ3gtbmF2aWdhdGlvbi10cmlnZ2VyIGhlcmUsIGxpa2U6IFxuICAgICAqIChldmVudC5yZXN0b3JlZFN0YXRlICYmIHRoaXMud2hlblNob3VsZFNjcm9sbFBvc2l0aW9uQmVSZXN0b3JlZC5oYXModGhpcy5uYXZpZ2F0aW9uVHJpZ2dlcikpXG4gICAgICovXG4gICAgdGhpcy5yZXN0b3JlZE5hdmlnYXRpb25JRCA9IGV2ZW50LnJlc3RvcmVkU3RhdGUgPyBldmVudC5yZXN0b3JlZFN0YXRlLm5hdmlnYXRpb25JZCA6IG51bGw7XG5cbiAgICAvLyBJZiB0aGUgdXNlciBpcyBuYXZpZ2F0aW5nIGF3YXkgZnJvbSB0aGUgY3VycmVudCB2aWV3LCBraWxsIGFueSB0aW1lcnMgdGhhdFxuICAgIC8vIG1heSBiZSB0cnlpbmcgdG8gcmVpbnN0YXRlIGEgcGFnZS1zdGF0ZS5cbiAgICBjbGVhclRpbWVvdXQodGhpcy5hcHBseVN0YXRlVG9Eb21UaW1lcik7XG5cbiAgICAvLyBCZWZvcmUgd2UgbmF2aWdhdGUgYXdheSBmcm9tIHRoZSBjdXJyZW50IHBhZ2Ugc3RhdGUsIGxldCdzIGNvbW1pdCBhbnlcbiAgICAvLyBzY3JvbGwtZWxlbWVudHMgdG8gdGhlIGN1cnJlbnQgcGFnZSBzdGF0ZS5cbiAgICBPYmplY3QuYXNzaWduKFxuICAgICAgdGhpcy5jdXJyZW50UGFnZVN0YXRlLFxuICAgICAgdGhpcy5nZXRQYWdlU3RhdGVGcm9tTm9kZXModGhpcy5zY3JvbGxlZEVsZW1lbnRzKVxuICAgICk7XG5cbiAgICB0aGlzLnNjcm9sbGVkRWxlbWVudHMuY2xlYXIoKTtcblxuICAgIGlmICh0aGlzLmNvbmZpZy5kZWJ1Zykge1xuICAgICAgdGhpcy5kZWJ1Z1BhZ2VTdGF0ZSh0aGlzLmN1cnJlbnRQYWdlU3RhdGUsICdSZWNvcmRlZCBzY3JvbGwgcG9zaXRpb25zLicpO1xuICAgIH1cbiAgfTtcblxuICAvLyBUaGUgcHJpbWFyeSBnb2FsIG9mIHRoZSBOYXZpZ2F0aW9uRW5kIGV2ZW50IGlzIHRvIHJlaW5zdGF0ZSBhIGNhY2hlZCBwYWdlXG4gIC8vIHN0YXRlIGluIHRoZSBldmVudCB0aGF0IHRoZSBuYXZpZ2F0aW9uIGlzIHJlc3RvcmluZyBhIHByZXZpb3VzbHkgcmVuZGVyZWQgcGFnZVxuICAvLyBhcyB0aGUgcmVzdWx0IG9mIGEgcG9wc3RhdGUgZXZlbnQgKGV4LCB0aGUgdXNlciBoaXQgdGhlIEJhY2sgb3IgRm9yd2FyZFxuICAvLyBidXR0b25zKS5cbiAgcHJpdmF0ZSBoYW5kbGVOYXZpZ2F0aW9uRW5kKCk6IHZvaWQge1xuXG4gICAgY29uc3QgcHJldmlvdXNQYWdlU3RhdGUgPSB0aGlzLmN1cnJlbnRQYWdlU3RhdGU7XG5cbiAgICAvLyBOb3cgdGhhdCB3ZSBrbm93IHRoZSBuYXZpZ2F0aW9uIHdhcyBzdWNjZXNzZnVsLCBsZXQncyBzdGFydCBhbmQgc3RvcmUgYVxuICAgIC8vIG5ldyBwYWdlIHN0YXRlIHRvIHRyYWNrIGZ1dHVyZSBzY3JvbGxpbmcuXG4gICAgdGhpcy5jdXJyZW50UGFnZVN0YXRlID0gdGhpcy5wYWdlU3RhdGVzW3RoaXMubmF2aWdhdGlvbklEXSA9IHt9O1xuXG4gICAgLy8gV2hpbGUgd2UgYXJlIGdvaW5nIHRvIHRyYWNrIGVsZW1lbnRzIHRoYXQgd2lsbCBiZSBzY3JvbGxlZCBkdXJpbmcgdGhlXG4gICAgLy8gY3VycmVudCBwYWdlIHJlbmRlcmluZywgaXQgaXMgcG9zc2libGUgdGhhdCB0aGVyZSBhcmUgZWxlbWVudHMgdGhhdCB3ZXJlXG4gICAgLy8gc2Nyb2xsZWQgZHVyaW5nIGEgcHJpb3IgcGFnZSByZW5kZXJpbmcgdGhhdCBzdGlsbCBleGlzdCBvbiB0aGUgcGFnZSwgYnV0XG4gICAgLy8gd2VyZSBub3Qgc2Nyb2xsZWQgcmVjZW50bHkgKHN1Y2ggYXMgYSBzZWNvbmRhcnkgcm91dGVyLW91dGxldCkuIEFzIHN1Y2gsXG4gICAgLy8gbGV0J3MgbG9vayBhdCB0aGUgcHJldmlvdXMgcGFnZSBzdGF0ZSBhbmQgJ3B1bGwgZm9yd2FyZCcgYW55IHN0YXRlIHRoYXRcbiAgICAvLyBzdGlsbCBwZXJ0YWlucyB0byB0aGUgY3VycmVudCBwYWdlLlxuICAgIGlmICghdGhpcy5yZXN0b3JlZE5hdmlnYXRpb25JRCkge1xuICAgICAgZm9yIChjb25zdCBzZWxlY3RvciBpbiBwcmV2aW91c1BhZ2VTdGF0ZSkge1xuICAgICAgICBjb25zdCB0YXJnZXQgPSBEb21VdGlscy5zZWxlY3Qoc2VsZWN0b3IpO1xuXG4gICAgICAgIC8vIE9ubHkgcHVsbCB0aGUgc2VsZWN0b3IgZm9yd2FyZCBpZiBpdCBjb3JyZXNwb25kcyB0byBhbiBlbGVtZW50XG4gICAgICAgIC8vIHRoYXQgc3RpbGwgZXhpc3RzIGluIHRoZSByZW5kZXJlZCBwYWdlLlxuICAgICAgICBpZiAoIXRhcmdldCkge1xuICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gT25seSBwdWxsIHRoZSBzZWxlY3RvciBmb3J3YXJkIGlmIHRoZSB0YXJnZXQgaXMgc3RpbGwgYXQgdGhlIHNhbWVcbiAgICAgICAgLy8gb2Zmc2V0IGFmdGVyIHRoZSBuYXZpZ2F0aW9uIGhhcyB0YWtlbiBwbGFjZS4gSW4gb3RoZXIgd29yZHMsIGlmXG4gICAgICAgIC8vIHRoZSBvZmZzZXQgaGFzIHNvbWVob3cgY2hhbmdlZCBpbiBiZXR3ZWVuIHRoZSBOYXZpZ2F0aW9uU3RhcnQgYW5kXG4gICAgICAgIC8vIE5hdmlnYXRpb25FbmQgZXZlbnRzLCB0aGVuIGlnbm9yZSBpdC4gVG8gYmUgaG9uZXN0LCB0aGlzIHJlYWxseVxuICAgICAgICAvLyBvbmx5IGFwcGxpZXMgdG8gdGhlIFdJTkRPVywgd2hpY2ggY2FuIGNoYW5nZSBpbiBvZmZzZXQgZHVlIHRvIHRoZVxuICAgICAgICAvLyBjaGFuZ2UgaW4gd2hhdCB0aGUgUm91dGVyIGlzIGFjdGl2ZWx5IHJlbmRlcmluZyBpbiB0aGUgRE9NLlxuICAgICAgICBpZiAoRG9tVXRpbHMuZ2V0U2Nyb2xsVG9wKHRhcmdldCkgIT09IHByZXZpb3VzUGFnZVN0YXRlW3NlbGVjdG9yXSkge1xuICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG5cbiAgICAgICAgdGhpcy5jdXJyZW50UGFnZVN0YXRlW3NlbGVjdG9yXSA9IHByZXZpb3VzUGFnZVN0YXRlW3NlbGVjdG9yXTtcblxuICAgICAgICBpZiAodGhpcy5jb25maWcuZGVidWcpIHtcbiAgICAgICAgICBjb25zb2xlLmdyb3VwKCdQdWxsaW5nIHNjcm9sbCBwb3NpdGlvbiBmcm9tIHByZXZpb3VzIHBhZ2Ugc3RhdGUgaW4gY3VycmVudCBwYWdlIHN0YXRlLicpO1xuICAgICAgICAgIGNvbnNvbGUubG9nKHtcbiAgICAgICAgICAgIHNlbGVjdG9yLFxuICAgICAgICAgICAgc2Nyb2xsUG9zaXRpb246IHRoaXMuY3VycmVudFBhZ2VTdGF0ZVtzZWxlY3Rvcl1cbiAgICAgICAgICB9KTtcbiAgICAgICAgICBjb25zb2xlLmdyb3VwRW5kKCk7XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgLy8gSWYgd2UncmUgcmVzdG9yaW5nIGEgcHJldmlvdXMgcGFnZSBzdGF0ZSBBTkQgd2UgaGF2ZSB0aGF0IHByZXZpb3VzIHBhZ2VcbiAgICAgIC8vIHN0YXRlIGNhY2hlZCBpbi1tZW1vcnksIGxldCdzIGNvcHkgdGhlIHByZXZpb3VzIHN0YXRlIGFuZCB0aGVuIHJlc3RvcmUgdGhlXG4gICAgICAvLyBvZmZzZXRzIGluIHRoZSBET00uXG4gICAgfSBlbHNlIGlmICh0aGlzLnJlc3RvcmVkTmF2aWdhdGlvbklEICYmIHRoaXMucGFnZVN0YXRlc1t0aGlzLnJlc3RvcmVkTmF2aWdhdGlvbklEXSkge1xuXG4gICAgICAvLyBOT1RFOiBXZSdyZSBjb3B5aW5nIHRoZSBvZmZzZXRzIGZyb20gdGhlIHJlc3RvcmVkIHN0YXRlIGludG8gdGhlXG4gICAgICAvLyBjdXJyZW50IHN0YXRlIGluc3RlYWQgb2YganVzdCBzd2FwcGluZyB0aGUgcmVmZXJlbmNlcyBiZWNhdXNlIHRoZXNlXG4gICAgICAvLyBuYXZpZ2F0aW9ucyBhcmUgZGlmZmVyZW50IGluIHRoZSBSb3V0ZXIgaGlzdG9yeS4gU2luY2UgZWFjaCBuYXZpZ2F0aW9uXG4gICAgICAvLyAtIGltcGVyYXRpdmUgb3IgcG9wc3RhdGUgLSBnZXRzIGEgdW5pcXVlIElELCB3ZSBuZXZlciB0cnVseSAnZ28gYmFjaydcbiAgICAgIC8vIGluIGhpc3Rvcnk7IHRoZSBSb3V0ZXIgb25seSAnZ29lcyBmb3J3YXJkJywgd2l0aCB0aGUgbm90aW9uIHRoYXQgd2UncmVcbiAgICAgIC8vIHJlY3JlYXRpbmcgYSBwcmV2aW91cyBzdGF0ZSBzb21ldGltZXMuXG4gICAgICB0aGlzLmFwcGx5UGFnZVN0YXRlVG9Eb20oXG4gICAgICAgIE9iamVjdC5hc3NpZ24oXG4gICAgICAgICAgdGhpcy5jdXJyZW50UGFnZVN0YXRlLFxuICAgICAgICAgIHRoaXMucGFnZVN0YXRlc1t0aGlzLnJlc3RvcmVkTmF2aWdhdGlvbklEXVxuICAgICAgICApXG4gICAgICApO1xuICAgIH1cblxuICAgIC8vIEtlZXAgdHJhY2sgb2YgdGhlIG5hdmlnYXRpb24gZXZlbnQgc28gd2UgY2FuIGxpbWl0IHRoZSBzaXplIG9mIG91clxuICAgIC8vIGluLW1lbW9yeSBwYWdlIHN0YXRlIGNhY2hlLlxuICAgIHRoaXMubmF2aWdhdGlvbklEcy5wdXNoKHRoaXMubmF2aWdhdGlvbklEKTtcblxuICAgIC8vIFRyaW0gdGhlIG9sZGVzdCBwYWdlIHN0YXRlcyBhcyB3ZSBnbyBzbyB0aGF0IHRoZSBpbi1tZW1vcnkgY2FjaGUgZG9lc24ndFxuICAgIC8vIGdyb3csIHVuYm91bmRlZC5cbiAgICB3aGlsZSAodGhpcy5uYXZpZ2F0aW9uSURzLmxlbmd0aCA+IHRoaXMubWF4aW11bU51bWJlck9mQ2FjaGVkUGFnZVN0YXRlcykge1xuICAgICAgZGVsZXRlICh0aGlzLnBhZ2VTdGF0ZXNbdGhpcy5uYXZpZ2F0aW9uSURzLnNoaWZ0KCkgYXMgbnVtYmVyXSk7XG4gICAgfVxuICB9O1xuXG4gIC8qKlxuICAgKiBJIGJpbmQgdG8gdGhlIHNjcm9sbCBldmVudCBhbmQga2VlcCB0cmFjayBvZiBhbnkgZWxlbWVudHMgdGhhdCBhcmUgc2Nyb2xsZWQgaW4gdGhlIHJlbmRlcmVkIGRvY3VtZW50LlxuICAgKi9cbiAgcHJpdmF0ZSBzZXR1cFNjcm9sbEJpbmRpbmcoKTogdm9pZCB7XG5cbiAgICAvKipcbiAgICAgKiBNYXliZSBAdG9kbzogWW91IHNob3VsZCB0cnkgdG8gZmluZCBhIHdheSB0byBnZXQgc2Nyb2xsYWJsZSAoc2Nyb2xsZWQpIGVsZW1lbnRzIG9ubHkgZHVyaW5nIE5hdmlnYXRpb25TdGFydC4gXG4gICAgICogQWR2YW50YWdlczpcbiAgICAgKiAtIEJldHRlciBwZXJmb3JtYW5jZTogbm8gbmVlZCB0byBsaXN0ZW4gdG8gdGhlIHNjcm9sbCBldmVudCB0aGUgd2hvbGUgdGltZS5cbiAgICAgKiAtIFNvbWUgZWxlbWVudHMgbWlnaHQgYmUgYWRkZWQgdG8gdGhlIGBzY3JvbGxlZEVsZW1lbnRzYCBhcmUgbm90IHBhcnQgb2YgdGhlIERPTSBhbnkgbW9yZS5cbiAgICAgKiBEaXNhdmFudGFnZXM6XG4gICAgICogLSBkdXJpbmcgTmF2aWdhdGlvblN0YXJ0IHNjcm9sbGFibGUgZWxlbWVudHMgdGhhdCBhcmUgbWF5YmUgcHJlc2VudCBhZnRlciB0aGUgaW50aWFsaXphdGlvbiBvZiBwYWdlIChiZWZvcmUgYW55IHVzZXItaW50ZXJhY3Rpb25zIHRoYXQgY2FuIHJlbW92ZSB0aGVtKSBtaWdodCBiZSBub3QgcGFydCBET00gYW55IG1vcmUuXG4gICAgICogXG4gICAgICovXG4gICAgLy8gQWRkIHNjcm9sbC1iaW5kaW5nIG91dHNpZGUgb2YgdGhlIEFuZ3VsYXIgWm9uZSBzbyBpdCBkb2Vzbid0IHRyaWdnZXIgYW55XG4gICAgLy8gYWRkaXRpb25hbCBjaGFuZ2UtZGV0ZWN0aW9uIGRpZ2VzdHMuXG4gICAgdGhpcy56b25lLnJ1bk91dHNpZGVBbmd1bGFyKCgpID0+IHtcbiAgICAgIC8vIFdoZW4gbmF2aWdhdGluZywgdGhlIGJyb3dzZXIgZW1pdHMgc29tZSBzY3JvbGwgZXZlbnRzIGFzIHRoZSBET00gXG4gICAgICAvLyAoRG9jdW1lbnQgT2JqZWN0IE1vZGVsKSBjaGFuZ2VzIHNoYXBlIGluIGEgd2F5IHRoYXQgZm9yY2VzIHRoZSB2YXJpb3VzXG4gICAgICAvLyBzY3JvbGwgb2Zmc2V0cyB0byBjaGFuZ2UuIFNpbmNlIHRoZXNlIHNjcm9sbCBldmVudHMgYXJlIG5vdCBpbmRpY2F0aXZlXG4gICAgICAvLyBvZiBhIHVzZXIncyBhY3R1YWwgc2Nyb2xsaW5nIGludGVudCwgd2UncmUgZ29pbmcgdG8gaWdub3JlIHRoZW0uIFRoaXNcbiAgICAgIC8vIG5lZWRzIHRvIGJlIGRvbmUgb24gYm90aCBzaWRlcyBvZiB0aGUgbmF2aWdhdGlvbiBldmVudCAoZm9yIHJlYXNvbnNcbiAgICAgIC8vIHRoYXQgYXJlIG5vdCBmdWxseSBvYnZpb3VzIG9yIGxvZ2ljYWwgLS0gYmFzaWNhbGx5LCB0aGUgd2luZG93J3NcbiAgICAgIC8vIHNjcm9sbCBjaGFuZ2VzIGF0IGEgdGltZSB0aGF0IGlzIG5vdCBlYXN5IHRvIHRhcCBpbnRvKS4gSWdub3JpbmcgdGhlc2VcbiAgICAgIC8vIHNjcm9sbCBldmVudHMgaXMgaW1wb3J0YW50IGJlY2F1c2UgdGhlIHBvbHlmaWxseSBzdG9wcyB0cnlpbmcgdG9cbiAgICAgIC8vIHJlaW5zdGF0ZSBhIHNjcm9sbC1vZmZzZXQgaWYgaXQgc2VlcyB0aGF0IHRoZSBnaXZlbiBlbGVtZW50IGhhc1xuICAgICAgLy8gYWxyZWFkeSBiZWVuIHNjcm9sbGVkIGR1cmluZyB0aGUgY3VycmVudCByZW5kZXJpbmcuXG4gICAgICBjb25zdCBzY3JvbGxCdWZmZXJXaW5kb3cgPSAxMDA7XG4gICAgICBsZXQgdGFyZ2V0OiBUYXJnZXQgfCBudWxsO1xuXG4gICAgICB3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihcbiAgICAgICAgJ3Njcm9sbCcsXG4gICAgICAgIGV2ZW50ID0+IHtcblxuICAgICAgICAgIC8vIElmIHRoZSBzY3JvbGw